diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /editor/libeditor/tests | |
parent | Initial commit. (diff) | |
download | firefox-upstream/124.0.1.tar.xz firefox-upstream/124.0.1.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'editor/libeditor/tests')
370 files changed, 62268 insertions, 0 deletions
diff --git a/editor/libeditor/tests/browser.toml b/editor/libeditor/tests/browser.toml new file mode 100644 index 0000000000..ac18b6eefc --- /dev/null +++ b/editor/libeditor/tests/browser.toml @@ -0,0 +1,10 @@ +[DEFAULT] +skip-if = ["os == 'android'"] + +["browser_bug527935.js"] +support-files = [ + "bug527935.html", + "bug527935_2.html" +] + +["browser_content_command_insert_text.js"] diff --git a/editor/libeditor/tests/browser_bug527935.js b/editor/libeditor/tests/browser_bug527935.js new file mode 100644 index 0000000000..cece783491 --- /dev/null +++ b/editor/libeditor/tests/browser_bug527935.js @@ -0,0 +1,78 @@ +add_task(async function () { + await new Promise(resolve => waitForFocus(resolve, window)); + + const kPageURL = + "http://example.org/browser/editor/libeditor/tests/bug527935.html"; + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: kPageURL, + }, + async function (aBrowser) { + var popupShown = false; + function listener() { + popupShown = true; + } + SpecialPowers.addAutoCompletePopupEventListener( + window, + "popupshowing", + listener + ); + + await SpecialPowers.spawn(aBrowser, [], async function () { + var window = content.window.wrappedJSObject; + var document = window.document; + var formTarget = document.getElementById("formTarget"); + var initValue = document.getElementById("initValue"); + + window.loadPromise = new Promise(resolve => { + formTarget.onload = resolve; + }); + + initValue.focus(); + initValue.value = "foo"; + }); + + EventUtils.synthesizeKey("KEY_Enter"); + + await SpecialPowers.spawn(aBrowser, [], async function () { + var window = content.window.wrappedJSObject; + var document = window.document; + + await window.loadPromise; + + var newInput = document.createElement("input"); + newInput.setAttribute("name", "test"); + document.body.appendChild(newInput); + + var event = new window.KeyboardEvent("keypress", { + bubbles: true, + cancelable: true, + view: null, + keyCode: 0, + charCode: "f".charCodeAt(0), + }); + newInput.value = ""; + newInput.focus(); + newInput.dispatchEvent(event); + }); + + await new Promise(resolve => hitEventLoop(resolve, 100)); + + ok(!popupShown, "Popup must not be opened"); + SpecialPowers.removeAutoCompletePopupEventListener( + window, + "popupshowing", + listener + ); + } + ); +}); + +function hitEventLoop(func, times) { + if (times > 0) { + setTimeout(hitEventLoop, 0, func, times - 1); + } else { + setTimeout(func, 0); + } +} diff --git a/editor/libeditor/tests/browser_content_command_insert_text.js b/editor/libeditor/tests/browser_content_command_insert_text.js new file mode 100644 index 0000000000..073b6f830e --- /dev/null +++ b/editor/libeditor/tests/browser_content_command_insert_text.js @@ -0,0 +1,271 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { CustomizableUITestUtils } = ChromeUtils.importESModule( + "resource://testing-common/CustomizableUITestUtils.sys.mjs" +); +const { ContentTaskUtils } = ChromeUtils.importESModule( + "resource://testing-common/ContentTaskUtils.sys.mjs" +); +const gCUITestUtils = new CustomizableUITestUtils(window); +const gDOMWindowUtils = EventUtils._getDOMWindowUtils(window); + +add_task(async function test_setup() { + await gCUITestUtils.addSearchBar(); + registerCleanupFunction(() => { + gCUITestUtils.removeSearchBar(); + }); +}); + +function promiseResettingSearchBarAndFocus() { + const waitForFocusInSearchBar = BrowserTestUtils.waitForEvent( + BrowserSearch.searchBar.textbox, + "focus" + ); + BrowserSearch.searchBar.textbox.focus(); + BrowserSearch.searchBar.textbox.value = ""; + return Promise.all([ + waitForFocusInSearchBar, + TestUtils.waitForCondition( + () => + gDOMWindowUtils.IMEStatus === Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED && + gDOMWindowUtils.inputContextOrigin === + Ci.nsIDOMWindowUtils.INPUT_CONTEXT_ORIGIN_MAIN + ), + ]); +} + +function promiseIMEStateEnabledByRemote() { + return TestUtils.waitForCondition( + () => + gDOMWindowUtils.IMEStatus === Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED && + gDOMWindowUtils.inputContextOrigin === + Ci.nsIDOMWindowUtils.INPUT_CONTEXT_ORIGIN_CONTENT + ); +} + +function promiseContentTick(browser) { + return SpecialPowers.spawn(browser, [], async () => { + await new Promise(r => { + content.requestAnimationFrame(() => { + content.requestAnimationFrame(r); + }); + }); + }); +} + +add_task(async function test_text_editor_in_chrome() { + await promiseResettingSearchBarAndFocus(); + + let events = []; + function logEvent(event) { + events.push(event); + } + BrowserSearch.searchBar.textbox.addEventListener("beforeinput", logEvent); + gDOMWindowUtils.sendContentCommandEvent("insertText", null, "XYZ"); + + is( + BrowserSearch.searchBar.textbox.value, + "XYZ", + "The string should be inserted into the focused search bar" + ); + is( + events.length, + 1, + "One beforeinput event should be fired in the searchbar" + ); + is(events[0]?.inputType, "insertText", 'inputType should be "insertText"'); + is(events[0]?.data, "XYZ", 'inputType should be "XYZ"'); + is(events[0]?.cancelable, true, "beforeinput event should be cancelable"); + BrowserSearch.searchBar.textbox.removeEventListener("beforeinput", logEvent); + + BrowserSearch.searchBar.textbox.blur(); +}); + +add_task(async function test_text_editor_in_content() { + for (const test of [ + { + tag: "input", + target: "input", + nonTarget: "input + input", + page: 'data:text/html,<input value="abc"><input value="def">', + }, + { + tag: "textarea", + target: "textarea", + nonTarget: "textarea + textarea", + page: "data:text/html,<textarea>abc</textarea><textarea>def</textarea>", + }, + ]) { + // Once, move focus to chrome's searchbar. + await promiseResettingSearchBarAndFocus(); + + await BrowserTestUtils.withNewTab(test.page, async browser => { + await SpecialPowers.spawn(browser, [test], async function (aTest) { + content.window.focus(); + await ContentTaskUtils.waitForCondition(() => + content.document.hasFocus() + ); + const target = content.document.querySelector(aTest.target); + target.focus(); + target.selectionStart = target.selectionEnd = 2; + content.document.documentElement.scrollTop; // Flush pending things + }); + + await promiseIMEStateEnabledByRemote(); + const waitForBeforeInputEvent = SpecialPowers.spawn( + browser, + [test], + async function (aTest) { + await new Promise(resolve => { + content.document.querySelector(aTest.target).addEventListener( + "beforeinput", + event => { + is( + event.inputType, + "insertText", + `The inputType of beforeinput event fired on <${aTest.target}> should be "insertText"` + ); + is( + event.data, + "XYZ", + `The data of beforeinput event fired on <${aTest.target}> should be "XYZ"` + ); + is( + event.cancelable, + true, + `The beforeinput event fired on <${aTest.target}> should be cancelable` + ); + resolve(); + }, + { once: true } + ); + }); + } + ); + const waitForInputEvent = BrowserTestUtils.waitForContentEvent( + browser, + "input" + ); + await promiseContentTick(browser); // Ensure "input" event listener in the remote process + gDOMWindowUtils.sendContentCommandEvent("insertText", null, "XYZ"); + await waitForBeforeInputEvent; + await waitForInputEvent; + + await SpecialPowers.spawn(browser, [test], async function (aTest) { + is( + content.document.querySelector(aTest.target).value, + "abXYZc", + `The string should be inserted into the focused <${aTest.target}> element` + ); + is( + content.document.querySelector(aTest.nonTarget).value, + "def", + `The string should not be inserted into the non-focused <${aTest.nonTarget}> element` + ); + }); + }); + + is( + BrowserSearch.searchBar.textbox.value, + "", + "The string should not be inserted into the previously focused search bar" + ); + } +}); + +add_task(async function test_html_editor_in_content() { + for (const test of [ + { + mode: "contenteditable", + target: "div", + page: "data:text/html,<div contenteditable>abc</div>", + }, + { + mode: "designMode", + target: "div", + page: "data:text/html,<div>abc</div>", + }, + ]) { + // Once, move focus to chrome's searchbar. + await promiseResettingSearchBarAndFocus(); + + await BrowserTestUtils.withNewTab(test.page, async browser => { + await SpecialPowers.spawn(browser, [test], async function (aTest) { + content.window.focus(); + await ContentTaskUtils.waitForCondition(() => + content.document.hasFocus() + ); + const target = content.document.querySelector(aTest.target); + if (aTest.mode == "designMode") { + content.document.designMode = "on"; + content.window.focus(); + } else { + target.focus(); + } + content.window.getSelection().collapse(target.firstChild, 2); + content.document.documentElement.scrollTop; // Flush pending things + }); + + await promiseIMEStateEnabledByRemote(); + const waitForBeforeInputEvent = SpecialPowers.spawn( + browser, + [test], + async function (aTest) { + await new Promise(resolve => { + const eventTarget = + aTest.mode === "designMode" + ? content.document + : content.document.querySelector(aTest.target); + eventTarget.addEventListener( + "beforeinput", + event => { + is( + event.inputType, + "insertText", + `The inputType of beforeinput event fired on ${aTest.mode} editor should be "insertText"` + ); + is( + event.data, + "XYZ", + `The data of beforeinput event fired on ${aTest.mode} editor should be "XYZ"` + ); + is( + event.cancelable, + true, + `The beforeinput event fired on ${aTest.mode} editor should be cancelable` + ); + resolve(); + }, + { once: true } + ); + }); + } + ); + const waitForInputEvent = BrowserTestUtils.waitForContentEvent( + browser, + "input" + ); + await promiseContentTick(browser); // Ensure "input" event listener in the remote process + gDOMWindowUtils.sendContentCommandEvent("insertText", null, "XYZ"); + await waitForBeforeInputEvent; + await waitForInputEvent; + + await SpecialPowers.spawn(browser, [test], async function (aTest) { + is( + content.document.querySelector(aTest.target).innerHTML, + "abXYZc", + `The string should be inserted into the focused ${aTest.mode} editor` + ); + }); + }); + + is( + BrowserSearch.searchBar.textbox.value, + "", + "The string should not be inserted into the previously focused search bar" + ); + } +}); diff --git a/editor/libeditor/tests/browserscope/lib/richtext/LICENSE b/editor/libeditor/tests/browserscope/lib/richtext/LICENSE new file mode 100644 index 0000000000..57bc88a15a --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/editor/libeditor/tests/browserscope/lib/richtext/README b/editor/libeditor/tests/browserscope/lib/richtext/README new file mode 100644 index 0000000000..a3bc3110f4 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext/README @@ -0,0 +1,58 @@ +README FOR BROWSERSCOPE +----------------------- + +Hey there - thanks for downloading the code. This file has instructions +for getting setup so that you can run the codebase locally. + +This project is built on Google App Engine using the +Django web application framework and written in Python. + +To get started, you'll need to first download the App Engine SDK at: +http://code.google.com/appengine/downloads.html + +For local development, just startup the server: +./pathto/google_appengine/dev_appserver.py --port=8080 browserscope + +You should then be able to access the local application at: +http://localhost:8080/ + +Note: the first time you hit the homepage it may take a little +while - that's because it's trying to read out median times for all +of the tests from a nonexistent datastore and write to memcache. +Just be a lil patient. + +You can run the unit tests at: + http://localhost:8080/test + + +CONTRIBUTING +------------------ + +Most likely you are interested in adding new tests or creating +a new test category. If you are interested in adding tests to an existing +"category" you may want to get in touch with the maintainer for that +branch of the tree. We are really looking forward to receiving your +code in patch format. Currently the category maintainers are: +Network: Steve Souders <souders@gmail.com> +Reflow: Lindsey Simon <elsigh@gmail.com> +Security: Adam Barth <adam@adambarth.com> and Collin Jackson <collin@collinjackson.com> + + +To create a completely new test category: + * Copy one of the existing directories in categories/ + * Edit your test_set.py, handlers.py + * Add your files in templates/ and static/ + * Update urls.py and settings.CATEGORIES + * Follow the examples of other tests re: + * beaconing using/testdriver_base + * your GetScoreAndDisplayValue method + * your GetRowScoreAndDisplayValue method + +References: + * App Engine Docs - http://code.google.com/appengine/docs/python/overview.html + * App Engine Group - http://groups.google.com/group/google-appengine + * Python Docs - http://www.python.org/doc/ + * Django - http://www.djangoproject.com/ + + + diff --git a/editor/libeditor/tests/browserscope/lib/richtext/README.Mozilla b/editor/libeditor/tests/browserscope/lib/richtext/README.Mozilla new file mode 100644 index 0000000000..5d304943f7 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext/README.Mozilla @@ -0,0 +1,17 @@ +The BrowserScope project provides a set of cross-browser HTML editor tests, +which we import in our test suite in order to run them as part of our +continuous integration system. + +We pull tests occasionally from their Subversion repository using the pull +script which can be found in this directory. We also record the revision ID +which we've used in the current_revision file inside this directory. + +Using the pull script is quite easy, just switch to this directory, and say: + +sh update_from_upstream + +There are tests which we're currently failing on, and there will probably be +more of those in the future. We should maintain a list of the failing tests +manually in currentStatus.js (which can also be found in this directory), to +make sure that the suite passes entirely, with failing tests marked as todo +items. diff --git a/editor/libeditor/tests/browserscope/lib/richtext/currentStatus.js b/editor/libeditor/tests/browserscope/lib/richtext/currentStatus.js new file mode 100644 index 0000000000..4b645d9db4 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext/currentStatus.js @@ -0,0 +1,43 @@ +/** + * This file lists the tests in the BrowserScope suite which we are currently + * failing. We mark them as todo items to keep track of them. + */ + +var knownFailures = { + // Dummy result items. There is one for each category. + 'apply' : { + '0-undefined' : true + }, + 'unapply' : { + '0-undefined' : true + }, + 'change' : { + '0-undefined' : true + }, + 'query' : { + '0-undefined' : true + }, + 'a' : { + 'createbookmark-0' : true, + 'decreasefontsize-0' : true, + 'fontsize-1' : true, + 'subscript-1' : true, + 'superscript-1' : true, + }, + 'u': { + 'removeformat-1' : true, + 'removeformat-2' : true, + 'strikethrough-2' : true, + 'subscript-1' : true, + 'superscript-1' : true, + 'unbookmark-0' : true, + }, + 'q': { + 'fontsize-1' : true, + 'fontsize-2' : true, + }, +}; + +function isKnownFailure(type, test, param) { + return (type in knownFailures) && knownFailures[type][test + "-" + param]; +} diff --git a/editor/libeditor/tests/browserscope/lib/richtext/current_revision b/editor/libeditor/tests/browserscope/lib/richtext/current_revision new file mode 100644 index 0000000000..1e25699145 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext/current_revision @@ -0,0 +1 @@ +775 diff --git a/editor/libeditor/tests/browserscope/lib/richtext/richtext/editable.html b/editor/libeditor/tests/browserscope/lib/richtext/richtext/editable.html new file mode 100644 index 0000000000..a294f0b56b --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext/richtext/editable.html @@ -0,0 +1,11 @@ +<html>
+<head>
+ <script>
+ function load(){
+ window.document.designMode = "On";
+ }
+ </script>
+</head>
+<body contentEditable="true" onload="load()">
+</body>
+</html>
\ No newline at end of file diff --git a/editor/libeditor/tests/browserscope/lib/richtext/richtext/js/range.js b/editor/libeditor/tests/browserscope/lib/richtext/richtext/js/range.js new file mode 100644 index 0000000000..edd23f86b9 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext/richtext/js/range.js @@ -0,0 +1,1069 @@ +var goog$global = this, goog$isString = function(val) { + return typeof val == "string" +}; +Math.floor(Math.random() * 2147483648).toString(36); +var goog$now = Date.now || function() { + return(new Date).getTime() +}, goog$inherits = function(childCtor, parentCtor) { + function tempCtor() { + } + tempCtor.prototype = parentCtor.prototype; + childCtor.superClass_ = parentCtor.prototype; + childCtor.prototype = new tempCtor +};var goog$array$peek = function(array) { + return array[array.length - 1] +}, goog$array$indexOf = function(arr, obj, opt_fromIndex) { + if(arr.indexOf)return arr.indexOf(obj, opt_fromIndex); + if(Array.indexOf)return Array.indexOf(arr, obj, opt_fromIndex); + for(var fromIndex = opt_fromIndex == null ? 0 : opt_fromIndex < 0 ? Math.max(0, arr.length + opt_fromIndex) : opt_fromIndex, i = fromIndex;i < arr.length;i++)if(i in arr && arr[i] === obj)return i; + return-1 +}, goog$array$map = function(arr, f, opt_obj) { + if(arr.map)return arr.map(f, opt_obj); + if(Array.map)return Array.map(arr, f, opt_obj); + for(var l = arr.length, res = [], resLength = 0, arr2 = goog$isString(arr) ? arr.split("") : arr, i = 0;i < l;i++)if(i in arr2)res[resLength++] = f.call(opt_obj, arr2[i], i, arr); + return res +}, goog$array$some = function(arr, f, opt_obj) { + if(arr.some)return arr.some(f, opt_obj); + if(Array.some)return Array.some(arr, f, opt_obj); + for(var l = arr.length, arr2 = goog$isString(arr) ? arr.split("") : arr, i = 0;i < l;i++)if(i in arr2 && f.call(opt_obj, arr2[i], i, arr))return true; + return false +}, goog$array$every = function(arr, f, opt_obj) { + if(arr.every)return arr.every(f, opt_obj); + if(Array.every)return Array.every(arr, f, opt_obj); + for(var l = arr.length, arr2 = goog$isString(arr) ? arr.split("") : arr, i = 0;i < l;i++)if(i in arr2 && !f.call(opt_obj, arr2[i], i, arr))return false; + return true +}, goog$array$find = function(arr, f, opt_obj) { + var i; + JSCompiler_inline_label_goog$array$findIndex_12: { + for(var JSCompiler_inline_l = arr.length, JSCompiler_inline_arr2 = goog$isString(arr) ? arr.split("") : arr, JSCompiler_inline_i = 0;JSCompiler_inline_i < JSCompiler_inline_l;JSCompiler_inline_i++)if(JSCompiler_inline_i in JSCompiler_inline_arr2 && f.call(opt_obj, JSCompiler_inline_arr2[JSCompiler_inline_i], JSCompiler_inline_i, arr)) { + i = JSCompiler_inline_i; + break JSCompiler_inline_label_goog$array$findIndex_12 + }i = -1 + }return i < 0 ? null : goog$isString(arr) ? arr.charAt(i) : arr[i] +};var goog$string$trim = function(str) { + return str.replace(/^[\s\xa0]+|[\s\xa0]+$/g, "") +}, goog$string$htmlEscape = function(str, opt_isLikelyToContainHtmlChars) { + if(opt_isLikelyToContainHtmlChars)return str.replace(goog$string$amperRe_, "&").replace(goog$string$ltRe_, "<").replace(goog$string$gtRe_, ">").replace(goog$string$quotRe_, """); + else { + if(!goog$string$allRe_.test(str))return str; + if(str.includes("&"))str = str.replace(goog$string$amperRe_, "&"); + if(str.includes("<"))str = str.replace(goog$string$ltRe_, "<"); + if(str.includes(">"))str = str.replace(goog$string$gtRe_, ">"); + if(str.includes('"'))str = str.replace(goog$string$quotRe_, """); + return str + } +}, goog$string$amperRe_ = /&/g, goog$string$ltRe_ = /</g, goog$string$gtRe_ = />/g, goog$string$quotRe_ = /\"/g, goog$string$allRe_ = /[&<>\"]/, goog$string$contains = function(s, ss) { + return s.includes(ss) +}, goog$string$compareVersions = function(version1, version2) { + for(var order = 0, v1Subs = goog$string$trim(String(version1)).split("."), v2Subs = goog$string$trim(String(version2)).split("."), subCount = Math.max(v1Subs.length, v2Subs.length), subIdx = 0;order == 0 && subIdx < subCount;subIdx++) { + var v1Sub = v1Subs[subIdx] || "", v2Sub = v2Subs[subIdx] || "", v1CompParser = new RegExp("(\\d*)(\\D*)", "g"), v2CompParser = new RegExp("(\\d*)(\\D*)", "g"); + do { + var v1Comp = v1CompParser.exec(v1Sub) || ["", "", ""], v2Comp = v2CompParser.exec(v2Sub) || ["", "", ""]; + if(v1Comp[0].length == 0 && v2Comp[0].length == 0)break; + var v1CompNum = v1Comp[1].length == 0 ? 0 : parseInt(v1Comp[1], 10), v2CompNum = v2Comp[1].length == 0 ? 0 : parseInt(v2Comp[1], 10); + order = goog$string$compareElements_(v1CompNum, v2CompNum) || goog$string$compareElements_(v1Comp[2].length == 0, v2Comp[2].length == 0) || goog$string$compareElements_(v1Comp[2], v2Comp[2]) + }while(order == 0) + }return order +}, goog$string$compareElements_ = function(left, right) { + if(left < right)return-1; + else if(left > right)return 1; + return 0 +}; +goog$now();var goog$userAgent$detectedOpera_, goog$userAgent$detectedIe_, goog$userAgent$detectedWebkit_, goog$userAgent$detectedMobile_, goog$userAgent$detectedGecko_, goog$userAgent$detectedCamino_, goog$userAgent$detectedMac_, goog$userAgent$detectedWindows_, goog$userAgent$detectedLinux_, goog$userAgent$detectedX11_, goog$userAgent$getUserAgentString = function() { + return goog$global.navigator ? goog$global.navigator.userAgent : null +}, goog$userAgent$getNavigator = function() { + return goog$global.navigator +}; +goog$userAgent$detectedCamino_ = goog$userAgent$detectedGecko_ = goog$userAgent$detectedMobile_ = goog$userAgent$detectedWebkit_ = goog$userAgent$detectedIe_ = goog$userAgent$detectedOpera_ = false; +var JSCompiler_inline_ua_15; +if(JSCompiler_inline_ua_15 = goog$userAgent$getUserAgentString()) { + var JSCompiler_inline_navigator$$1_16 = goog$userAgent$getNavigator(); + goog$userAgent$detectedOpera_ = JSCompiler_inline_ua_15.indexOf("Opera") == 0; + goog$userAgent$detectedIe_ = !goog$userAgent$detectedOpera_ && JSCompiler_inline_ua_15.includes("MSIE"); + goog$userAgent$detectedMobile_ = (goog$userAgent$detectedWebkit_ = !goog$userAgent$detectedOpera_ && JSCompiler_inline_ua_15.includes("WebKit")) && JSCompiler_inline_ua_15.includes("Mobile"); + goog$userAgent$detectedCamino_ = (goog$userAgent$detectedGecko_ = !goog$userAgent$detectedOpera_ && !goog$userAgent$detectedWebkit_ && JSCompiler_inline_navigator$$1_16.product == "Gecko") && JSCompiler_inline_navigator$$1_16.vendor == "Camino" +}var goog$userAgent$OPERA = goog$userAgent$detectedOpera_, goog$userAgent$IE = goog$userAgent$detectedIe_, goog$userAgent$GECKO = goog$userAgent$detectedGecko_, goog$userAgent$WEBKIT = goog$userAgent$detectedWebkit_, goog$userAgent$MOBILE = goog$userAgent$detectedMobile_, goog$userAgent$PLATFORM, JSCompiler_inline_navigator$$2_19 = goog$userAgent$getNavigator(); +goog$userAgent$PLATFORM = JSCompiler_inline_navigator$$2_19 && JSCompiler_inline_navigator$$2_19.platform || ""; +goog$userAgent$detectedMac_ = goog$string$contains(goog$userAgent$PLATFORM, "Mac"); +goog$userAgent$detectedWindows_ = goog$string$contains(goog$userAgent$PLATFORM, "Win"); +goog$userAgent$detectedLinux_ = goog$string$contains(goog$userAgent$PLATFORM, "Linux"); +goog$userAgent$detectedX11_ = !!goog$userAgent$getNavigator() && goog$string$contains(goog$userAgent$getNavigator().appVersion || "", "X11"); +var goog$userAgent$VERSION, JSCompiler_inline_version$$6_26 = "", JSCompiler_inline_re$$2_27; +if(goog$userAgent$OPERA && goog$global.opera) { + var JSCompiler_inline_operaVersion_28 = goog$global.opera.version; + JSCompiler_inline_version$$6_26 = typeof JSCompiler_inline_operaVersion_28 == "function" ? JSCompiler_inline_operaVersion_28() : JSCompiler_inline_operaVersion_28 +}else { + if(goog$userAgent$GECKO)JSCompiler_inline_re$$2_27 = /rv\:([^\);]+)(\)|;)/; + else if(goog$userAgent$IE)JSCompiler_inline_re$$2_27 = /MSIE\s+([^\);]+)(\)|;)/; + else if(goog$userAgent$WEBKIT)JSCompiler_inline_re$$2_27 = /WebKit\/(\S+)/; + if(JSCompiler_inline_re$$2_27) { + var JSCompiler_inline_arr$$41_29 = JSCompiler_inline_re$$2_27.exec(goog$userAgent$getUserAgentString()); + JSCompiler_inline_version$$6_26 = JSCompiler_inline_arr$$41_29 ? JSCompiler_inline_arr$$41_29[1] : "" + } +}goog$userAgent$VERSION = JSCompiler_inline_version$$6_26; +var goog$userAgent$isVersionCache_ = {}, goog$userAgent$isVersion = function(version) { + return goog$userAgent$isVersionCache_[version] || (goog$userAgent$isVersionCache_[version] = goog$string$compareVersions(goog$userAgent$VERSION, version) >= 0) +};var goog$dom$getWindow = function(opt_doc) { + return opt_doc ? goog$dom$getWindow_(opt_doc) : window +}, goog$dom$getWindow_ = function(doc) { + if(doc.parentWindow)return doc.parentWindow; + if(goog$userAgent$WEBKIT && !goog$userAgent$isVersion("500") && !goog$userAgent$MOBILE) { + var scriptElement = doc.createElement("script"); + scriptElement.innerHTML = "document.parentWindow=window"; + var parentElement = doc.documentElement; + parentElement.appendChild(scriptElement); + parentElement.removeChild(scriptElement); + return doc.parentWindow + }return doc.defaultView +}, goog$dom$appendChild = function(parent, child) { + parent.appendChild(child) +}, goog$dom$BAD_CONTAINS_WEBKIT_ = goog$userAgent$WEBKIT && goog$userAgent$isVersion("522"), goog$dom$contains = function(parent, descendant) { + if(typeof parent.contains != "undefined" && !goog$dom$BAD_CONTAINS_WEBKIT_ && descendant.nodeType == 1)return parent == descendant || parent.contains(descendant); + if(typeof parent.compareDocumentPosition != "undefined")return parent == descendant || Boolean(parent.compareDocumentPosition(descendant) & 16); + for(;descendant && parent != descendant;)descendant = descendant.parentNode; + return descendant == parent +}, goog$dom$compareNodeOrder = function(node1, node2) { + if(node1 == node2)return 0; + if(node1.compareDocumentPosition)return node1.compareDocumentPosition(node2) & 2 ? 1 : -1; + if("sourceIndex" in node1 || node1.parentNode && "sourceIndex" in node1.parentNode) { + var isElement1 = node1.nodeType == 1, isElement2 = node2.nodeType == 1; + if(isElement1 && isElement2)return node1.sourceIndex - node2.sourceIndex; + else { + var parent1 = node1.parentNode, parent2 = node2.parentNode; + if(parent1 == parent2)return goog$dom$compareSiblingOrder_(node1, node2); + if(!isElement1 && goog$dom$contains(parent1, node2))return-1 * goog$dom$compareParentsDescendantNodeIe_(node1, node2); + if(!isElement2 && goog$dom$contains(parent2, node1))return goog$dom$compareParentsDescendantNodeIe_(node2, node1); + return(isElement1 ? node1.sourceIndex : parent1.sourceIndex) - (isElement2 ? node2.sourceIndex : parent2.sourceIndex) + } + }var doc = goog$dom$getOwnerDocument(node1), range1, range2; + range1 = doc.createRange(); + range1.selectNode(node1); + range1.collapse(true); + range2 = doc.createRange(); + range2.selectNode(node2); + range2.collapse(true); + return range1.compareBoundaryPoints(goog$global.Range.START_TO_END, range2) +}, goog$dom$compareParentsDescendantNodeIe_ = function(textNode, node) { + var parent = textNode.parentNode; + if(parent == node)return-1; + for(var sibling = node;sibling.parentNode != parent;)sibling = sibling.parentNode; + return goog$dom$compareSiblingOrder_(sibling, textNode) +}, goog$dom$compareSiblingOrder_ = function(node1, node2) { + for(var s = node2;s = s.previousSibling;)if(s == node1)return-1; + return 1 +}, goog$dom$findCommonAncestor = function() { + var i, count = arguments.length; + if(count) { + if(count == 1)return arguments[0] + }else return null; + var paths = [], minLength = Infinity; + for(i = 0;i < count;i++) { + for(var ancestors = [], node = arguments[i];node;) { + ancestors.unshift(node); + node = node.parentNode + }paths.push(ancestors); + minLength = Math.min(minLength, ancestors.length) + }var output = null; + for(i = 0;i < minLength;i++) { + for(var first = paths[0][i], j = 1;j < count;j++)if(first != paths[j][i])return output; + output = first + }return output +}, goog$dom$getOwnerDocument = function(node) { + // Added 'editorDoc' as hack for browsers that don't support node.ownerDocument + return node.nodeType == 9 ? node : node.ownerDocument || node.document || editorDoc +}, goog$dom$DomHelper = function(opt_document) { + this.document_ = opt_document || goog$global.document || document +}; +goog$dom$DomHelper.prototype.getDocument = function() { + return this.document_ +}; +goog$dom$DomHelper.prototype.createElement = function(name) { + return this.document_.createElement(name) +}; +goog$dom$DomHelper.prototype.getWindow = function() { + return goog$dom$getWindow_(this.document_) +}; +goog$dom$DomHelper.prototype.appendChild = goog$dom$appendChild; +goog$dom$DomHelper.prototype.contains = goog$dom$contains;var goog$Disposable = function() { +};if("StopIteration" in goog$global)var goog$iter$StopIteration = goog$global.StopIteration; +else goog$iter$StopIteration = Error("StopIteration"); +var goog$iter$Iterator = function() { +}; +goog$iter$Iterator.prototype.next = function() { + throw goog$iter$StopIteration; +}; +goog$iter$Iterator.prototype.__iterator__ = function() { + return this +};var goog$debug$exposeException = function(err, opt_fn) { + try { + var e, JSCompiler_inline_href_34; + JSCompiler_inline_label_goog$getObjectByName_61: { + for(var JSCompiler_inline_parts = "window.location.href".split("."), JSCompiler_inline_cur = goog$global, JSCompiler_inline_part;JSCompiler_inline_part = JSCompiler_inline_parts.shift();)if(JSCompiler_inline_cur[JSCompiler_inline_part])JSCompiler_inline_cur = JSCompiler_inline_cur[JSCompiler_inline_part]; + else { + JSCompiler_inline_href_34 = null; + break JSCompiler_inline_label_goog$getObjectByName_61 + }JSCompiler_inline_href_34 = JSCompiler_inline_cur + }e = typeof err == "string" ? {message:err, name:"Unknown error", lineNumber:"Not available", fileName:JSCompiler_inline_href_34, stack:"Not available"} : !err.lineNumber || !err.fileName || !err.stack ? {message:err.message, name:err.name, lineNumber:err.lineNumber || err.line || "Not available", fileName:err.fileName || err.filename || err.sourceURL || JSCompiler_inline_href_34, stack:err.stack || "Not available"} : err; + var error = "Message: " + goog$string$htmlEscape(e.message) + '\nUrl: <a href="view-source:' + e.fileName + '" target="_new">' + e.fileName + "</a>\nLine: " + e.lineNumber + "\n\nBrowser stack:\n" + goog$string$htmlEscape(e.stack + "-> ") + "[end]\n\nJS stack traversal:\n" + goog$string$htmlEscape(goog$debug$getStacktrace(opt_fn) + "-> "); + return error + }catch(e2) { + return"Exception trying to expose exception! You win, we lose. " + e2 + } +}, goog$debug$getStacktrace = function(opt_fn) { + return goog$debug$getStacktraceHelper_(opt_fn || arguments.callee.caller, []) +}, goog$debug$getStacktraceHelper_ = function(fn, visited) { + var sb = [], JSCompiler_inline_result_36; + JSCompiler_inline_label_goog$array$contains_41:JSCompiler_inline_result_36 = visited.contains ? visited.contains(fn) : goog$array$indexOf(visited, fn) > -1; + if(JSCompiler_inline_result_36)sb.push("[...circular reference...]"); + else if(fn && visited.length < 50) { + sb.push(goog$debug$getFunctionName(fn) + "("); + for(var args = fn.arguments, i = 0;i < args.length;i++) { + i > 0 && sb.push(", "); + var argDesc, arg = args[i]; + switch(typeof arg) { + case "object": + argDesc = arg ? "object" : "null"; + break; + case "string": + argDesc = arg; + break; + case "number": + argDesc = String(arg); + break; + case "boolean": + argDesc = arg ? "true" : "false"; + break; + case "function": + argDesc = (argDesc = goog$debug$getFunctionName(arg)) ? argDesc : "[fn]"; + break; + case "undefined": + ; + default: + argDesc = typeof arg; + break + } + if(argDesc.length > 40)argDesc = argDesc.substr(0, 40) + "..."; + sb.push(argDesc) + }visited.push(fn); + sb.push(")\n"); + try { + sb.push(goog$debug$getStacktraceHelper_(fn.caller, visited)) + }catch(e) { + sb.push("[exception trying to get caller]\n") + } + }else fn ? sb.push("[...long stack...]") : sb.push("[end]"); + return sb.join("") +}, goog$debug$getFunctionName = function(fn) { + var functionSource = String(fn); + if(!goog$debug$fnNameCache_[functionSource]) { + var matches = /function ([^\(]+)/.exec(functionSource); + if(matches) { + var method = matches[1]; + goog$debug$fnNameCache_[functionSource] = method + }else goog$debug$fnNameCache_[functionSource] = "[Anonymous]" + }return goog$debug$fnNameCache_[functionSource] +}, goog$debug$fnNameCache_ = {};var goog$debug$LogRecord = function(level, msg, loggerName, opt_time, opt_sequenceNumber) { + this.sequenceNumber_ = typeof opt_sequenceNumber == "number" ? opt_sequenceNumber : goog$debug$LogRecord$nextSequenceNumber_++; + this.time_ = opt_time || goog$now(); + this.level_ = level; + this.msg_ = msg; + this.loggerName_ = loggerName +}; +goog$debug$LogRecord.prototype.exception_ = null; +goog$debug$LogRecord.prototype.exceptionText_ = null; +var goog$debug$LogRecord$nextSequenceNumber_ = 0; +goog$debug$LogRecord.prototype.setException = function(exception) { + this.exception_ = exception +}; +goog$debug$LogRecord.prototype.setExceptionText = function(text) { + this.exceptionText_ = text +}; +goog$debug$LogRecord.prototype.setLevel = function(level) { + this.level_ = level +};var goog$debug$Logger = function(name) { + this.name_ = name; + this.parent_ = null; + this.children_ = {}; + this.handlers_ = [] +}; +goog$debug$Logger.prototype.level_ = null; +var goog$debug$Logger$Level = function(name, value) { + this.name = name; + this.value = value +}; +goog$debug$Logger$Level.prototype.toString = function() { + return this.name +}; +new goog$debug$Logger$Level("OFF", Infinity); +new goog$debug$Logger$Level("SHOUT", 1200); +var goog$debug$Logger$Level$SEVERE = new goog$debug$Logger$Level("SEVERE", 1000), goog$debug$Logger$Level$WARNING = new goog$debug$Logger$Level("WARNING", 900); +new goog$debug$Logger$Level("INFO", 800); +var goog$debug$Logger$Level$CONFIG = new goog$debug$Logger$Level("CONFIG", 700); +new goog$debug$Logger$Level("FINE", 500); +new goog$debug$Logger$Level("FINER", 400); +new goog$debug$Logger$Level("FINEST", 300); +new goog$debug$Logger$Level("ALL", 0); +goog$debug$Logger.prototype.setLevel = function(level) { + this.level_ = level +}; +goog$debug$Logger.prototype.isLoggable = function(level) { + if(this.level_)return level.value >= this.level_.value; + if(this.parent_)return this.parent_.isLoggable(level); + return false +}; +goog$debug$Logger.prototype.log = function(level, msg, opt_exception) { + this.isLoggable(level) && this.logRecord(this.getLogRecord(level, msg, opt_exception)) +}; +goog$debug$Logger.prototype.getLogRecord = function(level, msg, opt_exception) { + var logRecord = new goog$debug$LogRecord(level, String(msg), this.name_); + if(opt_exception) { + logRecord.setException(opt_exception); + logRecord.setExceptionText(goog$debug$exposeException(opt_exception, arguments.callee.caller)) + }return logRecord +}; +goog$debug$Logger.prototype.severe = function(msg, opt_exception) { + this.log(goog$debug$Logger$Level$SEVERE, msg, opt_exception) +}; +goog$debug$Logger.prototype.warning = function(msg, opt_exception) { + this.log(goog$debug$Logger$Level$WARNING, msg, opt_exception) +}; +goog$debug$Logger.prototype.logRecord = function(logRecord) { + if(this.isLoggable(logRecord.level_))for(var target = this;target;) { + target.callPublish_(logRecord); + target = target.parent_ + } +}; +goog$debug$Logger.prototype.callPublish_ = function(logRecord) { + for(var i = 0;i < this.handlers_.length;i++)this.handlers_[i](logRecord) +}; +goog$debug$Logger.prototype.setParent_ = function(parent) { + this.parent_ = parent +}; +goog$debug$Logger.prototype.addChild_ = function(name, logger) { + this.children_[name] = logger +}; +var goog$debug$LogManager$loggers_ = {}, goog$debug$LogManager$rootLogger_ = null, goog$debug$LogManager$getLogger = function(name) { + if(!goog$debug$LogManager$rootLogger_) { + goog$debug$LogManager$rootLogger_ = new goog$debug$Logger(""); + goog$debug$LogManager$loggers_[""] = goog$debug$LogManager$rootLogger_; + goog$debug$LogManager$rootLogger_.setLevel(goog$debug$Logger$Level$CONFIG) + }return name in goog$debug$LogManager$loggers_ ? goog$debug$LogManager$loggers_[name] : goog$debug$LogManager$createLogger_(name) +}, goog$debug$LogManager$createLogger_ = function(name) { + var logger = new goog$debug$Logger(name), parts = name.split("."), leafName = parts[parts.length - 1]; + parts.length = parts.length - 1; + var parentName = parts.join("."), parentLogger = goog$debug$LogManager$getLogger(parentName); + parentLogger.addChild_(leafName, logger); + logger.setParent_(parentLogger); + return goog$debug$LogManager$loggers_[name] = logger +};var goog$dom$SavedRange = function() { + goog$Disposable.call(this) +}; +goog$inherits(goog$dom$SavedRange, goog$Disposable); +goog$debug$LogManager$getLogger("goog.dom.SavedRange");var goog$dom$TagIterator = function(opt_node, opt_reversed, opt_unconstrained, opt_tagType, opt_depth) { + this.reversed = !!opt_reversed; + opt_node && this.setPosition(opt_node, opt_tagType); + this.depth = opt_depth != undefined ? opt_depth : this.tagType || 0; + if(this.reversed)this.depth *= -1; + this.constrained = !opt_unconstrained +}; +goog$inherits(goog$dom$TagIterator, goog$iter$Iterator); +goog$dom$TagIterator.prototype.node = null; +goog$dom$TagIterator.prototype.tagType = null; +goog$dom$TagIterator.prototype.started_ = false; +goog$dom$TagIterator.prototype.setPosition = function(node, opt_tagType, opt_depth) { + if(this.node = node)this.tagType = typeof opt_tagType == "number" ? opt_tagType : this.node.nodeType != 1 ? 0 : this.reversed ? -1 : 1; + if(typeof opt_depth == "number")this.depth = opt_depth +}; +goog$dom$TagIterator.prototype.next = function() { + var node; + if(this.started_) { + if(!this.node || this.constrained && this.depth == 0)throw goog$iter$StopIteration;node = this.node; + var startType = this.reversed ? -1 : 1; + if(this.tagType == startType) { + var child = this.reversed ? node.lastChild : node.firstChild; + child ? this.setPosition(child) : this.setPosition(node, startType * -1) + }else { + var sibling = this.reversed ? node.previousSibling : node.nextSibling; + sibling ? this.setPosition(sibling) : this.setPosition(node.parentNode, startType * -1) + }this.depth += this.tagType * (this.reversed ? -1 : 1) + }else this.started_ = true; + node = this.node; + if(!this.node)throw goog$iter$StopIteration;return node +}; +goog$dom$TagIterator.prototype.isStartTag = function() { + return this.tagType == 1 +};var goog$dom$AbstractRange = function() { +}; +goog$dom$AbstractRange.prototype.getTextRanges = function() { + for(var output = [], i = 0, len = this.getTextRangeCount();i < len;i++)output.push(this.getTextRange(i)); + return output +}; +goog$dom$AbstractRange.prototype.getAnchorNode = function() { + return this.isReversed() ? this.getEndNode() : this.getStartNode() +}; +goog$dom$AbstractRange.prototype.getAnchorOffset = function() { + return this.isReversed() ? this.getEndOffset() : this.getStartOffset() +}; +goog$dom$AbstractRange.prototype.getFocusNode = function() { + return this.isReversed() ? this.getStartNode() : this.getEndNode() +}; +goog$dom$AbstractRange.prototype.getFocusOffset = function() { + return this.isReversed() ? this.getStartOffset() : this.getEndOffset() +}; +goog$dom$AbstractRange.prototype.isReversed = function() { + return false +}; +goog$dom$AbstractRange.prototype.getDocument = function() { + return goog$dom$getOwnerDocument(goog$userAgent$IE ? this.getContainer() : this.getStartNode()) +}; +goog$dom$AbstractRange.prototype.getWindow = function() { + return goog$dom$getWindow(this.getDocument()) +}; +goog$dom$AbstractRange.prototype.containsNode = function(node, opt_allowPartial) { + return this.containsRange(goog$dom$TextRange$createFromNodeContents(node, undefined), opt_allowPartial) +}; +var goog$dom$RangeIterator = function(node, opt_reverse) { + goog$dom$TagIterator.call(this, node, opt_reverse, true) +}; +goog$inherits(goog$dom$RangeIterator, goog$dom$TagIterator);var goog$dom$AbstractMultiRange = function() { +}; +goog$inherits(goog$dom$AbstractMultiRange, goog$dom$AbstractRange); +goog$dom$AbstractMultiRange.prototype.containsRange = function(otherRange, opt_allowPartial) { + var ranges = this.getTextRanges(), otherRanges = otherRange.getTextRanges(), fn = opt_allowPartial ? goog$array$some : goog$array$every; + return fn(otherRanges, function(otherRange) { + return goog$array$some(ranges, function(range) { + return range.containsRange(otherRange, opt_allowPartial) + }) + }) +};var goog$dom$TextRangeIterator = function(startNode, startOffset, endNode, endOffset, opt_reverse) { + var goNext; + if(startNode) { + this.startNode_ = startNode; + this.startOffset_ = startOffset; + this.endNode_ = endNode; + this.endOffset_ = endOffset; + if(startNode.nodeType == 1 && startNode.tagName != "BR") { + var startChildren = startNode.childNodes, candidate = startChildren[startOffset]; + if(candidate) { + this.startNode_ = candidate; + this.startOffset_ = 0 + }else { + if(startChildren.length)this.startNode_ = goog$array$peek(startChildren); + goNext = true + } + }if(endNode.nodeType == 1)if(this.endNode_ = endNode.childNodes[endOffset])this.endOffset_ = 0; + else this.endNode_ = endNode + }goog$dom$RangeIterator.call(this, opt_reverse ? this.endNode_ : this.startNode_, opt_reverse); + if(goNext)try { + this.next() + }catch(e) { + if(e != goog$iter$StopIteration)throw e; + } +}; +goog$inherits(goog$dom$TextRangeIterator, goog$dom$RangeIterator); +goog$dom$TextRangeIterator.prototype.startNode_ = null; +goog$dom$TextRangeIterator.prototype.endNode_ = null; +goog$dom$TextRangeIterator.prototype.startOffset_ = 0; +goog$dom$TextRangeIterator.prototype.endOffset_ = 0; +goog$dom$TextRangeIterator.prototype.getStartNode = function() { + return this.startNode_ +}; +goog$dom$TextRangeIterator.prototype.getEndNode = function() { + return this.endNode_ +}; +goog$dom$TextRangeIterator.prototype.isLast = function() { + return this.started_ && this.node == this.endNode_ && (!this.endOffset_ || !this.isStartTag()) +}; +goog$dom$TextRangeIterator.prototype.next = function() { + if(this.isLast())throw goog$iter$StopIteration;return goog$dom$TextRangeIterator.superClass_.next.call(this) +};var goog$userAgent$jscript$DETECTED_HAS_JSCRIPT_, goog$userAgent$jscript$DETECTED_VERSION_, JSCompiler_inline_hasScriptEngine_44 = "ScriptEngine" in goog$global; +goog$userAgent$jscript$DETECTED_VERSION_ = (goog$userAgent$jscript$DETECTED_HAS_JSCRIPT_ = JSCompiler_inline_hasScriptEngine_44 && goog$global.ScriptEngine() == "JScript") ? goog$global.ScriptEngineMajorVersion() + "." + goog$global.ScriptEngineMinorVersion() + "." + goog$global.ScriptEngineBuildVersion() : "0";var goog$dom$browserrange$AbstractRange = function() { +}; +goog$dom$browserrange$AbstractRange.prototype.containsRange = function(range, opt_allowPartial) { + return this.containsBrowserRange(range.range_, opt_allowPartial) +}; +goog$dom$browserrange$AbstractRange.prototype.containsBrowserRange = function(range, opt_allowPartial) { + try { + return opt_allowPartial ? this.compareBrowserRangeEndpoints(range, 0, 1) >= 0 && this.compareBrowserRangeEndpoints(range, 1, 0) <= 0 : this.compareBrowserRangeEndpoints(range, 0, 0) >= 0 && this.compareBrowserRangeEndpoints(range, 1, 1) <= 0 + }catch(e) { + if(!goog$userAgent$IE)throw e;return false + } +}; +goog$dom$browserrange$AbstractRange.prototype.containsNode = function(node, opt_allowPartial) { + return this.containsRange(goog$userAgent$IE ? goog$dom$browserrange$IeRange$createFromNodeContents(node) : goog$userAgent$WEBKIT ? new goog$dom$browserrange$WebKitRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNode(node)) : goog$userAgent$GECKO ? new goog$dom$browserrange$GeckoRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNode(node)) : new goog$dom$browserrange$W3cRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNode(node)), opt_allowPartial) +}; +goog$dom$browserrange$AbstractRange.prototype.__iterator__ = function() { + return new goog$dom$TextRangeIterator(this.getStartNode(), this.getStartOffset(), this.getEndNode(), this.getEndOffset()) +};var goog$dom$browserrange$W3cRange = function(range) { + this.range_ = range +}; +goog$inherits(goog$dom$browserrange$W3cRange, goog$dom$browserrange$AbstractRange); +var goog$dom$browserrange$W3cRange$getBrowserRangeForNode = function(node) { + var nodeRange = goog$dom$getOwnerDocument(node).createRange(); + if(node.nodeType == 3) { + nodeRange.setStart(node, 0); + nodeRange.setEnd(node, node.length) + }else { + for(var tempNode, leaf = node;tempNode = leaf.firstChild;)leaf = tempNode; + nodeRange.setStart(leaf, 0); + for(leaf = node;tempNode = leaf.lastChild;)leaf = tempNode; + nodeRange.setEnd(leaf, leaf.nodeType == 1 ? leaf.childNodes.length : leaf.length) + }return nodeRange +}, goog$dom$browserrange$W3cRange$getBrowserRangeForNodes_ = function(startNode, startOffset, endNode, endOffset) { + var nodeRange = goog$dom$getOwnerDocument(startNode).createRange(); + nodeRange.setStart(startNode, startOffset); + nodeRange.setEnd(endNode, endOffset); + return nodeRange +}; +goog$dom$browserrange$W3cRange.prototype.getContainer = function() { + return this.range_.commonAncestorContainer +}; +goog$dom$browserrange$W3cRange.prototype.getStartNode = function() { + return this.range_.startContainer +}; +goog$dom$browserrange$W3cRange.prototype.getStartOffset = function() { + return this.range_.startOffset +}; +goog$dom$browserrange$W3cRange.prototype.getEndNode = function() { + return this.range_.endContainer +}; +goog$dom$browserrange$W3cRange.prototype.getEndOffset = function() { + return this.range_.endOffset +}; +goog$dom$browserrange$W3cRange.prototype.compareBrowserRangeEndpoints = function(range, thisEndpoint, otherEndpoint) { + return this.range_.compareBoundaryPoints(otherEndpoint == 1 ? thisEndpoint == 1 ? goog$global.Range.START_TO_START : goog$global.Range.START_TO_END : thisEndpoint == 1 ? goog$global.Range.END_TO_START : goog$global.Range.END_TO_END, range) +}; +goog$dom$browserrange$W3cRange.prototype.isCollapsed = function() { + return this.range_.collapsed +}; +goog$dom$browserrange$W3cRange.prototype.select = function(reverse) { + var win = goog$dom$getWindow(goog$dom$getOwnerDocument(this.getStartNode())); + this.selectInternal(win.getSelection(), reverse) +}; +goog$dom$browserrange$W3cRange.prototype.selectInternal = function(selection) { + selection.addRange(this.range_) +}; +goog$dom$browserrange$W3cRange.prototype.collapse = function(toStart) { + this.range_.collapse(toStart) +};var goog$dom$browserrange$GeckoRange = function(range) { + goog$dom$browserrange$W3cRange.call(this, range) +}; +goog$inherits(goog$dom$browserrange$GeckoRange, goog$dom$browserrange$W3cRange); +goog$dom$browserrange$GeckoRange.prototype.selectInternal = function(selection, reversed) { + var anchorNode = reversed ? this.getEndNode() : this.getStartNode(), anchorOffset = reversed ? this.getEndOffset() : this.getStartOffset(), focusNode = reversed ? this.getStartNode() : this.getEndNode(), focusOffset = reversed ? this.getStartOffset() : this.getEndOffset(); + selection.collapse(anchorNode, anchorOffset); + if(anchorNode != focusNode || anchorOffset != focusOffset)selection.extend(focusNode, focusOffset) +};var goog$dom$browserrange$IeRange = function(range, doc) { + this.range_ = range; + this.doc_ = doc +}; +goog$inherits(goog$dom$browserrange$IeRange, goog$dom$browserrange$AbstractRange); +var goog$dom$browserrange$IeRange$logger_ = goog$debug$LogManager$getLogger("goog.dom.browserrange.IeRange"), goog$dom$browserrange$IeRange$getBrowserRangeForNode_ = function(node) { + var nodeRange = goog$dom$getOwnerDocument(node).body.createTextRange(); + if(node.nodeType == 1)nodeRange.moveToElementText(node); + else { + for(var offset = 0, sibling = node;sibling = sibling.previousSibling;) { + var nodeType = sibling.nodeType; + if(nodeType == 3)offset += sibling.length; + else if(nodeType == 1) { + nodeRange.moveToElementText(sibling); + break + } + }sibling || nodeRange.moveToElementText(node.parentNode); + nodeRange.collapse(!sibling); + offset && nodeRange.move("character", offset); + nodeRange.moveEnd("character", node.length) + }return nodeRange +}, goog$dom$browserrange$IeRange$getBrowserRangeForNodes_ = function(startNode, startOffset, endNode, endOffset) { + var child, collapse = false; + if(startNode.nodeType == 1) { + startOffset > startNode.childNodes.length && goog$dom$browserrange$IeRange$logger_.severe("Cannot have startOffset > startNode child count"); + child = startNode.childNodes[startOffset]; + collapse = !child; + startNode = child || startNode; + startOffset = 0 + }var leftRange = goog$dom$browserrange$IeRange$getBrowserRangeForNode_(startNode); + startOffset && leftRange.move("character", startOffset); + collapse && leftRange.collapse(false); + collapse = false; + if(endNode.nodeType == 1) { + startOffset > startNode.childNodes.length && goog$dom$browserrange$IeRange$logger_.severe("Cannot have endOffset > endNode child count"); + endNode = (child = endNode.childNodes[endOffset]) || endNode; + if(endNode.tagName == "BR")endOffset = 1; + else { + endOffset = 0; + collapse = !child + } + }var rightRange = goog$dom$browserrange$IeRange$getBrowserRangeForNode_(endNode); + rightRange.collapse(!collapse); + endOffset && rightRange.moveEnd("character", endOffset); + leftRange.setEndPoint("EndToEnd", rightRange); + return leftRange +}, goog$dom$browserrange$IeRange$createFromNodeContents = function(node) { + var range = new goog$dom$browserrange$IeRange(goog$dom$browserrange$IeRange$getBrowserRangeForNode_(node), goog$dom$getOwnerDocument(node)); + range.parentNode_ = node; + return range +}; +goog$dom$browserrange$IeRange.prototype.parentNode_ = null; +goog$dom$browserrange$IeRange.prototype.startNode_ = null; +goog$dom$browserrange$IeRange.prototype.endNode_ = null; +goog$dom$browserrange$IeRange.prototype.clearCachedValues_ = function() { + this.parentNode_ = this.startNode_ = this.endNode_ = null +}; +goog$dom$browserrange$IeRange.prototype.getContainer = function() { + if(!this.parentNode_) { + for(var selectText = this.range_.text, i = 1;selectText.charAt(selectText.length - i) == " ";i++)this.range_.moveEnd("character", -1); + for(var parent = this.range_.parentElement(), htmlText = this.range_.htmlText.replace(/(\r\n|\r|\n)+/g, " ");htmlText.length > parent.outerHTML.replace(/(\r\n|\r|\n)+/g, " ").length;)parent = parent.parentNode; + for(;parent.childNodes.length == 1 && parent.innerText == (parent.firstChild.nodeType == 3 ? parent.firstChild.nodeValue : parent.firstChild.innerText);) { + if(parent.firstChild.tagName == "IMG")break; + parent = parent.firstChild + }if(selectText.length == 0)parent = this.findDeepestContainer_(parent); + this.parentNode_ = parent + }return this.parentNode_ +}; +goog$dom$browserrange$IeRange.prototype.findDeepestContainer_ = function(node) { + for(var childNodes = node.childNodes, i = 0, len = childNodes.length;i < len;i++) { + var child = childNodes[i]; + if(child.nodeType == 1)if(this.range_.inRange(goog$dom$browserrange$IeRange$getBrowserRangeForNode_(child)))return this.findDeepestContainer_(child) + }return node +}; +goog$dom$browserrange$IeRange.prototype.getStartNode = function() { + return this.startNode_ || (this.startNode_ = this.getEndpointNode_(1)) +}; +goog$dom$browserrange$IeRange.prototype.getStartOffset = function() { + return this.getOffset_(1) +}; +goog$dom$browserrange$IeRange.prototype.getEndNode = function() { + return this.endNode_ || (this.endNode_ = this.getEndpointNode_(0)) +}; +goog$dom$browserrange$IeRange.prototype.getEndOffset = function() { + return this.getOffset_(0) +}; +goog$dom$browserrange$IeRange.prototype.containsRange = function(range, opt_allowPartial) { + return this.containsBrowserRange(range.range_, opt_allowPartial) +}; +goog$dom$browserrange$IeRange.prototype.compareBrowserRangeEndpoints = function(range, thisEndpoint, otherEndpoint) { + return this.range_.compareEndPoints((thisEndpoint == 1 ? "Start" : "End") + "To" + (otherEndpoint == 1 ? "Start" : "End"), range) +}; +goog$dom$browserrange$IeRange.prototype.getEndpointNode_ = function(endpoint, opt_node) { + var node = opt_node || this.getContainer(); + if(!node || !node.firstChild) { + if(endpoint == 0 && node.previousSibling && node.previousSibling.tagName == "BR" && this.getOffset_(endpoint, node) == 0)node = node.previousSibling; + return node.tagName == "BR" ? node.parentNode : node + }for(var child = endpoint == 1 ? node.firstChild : node.lastChild;child;) { + if(this.containsNode(child, true))return this.getEndpointNode_(endpoint, child); + child = endpoint == 1 ? child.nextSibling : child.previousSibling + }return node +}; +goog$dom$browserrange$IeRange.prototype.getOffset_ = function(endpoint, opt_container) { + var container = opt_container || (endpoint == 1 ? this.getStartNode() : this.getEndNode()); + if(container.nodeType == 1) { + for(var children = container.childNodes, len = children.length, i = endpoint == 1 ? 0 : len - 1;i >= 0 && i < len;) { + var child = children[i]; + if(this.containsNode(child, true)) { + endpoint == 0 && child.previousSibling && child.previousSibling.tagName == "BR" && this.getOffset_(endpoint, child) == 0 && i--; + break + }i += endpoint == 1 ? 1 : -1 + }return i == -1 ? 0 : i + }else { + var range = this.range_.duplicate(), nodeRange = goog$dom$browserrange$IeRange$getBrowserRangeForNode_(container); + range.setEndPoint(endpoint == 1 ? "EndToEnd" : "StartToStart", nodeRange); + var rangeLength = range.text.length; + return endpoint == 0 ? rangeLength : container.length - rangeLength + } +}; +goog$dom$browserrange$IeRange.prototype.isCollapsed = function() { + return this.range_.text == "" +}; +goog$dom$browserrange$IeRange.prototype.select = function() { + this.range_.select() +}; +goog$dom$browserrange$IeRange.prototype.collapse = function(toStart) { + this.range_.collapse(toStart); + if(toStart)this.endNode_ = this.startNode_; + else this.startNode_ = this.endNode_ +};var goog$dom$browserrange$WebKitRange = function(range) { + goog$dom$browserrange$W3cRange.call(this, range) +}; +goog$inherits(goog$dom$browserrange$WebKitRange, goog$dom$browserrange$W3cRange); +goog$dom$browserrange$WebKitRange.prototype.compareBrowserRangeEndpoints = function(range, thisEndpoint, otherEndpoint) { + if(goog$userAgent$isVersion("528"))return goog$dom$browserrange$WebKitRange.superClass_.compareBrowserRangeEndpoints.call(this, range, thisEndpoint, otherEndpoint); + return this.range_.compareBoundaryPoints(otherEndpoint == 1 ? thisEndpoint == 1 ? goog$global.Range.START_TO_START : goog$global.Range.END_TO_START : thisEndpoint == 1 ? goog$global.Range.START_TO_END : goog$global.Range.END_TO_END, range) +}; +goog$dom$browserrange$WebKitRange.prototype.selectInternal = function(selection, reversed) { + selection.removeAllRanges(); + reversed ? selection.setBaseAndExtent(this.getEndNode(), this.getEndOffset(), this.getStartNode(), this.getStartOffset()) : selection.setBaseAndExtent(this.getStartNode(), this.getStartOffset(), this.getEndNode(), this.getEndOffset()) +};var goog$dom$browserrange$createRangeFromNodes = function(startNode, startOffset, endNode, endOffset) { + return goog$userAgent$IE ? new goog$dom$browserrange$IeRange(goog$dom$browserrange$IeRange$getBrowserRangeForNodes_(startNode, startOffset, endNode, endOffset), goog$dom$getOwnerDocument(startNode)) : goog$userAgent$WEBKIT ? new goog$dom$browserrange$WebKitRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNodes_(startNode, startOffset, endNode, endOffset)) : goog$userAgent$GECKO ? new goog$dom$browserrange$GeckoRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNodes_(startNode, startOffset, + endNode, endOffset)) : new goog$dom$browserrange$W3cRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNodes_(startNode, startOffset, endNode, endOffset)) +};var goog$dom$TextRange = function() { +}; +goog$inherits(goog$dom$TextRange, goog$dom$AbstractRange); +var goog$dom$TextRange$createFromBrowserRangeWrapper_ = function(browserRange, opt_isReversed) { + var range = new goog$dom$TextRange; + range.browserRangeWrapper_ = browserRange; + range.isReversed_ = !!opt_isReversed; + return range +}, goog$dom$TextRange$createFromNodeContents = function(node, opt_isReversed) { + return goog$dom$TextRange$createFromBrowserRangeWrapper_(goog$userAgent$IE ? goog$dom$browserrange$IeRange$createFromNodeContents(node) : goog$userAgent$WEBKIT ? new goog$dom$browserrange$WebKitRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNode(node)) : goog$userAgent$GECKO ? new goog$dom$browserrange$GeckoRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNode(node)) : new goog$dom$browserrange$W3cRange(goog$dom$browserrange$W3cRange$getBrowserRangeForNode(node)), opt_isReversed) +}, goog$dom$TextRange$createFromNodes = function(anchorNode, anchorOffset, focusNode, focusOffset) { + var range = new goog$dom$TextRange; + range.isReversed_ = goog$dom$Range$isReversed(anchorNode, anchorOffset, focusNode, focusOffset); + if(anchorNode.tagName == "BR") { + var parent = anchorNode.parentNode; + anchorOffset = goog$array$indexOf(parent.childNodes, anchorNode); + anchorNode = parent + }if(focusNode.tagName == "BR") { + parent = focusNode.parentNode; + focusOffset = goog$array$indexOf(parent.childNodes, focusNode); + focusNode = parent + }if(range.isReversed_) { + range.startNode_ = focusNode; + range.startOffset_ = focusOffset; + range.endNode_ = anchorNode; + range.endOffset_ = anchorOffset + }else { + range.startNode_ = anchorNode; + range.startOffset_ = anchorOffset; + range.endNode_ = focusNode; + range.endOffset_ = focusOffset + }return range +}; +goog$dom$TextRange.prototype.browserRangeWrapper_ = null; +goog$dom$TextRange.prototype.startNode_ = null; +goog$dom$TextRange.prototype.startOffset_ = null; +goog$dom$TextRange.prototype.endNode_ = null; +goog$dom$TextRange.prototype.endOffset_ = null; +goog$dom$TextRange.prototype.isReversed_ = false; +goog$dom$TextRange.prototype.getType = function() { + return"text" +}; +goog$dom$TextRange.prototype.getBrowserRangeObject = function() { + return this.getBrowserRangeWrapper_().range_ +}; +goog$dom$TextRange.prototype.clearCachedValues_ = function() { + this.startNode_ = this.startOffset_ = this.endNode_ = this.endOffset_ = null +}; +goog$dom$TextRange.prototype.getTextRangeCount = function() { + return 1 +}; +goog$dom$TextRange.prototype.getTextRange = function() { + return this +}; +goog$dom$TextRange.prototype.getBrowserRangeWrapper_ = function() { + return this.browserRangeWrapper_ || (this.browserRangeWrapper_ = goog$dom$browserrange$createRangeFromNodes(this.getStartNode(), this.getStartOffset(), this.getEndNode(), this.getEndOffset())) +}; +goog$dom$TextRange.prototype.getContainer = function() { + return this.getBrowserRangeWrapper_().getContainer() +}; +goog$dom$TextRange.prototype.getStartNode = function() { + return this.startNode_ || (this.startNode_ = this.getBrowserRangeWrapper_().getStartNode()) +}; +goog$dom$TextRange.prototype.getStartOffset = function() { + return this.startOffset_ != null ? this.startOffset_ : (this.startOffset_ = this.getBrowserRangeWrapper_().getStartOffset()) +}; +goog$dom$TextRange.prototype.getEndNode = function() { + return this.endNode_ || (this.endNode_ = this.getBrowserRangeWrapper_().getEndNode()) +}; +goog$dom$TextRange.prototype.getEndOffset = function() { + return this.endOffset_ != null ? this.endOffset_ : (this.endOffset_ = this.getBrowserRangeWrapper_().getEndOffset()) +}; +goog$dom$TextRange.prototype.isReversed = function() { + return this.isReversed_ +}; +goog$dom$TextRange.prototype.containsRange = function(otherRange, opt_allowPartial) { + var otherRangeType = otherRange.getType(); + if(otherRangeType == "text")return this.getBrowserRangeWrapper_().containsRange(otherRange.getBrowserRangeWrapper_(), opt_allowPartial); + else if(otherRangeType == "control") { + var elements = otherRange.getElements(), fn = opt_allowPartial ? goog$array$some : goog$array$every; + return fn(elements, function(el) { + return this.containsNode(el, opt_allowPartial) + }, this) + } +}; +goog$dom$TextRange.prototype.isCollapsed = function() { + return this.getBrowserRangeWrapper_().isCollapsed() +}; +goog$dom$TextRange.prototype.__iterator__ = function() { + return new goog$dom$TextRangeIterator(this.getStartNode(), this.getStartOffset(), this.getEndNode(), this.getEndOffset()) +}; +goog$dom$TextRange.prototype.select = function() { + this.getBrowserRangeWrapper_().select(this.isReversed_) +}; +goog$dom$TextRange.prototype.saveUsingDom = function() { + return new goog$dom$DomSavedTextRange_(this) +}; +goog$dom$TextRange.prototype.collapse = function(toAnchor) { + var toStart = this.isReversed() ? !toAnchor : toAnchor; + this.browserRangeWrapper_ && this.browserRangeWrapper_.collapse(toStart); + if(toStart) { + this.endNode_ = this.startNode_; + this.endOffset_ = this.startOffset_ + }else { + this.startNode_ = this.endNode_; + this.startOffset_ = this.endOffset_ + }this.isReversed_ = false +}; +var goog$dom$DomSavedTextRange_ = function(range) { + this.anchorNode_ = range.getAnchorNode(); + this.anchorOffset_ = range.getAnchorOffset(); + this.focusNode_ = range.getFocusNode(); + this.focusOffset_ = range.getFocusOffset() +}; +goog$inherits(goog$dom$DomSavedTextRange_, goog$dom$SavedRange);var goog$dom$ControlRange = function() { +}; +goog$inherits(goog$dom$ControlRange, goog$dom$AbstractMultiRange); +goog$dom$ControlRange.prototype.range_ = null; +goog$dom$ControlRange.prototype.elements_ = null; +goog$dom$ControlRange.prototype.sortedElements_ = null; +goog$dom$ControlRange.prototype.clearCachedValues_ = function() { + this.sortedElements_ = this.elements_ = null +}; +goog$dom$ControlRange.prototype.getType = function() { + return"control" +}; +goog$dom$ControlRange.prototype.getBrowserRangeObject = function() { + return this.range_ || document.body.createControlRange() +}; +goog$dom$ControlRange.prototype.getTextRangeCount = function() { + return this.range_ ? this.range_.length : 0 +}; +goog$dom$ControlRange.prototype.getTextRange = function(i) { + return goog$dom$TextRange$createFromNodeContents(this.range_.item(i)) +}; +goog$dom$ControlRange.prototype.getContainer = function() { + return goog$dom$findCommonAncestor.apply(null, this.getElements()) +}; +goog$dom$ControlRange.prototype.getStartNode = function() { + return this.getSortedElements()[0] +}; +goog$dom$ControlRange.prototype.getStartOffset = function() { + return 0 +}; +goog$dom$ControlRange.prototype.getEndNode = function() { + var sorted = this.getSortedElements(), startsLast = goog$array$peek(sorted); + return goog$array$find(sorted, function(el) { + return goog$dom$contains(el, startsLast) + }) +}; +goog$dom$ControlRange.prototype.getEndOffset = function() { + return this.getEndNode().childNodes.length +}; +goog$dom$ControlRange.prototype.getElements = function() { + if(!this.elements_) { + this.elements_ = []; + if(this.range_)for(var i = 0;i < this.range_.length;i++)this.elements_.push(this.range_.item(i)) + }return this.elements_ +}; +goog$dom$ControlRange.prototype.getSortedElements = function() { + if(!this.sortedElements_) { + this.sortedElements_ = this.getElements().concat(); + this.sortedElements_.sort(function(a, b) { + return a.sourceIndex - b.sourceIndex + }) + }return this.sortedElements_ +}; +goog$dom$ControlRange.prototype.isCollapsed = function() { + return!this.range_ || !this.range_.length +}; +goog$dom$ControlRange.prototype.__iterator__ = function() { + return new goog$dom$ControlRangeIterator(this) +}; +goog$dom$ControlRange.prototype.select = function() { + this.range_ && this.range_.select() +}; +goog$dom$ControlRange.prototype.saveUsingDom = function() { + return new goog$dom$DomSavedControlRange_(this) +}; +goog$dom$ControlRange.prototype.collapse = function() { + this.range_ = null; + this.clearCachedValues_() +}; +var goog$dom$DomSavedControlRange_ = function(range) { + this.elements_ = range.getElements() +}; +goog$inherits(goog$dom$DomSavedControlRange_, goog$dom$SavedRange); +var goog$dom$ControlRangeIterator = function(range) { + if(range) { + this.elements_ = range.getSortedElements(); + this.startNode_ = this.elements_.shift(); + this.endNode_ = goog$array$peek(this.elements_) || this.startNode_ + }goog$dom$RangeIterator.call(this, this.startNode_, false) +}; +goog$inherits(goog$dom$ControlRangeIterator, goog$dom$RangeIterator); +goog$dom$ControlRangeIterator.prototype.startNode_ = null; +goog$dom$ControlRangeIterator.prototype.endNode_ = null; +goog$dom$ControlRangeIterator.prototype.elements_ = null; +goog$dom$ControlRangeIterator.prototype.getStartNode = function() { + return this.startNode_ +}; +goog$dom$ControlRangeIterator.prototype.getEndNode = function() { + return this.endNode_ +}; +goog$dom$ControlRangeIterator.prototype.isLast = function() { + return!this.depth && !this.elements_.length +}; +goog$dom$ControlRangeIterator.prototype.next = function() { + if(this.isLast())throw goog$iter$StopIteration;else if(!this.depth) { + var el = this.elements_.shift(); + this.setPosition(el, 1, 1); + return el + }return goog$dom$ControlRangeIterator.superClass_.next.call(this) +};var goog$dom$MultiRange = function() { + this.browserRanges_ = []; + this.ranges_ = []; + this.container_ = this.sortedRanges_ = null +}; +goog$inherits(goog$dom$MultiRange, goog$dom$AbstractMultiRange); +goog$dom$MultiRange.prototype.logger_ = goog$debug$LogManager$getLogger("goog.dom.MultiRange"); +goog$dom$MultiRange.prototype.clearCachedValues_ = function() { + this.ranges_ = []; + this.container_ = this.sortedRanges_ = null +}; +goog$dom$MultiRange.prototype.getType = function() { + return"mutli" +}; +goog$dom$MultiRange.prototype.getBrowserRangeObject = function() { + this.browserRanges_.length > 1 && this.logger_.warning("getBrowserRangeObject called on MultiRange with more than 1 range"); + return this.browserRanges_[0] +}; +goog$dom$MultiRange.prototype.getTextRangeCount = function() { + return this.browserRanges_.length +}; +goog$dom$MultiRange.prototype.getTextRange = function(i) { + this.ranges_[i] || (this.ranges_[i] = goog$dom$TextRange$createFromBrowserRangeWrapper_(goog$userAgent$IE ? new goog$dom$browserrange$IeRange(this.browserRanges_[i], goog$dom$getOwnerDocument(this.browserRanges_[i].parentElement())) : goog$userAgent$WEBKIT ? new goog$dom$browserrange$WebKitRange(this.browserRanges_[i]) : goog$userAgent$GECKO ? new goog$dom$browserrange$GeckoRange(this.browserRanges_[i]) : new goog$dom$browserrange$W3cRange(this.browserRanges_[i]), undefined)); + return this.ranges_[i] +}; +goog$dom$MultiRange.prototype.getContainer = function() { + if(!this.container_) { + for(var nodes = [], i = 0, len = this.getTextRangeCount();i < len;i++)nodes.push(this.getTextRange(i).getContainer()); + this.container_ = goog$dom$findCommonAncestor.apply(null, nodes) + }return this.container_ +}; +goog$dom$MultiRange.prototype.getSortedRanges = function() { + if(!this.sortedRanges_) { + this.sortedRanges_ = this.getTextRanges(); + this.sortedRanges_.sort(function(a, b) { + var aStartNode = a.getStartNode(), aStartOffset = a.getStartOffset(), bStartNode = b.getStartNode(), bStartOffset = b.getStartOffset(); + if(aStartNode == bStartNode && aStartOffset == bStartOffset)return 0; + return goog$dom$Range$isReversed(aStartNode, aStartOffset, bStartNode, bStartOffset) ? 1 : -1 + }) + }return this.sortedRanges_ +}; +goog$dom$MultiRange.prototype.getStartNode = function() { + return this.getSortedRanges()[0].getStartNode() +}; +goog$dom$MultiRange.prototype.getStartOffset = function() { + return this.getSortedRanges()[0].getStartOffset() +}; +goog$dom$MultiRange.prototype.getEndNode = function() { + return goog$array$peek(this.getSortedRanges()).getEndNode() +}; +goog$dom$MultiRange.prototype.getEndOffset = function() { + return goog$array$peek(this.getSortedRanges()).getEndOffset() +}; +goog$dom$MultiRange.prototype.isCollapsed = function() { + return this.browserRanges_.length == 0 || this.browserRanges_.length == 1 && this.getTextRange(0).isCollapsed() +}; +goog$dom$MultiRange.prototype.__iterator__ = function() { + return new goog$dom$MultiRangeIterator(this) +}; +goog$dom$MultiRange.prototype.select = function() { + var selection; + JSCompiler_inline_label_goog$dom$AbstractRange$getBrowserSelectionForWindow_50: { + var JSCompiler_inline_win = this.getWindow(); + if(JSCompiler_inline_win.getSelection)selection = JSCompiler_inline_win.getSelection(); + else { + var JSCompiler_inline_doc = JSCompiler_inline_win.document; + selection = JSCompiler_inline_doc.selection || JSCompiler_inline_doc.getSelection && JSCompiler_inline_doc.getSelection() + } + }selection.removeAllRanges(); + for(var i = 0, len = this.getTextRangeCount();i < len;i++)selection.addRange(this.getTextRange(i).getBrowserRangeObject()) +}; +goog$dom$MultiRange.prototype.saveUsingDom = function() { + return new goog$dom$DomSavedMultiRange_(this) +}; +goog$dom$MultiRange.prototype.collapse = function(toAnchor) { + if(!this.isCollapsed()) { + var range = toAnchor ? this.getTextRange(0) : this.getTextRange(this.getTextRangeCount() - 1); + this.clearCachedValues_(); + range.collapse(toAnchor); + this.ranges_ = [range]; + this.sortedRanges_ = [range]; + this.browserRanges_ = [range.getBrowserRangeObject()] + } +}; +var goog$dom$DomSavedMultiRange_ = function(range) { + this.savedRanges_ = goog$array$map(range.getTextRanges(), function(range) { + return range.saveUsingDom() + }) +}; +goog$inherits(goog$dom$DomSavedMultiRange_, goog$dom$SavedRange); +var goog$dom$MultiRangeIterator = function(range) { + if(range) { + this.ranges_ = range.getSortedRanges(); + if(this.ranges_.length) { + this.startNode_ = this.ranges_[0].getStartNode(); + this.endNode_ = goog$array$peek(this.ranges_).getEndNode() + } + }goog$dom$RangeIterator.call(this, this.startNode_, false) +}; +goog$inherits(goog$dom$MultiRangeIterator, goog$dom$RangeIterator); +goog$dom$MultiRangeIterator.prototype.startNode_ = null; +goog$dom$MultiRangeIterator.prototype.endNode_ = null; +goog$dom$MultiRangeIterator.prototype.ranges_ = null; +goog$dom$MultiRangeIterator.prototype.getStartNode = function() { + return this.startNode_ +}; +goog$dom$MultiRangeIterator.prototype.getEndNode = function() { + return this.endNode_ +}; +goog$dom$MultiRangeIterator.prototype.isLast = function() { + return this.ranges_.length == 1 && this.ranges_[0].isLast() +}; +goog$dom$MultiRangeIterator.prototype.next = function() { + do try { + this.ranges_[0].next(); + break + }catch(ex) { + if(ex != goog$iter$StopIteration)throw ex;this.ranges_.shift() + }while(this.ranges_.length); + if(this.ranges_.length) { + var range = this.ranges_[0]; + this.setPosition(range.node, range.tagType, range.depth); + return range.node + }else throw goog$iter$StopIteration; +};var goog$dom$Range$createCaret = function(node, offset) { + return goog$dom$TextRange$createFromNodes(node, offset, node, offset) +}, goog$dom$Range$createFromNodes = function(startNode, startOffset, endNode, endOffset) { + return goog$dom$TextRange$createFromNodes(startNode, startOffset, endNode, endOffset) +}, goog$dom$Range$isReversed = function(anchorNode, anchorOffset, focusNode, focusOffset) { + if(anchorNode == focusNode)return focusOffset < anchorOffset; + var child; + if(anchorNode.nodeType == 1 && anchorOffset)if(child = anchorNode.childNodes[anchorOffset]) { + anchorNode = child; + anchorOffset = 0 + }else if(goog$dom$contains(anchorNode, focusNode))return true; + if(focusNode.nodeType == 1 && focusOffset)if(child = focusNode.childNodes[focusOffset]) { + focusNode = child; + focusOffset = 0 + }else if(goog$dom$contains(focusNode, anchorNode))return false; + return(goog$dom$compareNodeOrder(anchorNode, focusNode) || anchorOffset - focusOffset) > 0 +};window.createCaret = goog$dom$Range$createCaret; +window.createFromNodes = goog$dom$Range$createFromNodes; +try { + goog$dom$Range$createCaret(document.body, 0).select() +}catch(e$$13) { +}; + +/************************************************** + Trace: + 56.427 Start Handling request + 0 56.427 Start Building cUnit + 1 56.428 Done 1 ms Building cUnit + 0 56.428 Start Checking memcacheg + 0 56.428 Start Connecting to memcacheg + 8 56.436 Done 8 ms Connecting to memcacheg + 1 56.437 Done 9 ms Checking memcacheg + 0 56.437 Done 10 ms Handling request +**************************************************/ diff --git a/editor/libeditor/tests/browserscope/lib/richtext/richtext/richtext.html b/editor/libeditor/tests/browserscope/lib/richtext/richtext/richtext.html new file mode 100644 index 0000000000..140f1a2045 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext/richtext/richtext.html @@ -0,0 +1,1081 @@ +<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <title>Rich Text Tests</title>
+ <script src="js/range.js"></script>
+ <script>
+ /**
+ * Color class allows cross-browser comparison of values, which can
+ * be returned from queryCommandValue in several formats:
+ * 0xff00ff
+ * rgb(255, 0, 0)
+ * Number containing the hex value
+ */
+ function Color(value) {
+ this.compare = function(other) {
+ if (!this.valid || !other.valid) {
+ return false;
+ }
+ return this.red == other.red && this.green == other.green && this.blue == other.blue;
+ }
+ this.parse = function(value) {
+ var hexMatch = String(value).match(/#([0-9a-f]{6})/i);
+ if (hexMatch) {
+ this.red = parseInt(hexMatch[1].substring(0, 2), 16);
+ this.green = parseInt(hexMatch[1].substring(2, 4), 16);
+ this.blue = parseInt(hexMatch[1].substring(4, 6), 16);
+ return true;
+ }
+ var rgbMatch = String(value).match(/rgb\(([0-9]{1,3}),\s*([0-9]{1,3}),\s*([0-9]{1,3})\)/i);
+ if (rgbMatch) {
+ this.red = Number(rgbMatch[1]);
+ this.green = Number(rgbMatch[2]);
+ this.blue = Number(rgbMatch[3]);
+ return true;
+ }
+ if (Number(value)) {
+ this.red = value & 0xFF;
+ this.green = (value & 0xFF00) >> 8;
+ this.blue = (value & 0xFF0000) >> 16;
+ return true;
+ }
+ return false;
+ }
+ this.toString = function() {
+ return this.red + ',' + this.green + ',' + this.blue;
+ }
+ this.valid = this.parse(value);
+ }
+
+ /**
+ * Utility class for converting font sizes to the size
+ * attribute in a font tag. Currently only converts px because
+ * only the sizes and px ever come from queryCommandValue.
+ */
+ function Size(value) {
+ var pxMatch = String(value).match(/([0-9]+)px/);
+ if (pxMatch) {
+ var px = Number(pxMatch[1]);
+ if (px <= 10) {
+ this.size = 1;
+ } else if (px <= 13) {
+ this.size = 2;
+ } else if (px <= 16) {
+ this.size = 3;
+ } else if (px <= 18) {
+ this.size = 4;
+ } else if (px <= 24) {
+ this.size = 5;
+ } else if (px <= 32) {
+ this.size = 6;
+ } else if (px <= 47) {
+ this.size = 7;
+ } else {
+ this.size = NaN;
+ }
+ } else if (Number(value)) {
+ this.size = Number(value);
+ } else {
+ switch (value) {
+ case 'x-small':
+ this.size = 1;
+ break;
+ case 'small':
+ this.size = 2;
+ break;
+ case 'medium':
+ this.size = 3;
+ break;
+ case 'large':
+ this.size = 4;
+ break;
+ case 'x-large':
+ this.size = 5;
+ break;
+ case 'xx-large':
+ this.size = 6;
+ break;
+ case 'xxx-large':
+ case '-webkit-xxx-large':
+ this.size = 7;
+ break;
+ default:
+ this.size = null;
+ }
+ }
+ this.compare = function(other) {
+ return this.size == other.size;
+ }
+ this.toString = function() {
+ return String(this.size);
+ }
+ }
+
+ var IMAGE_URI = '/tests/editor/libeditor/tests/green.png';
+
+ var APPLY_TESTS = {
+ 'backcolor' : {
+ opt_arg: '#FF0000',
+ styleWithCSS: 'background-color'},
+ 'bold' : {
+ opt_arg: null,
+ styleWithCSS: 'font-weight'},
+ 'createbookmark' : {
+ opt_arg: 'bookmark_name'},
+ 'createlink' : {
+ opt_arg: 'http://www.openweb.org'},
+ 'decreasefontsize' : {
+ opt_arg: null},
+ 'fontname' : {
+ opt_arg: 'Arial',
+ styleWithCSS: 'font-family'},
+ 'fontsize' : {
+ opt_arg: 4,
+ styleWithCSS: 'font-size'},
+ 'forecolor' : {
+ opt_arg: '#FF0000',
+ styleWithCSS: 'color'},
+ 'formatblock' : {
+ opt_arg: 'h1',
+ wholeline: true},
+ 'hilitecolor' : {
+ opt_arg: '#FF0000',
+ styleWithCSS: 'background-color'},
+ 'indent' : {
+ opt_arg: null,
+ wholeline: true,
+ styleWithCSS: 'margin'},
+ 'inserthorizontalrule' : {
+ opt_arg: null,
+ collapse: true},
+ 'inserthtml': {
+ opt_arg: '<br>',
+ collapse: true},
+ 'insertimage': {
+ opt_arg: IMAGE_URI,
+ collapse: true},
+ 'insertorderedlist' : {
+ opt_arg: null,
+ wholeline: true},
+ 'insertunorderedlist' : {
+ opt_arg: null,
+ wholeline: true},
+ 'italic' : {
+ opt_arg: null,
+ styleWithCSS: 'font-style'},
+ 'justifycenter' : {
+ opt_arg: null,
+ wholeline: true,
+ styleWithCSS: 'text-align'},
+ 'justifyfull' : {
+ opt_arg: null,
+ wholeline: true,
+ styleWithCSS: 'text-align'},
+ 'justifyleft' : {
+ opt_arg: null,
+ wholeline: true,
+ styleWithCSS: 'text-align'},
+ 'justifyright' : {
+ opt_arg: null,
+ wholeline: true,
+ styleWithCSS: 'text-align'},
+ 'strikethrough' : {
+ opt_arg: null,
+ styleWithCSS: 'text-decoration'},
+ 'subscript' : {
+ opt_arg: null,
+ styleWithCSS: 'vertical-align'},
+ 'superscript' : {
+ opt_arg: null,
+ styleWithCSS: 'vertical-align'},
+ 'underline' : {
+ opt_arg: null,
+ styleWithCSS: 'text-decoration'}};
+
+ var UNAPPLY_TESTS = {
+ 'bold' : {
+ tags: [
+ ['<b>', '</b>'],
+ ['<STRONG>', '</STRONG>'],
+ ['<span style="font-weight: bold;">', '</span>']]},
+ 'italic' : {
+ tags: [
+ ['<i>', '</i>'],
+ ['<EM>', '</EM>'],
+ ['<span style="font-style: italic;">', '</span>']]},
+ 'outdent' : {
+ unapply: 'indent',
+ block: true,
+ tags: [
+ ['<blockquote>', '</blockquote>'],
+ ['<blockquote class="webkit-indent-blockquote" style="margin: 0 0 0 40px; border: none; padding: 0px;">', '</blockquote>'],
+ ['<ul><li>', '</li></ul>'],
+ ['<ol><li>', '</li></ol>'],
+ ['<div style="margin-left: 40px;">', '</div>']]},
+ 'removeformat' : {
+ unapply: '*',
+ block: true,
+ tags: [
+ ['<b>', '</b>'],
+ ['<a href="http://www.foo.com">', '</a>'],
+ ['<table><tr><td>', '</td></tr></table>']]},
+ 'strikethrough' : {
+ tags: [
+ ['<strike>', '</strike>'],
+ ['<s>', '</s>'],
+ ['<del>', '</del>'],
+ ['<span style="text-decoration: line-through;">', '</span>']]},
+ 'subscript' : {
+ tags: [
+ ['<sub>', '</sub>'],
+ ['<span style="vertical-align: sub;">', '</span>']]},
+ 'superscript' : {
+ tags: [
+ ['<sup>', '</sup>'],
+ ['<span style="vertical-align: super;">', '</span>']]},
+ 'unbookmark' : {
+ unapply: 'createbookmark',
+ tags: [
+ ['<a name="bookmark">', '</a>']]},
+ 'underline' : {
+ tags: [
+ ['<u>', '</u>'],
+ ['<span style="text-decoration: underline;">', '</span>']]},
+ 'unlink' : {
+ unapply: 'createbookmark',
+ tags: [
+ ['<a href="http://www.foo.com">', '</a>']]}};
+
+ var QUERY_TESTS = {
+ 'backcolor' : {
+ type: 'value',
+ tests: [
+ {html: '<FONT style="BACKGROUND-COLOR: #ffccaa">foo bar baz</FONT>', expected: new Color('#ffccaa')},
+ {html: '<span class="Apple-style-span" style="background-color: rgb(255, 0, 0);">foo bar baz</span>', expected: new Color('#ff0000')},
+ {html: '<span style="background-color: #ff0000">foo bar baz</span>', expected: new Color('#ff0000')}
+ ]
+ },
+ 'bold' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<b>foo bar baz</b>', expected: true},
+ {html: '<STRONG>foo bar baz</STRONG>', expected: true},
+ {html: '<span style="font-weight:bold">foo bar baz</span>', expected: true},
+ {html: '<b style="font-weight:normal">foo bar baz</b>', expected: false},
+ {html: '<b><span style="font-weight:normal;">foo bar baz</span>', expected: false}
+ ]
+ },
+ 'fontname' : {
+ type: 'value',
+ tests: [
+ {html: '<font face="Arial">foo bar baz</font>', expected: 'Arial'},
+ {html: '<span style="font-family:Arial">foo bar baz</span>', expected: 'Arial'},
+ {html: '<font face="Arial" style="font-family:Courier">foo bar baz</font>', expected: 'Courier'},
+ {html: '<font face="Courier"><font face="Arial">foo bar baz</font></font>', expected: 'Arial'},
+ {html: '<span style="font-family:Courier"><font face="Arial">foo bar baz</font></span>', expected: 'Arial'}
+ ]
+ },
+ 'fontsize' : {
+ type: 'value',
+ tests: [
+ {html: '<font size=4>foo bar baz</font>', expected: new Size(4)},
+ // IE adds +1 to font size from font-size style attributes.
+ // This is hard to correct for since it does NOT add +1 to size attribute from font tag.
+ {html: '<span class="Apple-style-span" style="font-size: large;">foo bar baz</span>', expected: new Size(4)},
+ {html: '<font size=1 style="font-size:x-large;">foo bar baz</font>', expected: new Size(5)}
+ ]
+ },
+ 'forecolor' : {
+ type: 'value',
+ tests: [
+ {html: '<font color="#ff0000">foo bar baz</font>', expected: new Color('#ff0000')},
+ {html: '<span style="color:#ff0000">foo bar baz</span>', expected: new Color('#ff0000')},
+ {html: '<font color="#0000ff" style="color:#ff0000">foo bar baz</span>', expected: new Color('#ff0000')}
+ ]
+ },
+ 'hilitecolor' : {
+ type: 'value',
+ tests: [
+ {html: '<FONT style="BACKGROUND-COLOR: #ffccaa">foo bar baz</FONT>', expected: new Color('#ffccaa')},
+ {html: '<span class="Apple-style-span" style="background-color: rgb(255, 0, 0);">foo bar baz</span>', expected: new Color('#ff0000')},
+ {html: '<span style="background-color: #ff0000">foo bar baz</span>', expected: new Color('#ff0000')}
+ ]
+ },
+ 'insertorderedlist' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<ol><li>foo bar baz</li></ol>', expected: true},
+ {html: '<ul><li>foo bar baz</li></ul>', expected: false}
+ ]
+ },
+ 'insertunorderedlist' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<ol><li>foo bar baz</li></ol>', expected: false},
+ {html: '<ul><li>foo bar baz</li></ul>', expected: true}
+ ]
+ },
+ 'italic' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<i>foo bar baz</i>', expected: true},
+ {html: '<EM>foo bar baz</EM>', expected: true},
+ {html: '<span style="font-style:italic">foo bar baz</span>', expected: true},
+ {html: '<i><span style="font-style:normal">foo bar baz</span></i>', expected: false}
+ ]
+ },
+ 'justifycenter' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<div align="center">foo bar baz</div>', expected: true},
+ {html: '<p align="center">foo bar baz</p>', expected: true},
+ {html: '<div style="text-align: center;">foo bar baz</div>', expected: true}
+ ]
+ },
+ 'justifyfull' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<div align="justify">foo bar baz</div>', expected: true},
+ {html: '<p align="justify">foo bar baz</p>', expected: true},
+ {html: '<div style="text-align: justify;">foo bar baz</div>', expected: true}
+ ]
+ },
+ 'justifyleft' : {
+ type: 'state',
+ tests: [
+ {html: '<div align="left">foo bar baz</div>', expected: true},
+ {html: '<p align="left">foo bar baz</p>', expected: true},
+ {html: '<div style="text-align: left;">foo bar baz</div>', expected: true}
+ ]
+ },
+ 'justifyright' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<div align="right">foo bar baz</div>', expected: true},
+ {html: '<p align="right">foo bar baz</p>', expected: true},
+ {html: '<div style="text-align: right;">foo bar baz</div>', expected: true}
+ ]
+ },
+ 'strikethrough' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<strike>foo bar baz</strike>', expected: true},
+ {html: '<strike style="text-decoration: none">foo bar baz</strike>', expected: false},
+ {html: '<s>foo bar baz</s>', expected: true},
+ {html: '<del>foo bar baz</del>', expected: true},
+ {html: '<span style="text-decoration:line-through">foo bar baz</span>', expected: true}
+ ]
+ },
+ 'subscript' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<sub>foo bar baz</sub>', expected: true}
+ ]
+ },
+ 'superscript' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<sup>foo bar baz</sup>', expected: true}
+ ]
+ },
+ 'underline' : {
+ type: 'state',
+ tests: [
+ {html: 'foo bar baz', expected: false},
+ {html: '<u>foo bar baz</u>', expected: true},
+ {html: '<a href="http://www.foo.com">foo bar baz</a>', expected: true},
+ {html: '<span style="text-decoration:underline">foo bar baz</span>', expected: true},
+ {html: '<u style="text-decoration:none">foo bar baz</u>', expected: false},
+ {html: '<a style="text-decoration:none" href="http://www.foo.com">foo bar baz</a>', expected: false}
+ ]
+ }
+ };
+
+ var CHANGE_TESTS = {
+ 'backcolor' : {
+ type: 'value',
+ tests: [
+ {html: '<FONT style="BACKGROUND-COLOR: #ffccaa">foo bar baz</FONT>', opt_arg: '#884422'},
+ {html: '<span class="Apple-style-span" style="background-color: rgb(255, 0, 0);">foo bar baz</span>', opt_arg: '#0000ff'},
+ {html: '<span style="background-color: #ff0000">foo bar baz</span>', opt_arg: '#0000ff'}
+ ]
+ },
+ 'fontname' : {
+ type: 'value',
+ tests: [
+ {html: '<font face="Arial">foo bar baz</font>', opt_arg: 'Courier'},
+ {html: '<span style="font-family:Arial">foo bar baz</span>', opt_arg: 'Courier'},
+ {html: '<font face="Arial" style="font-family:Verdana">foo bar baz</font>', opt_arg: 'Courier'},
+ {html: '<font face="Verdana"><font face="Arial">foo bar baz</font></font>', opt_arg: 'Courier'},
+ {html: '<span style="font-family:Verdana"><font face="Arial">foo bar baz</font></span>', opt_arg: 'Courier'}
+ ]
+ },
+ 'fontsize' : {
+ type: 'value',
+ tests: [
+ {html: '<font size=4>foo bar baz</font>', opt_arg: 1},
+ {html: '<span class="Apple-style-span" style="font-size: large;">foo bar baz</span>', opt_arg: 1},
+ {html: '<font size=1 style="font-size:x-small;">foo bar baz</font>', opt_arg: 5}
+ ]
+ },
+ 'forecolor' : {
+ type: 'value',
+ tests: [
+ {html: '<font color="#ff0000">foo bar baz</font>', opt_arg: '#00ff00'},
+ {html: '<span style="color:#ff0000">foo bar baz</span>', opt_arg: '#00ff00'},
+ {html: '<font color="#0000ff" style="color:#ff0000">foo bar baz</span>', opt_arg: '#00ff00'}
+ ]
+ },
+ 'hilitecolor' : {
+ type: 'value',
+ tests: [
+ {html: '<FONT style="BACKGROUND-COLOR: #ffccaa">foo bar baz</FONT>', opt_arg: '#884422'},
+ {html: '<span class="Apple-style-span" style="background-color: rgb(255, 0, 0);">foo bar baz</span>', opt_arg: '#00ff00'},
+ {html: '<span style="background-color: #ff0000">foo bar baz</span>', opt_arg: '#00ff00'}
+ ]
+ }
+ };
+
+ /** The document of the editable iframe */
+ var editorDoc = null;
+ /** Dummy text to apply and unapply formatting to */
+ var TEST_CONTENT = 'foo bar baz';
+ /**
+ * Word in dummy text that should change. Formatting is sometimes applied
+ * to a single word instead of the entire text node because sometimes a
+ * style might get applied to the body node instead of wrapped around
+ * the text, and that's not what's being tested.
+ */
+ var TEST_WORD = 'bar';
+ /** Constant for indicating an action is unsupported (threw exception) */
+ var UNSUPPORTED = 'UNSUPPORTED';
+ /** <br> and <p> are acceptable HTML to be left over from block elements */
+ var BLOCK_REMOVE_TAGS = [/\s*<br>\s*/i, /\s*<p>\s*/i];
+ /** Array used to accumulate test results */
+ // Tack on the actual display tests with bogus data
+ // otherwise the beacon will fail.
+ var results = ['apply=0', 'unapply=0', 'change=0', 'query=0'];
+
+ /**
+ *
+ */
+ function resetIframe(newHtml) {
+ // These attributes can get set on the iframe by some errant execCommands
+ editorDoc.body.setAttribute('style', '');
+ editorDoc.body.setAttribute('bgcolor', '');
+ editorDoc.body.innerHTML = newHtml;
+ }
+
+ /**
+ * Finds the text node in the given node containing the given word.
+ * Returns null if not found.
+ */
+ function findTextNode(word, node) {
+ if (node.nodeType == 3) {
+ // Text node, check value.
+ if (node.data.includes(word)) {
+ return node;
+ }
+ } else if (node.nodeType == 1) {
+ // Element node, check children.
+ for (var i = 0; i < node.childNodes.length; i++) {
+ var result = findTextNode(word, node.childNodes[i]);
+ if (result) {
+ return result;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Sets the selection to be collapsed at the start of the word,
+ * or the start of the editor if no word is passed in.
+ */
+ function selectStart(word) {
+ var textNode = findTextNode(word || '', editorDoc.body);
+ var startOffset = 0;
+ if (word) {
+ startOffset = textNode.data.indexOf(word);
+ }
+ var range = createCaret(textNode, startOffset);
+ range.select();
+ }
+
+ /**
+ * Selects the given word in the editor iframe.
+ */
+ function selectWord(word) {
+ var textNode = findTextNode(word, editorDoc.body);
+ if (!textNode) {
+ return;
+ }
+ var start = textNode.data.indexOf(word);
+ var range = createFromNodes(textNode, start, textNode, start + word.length);
+ range.select();
+ }
+
+ /**
+ * Gets the HTML before the text, so that we know how the browser
+ * applied a style
+ */
+ function getSurroundingTags(text) {
+ var html = editorDoc.body.innerHTML;
+ var tagStart = html.indexOf('<');
+ var index = editorDoc.body.innerHTML.indexOf(text);
+ if (tagStart == -1 || index == -1) {
+ return '';
+ }
+ return editorDoc.body.innerHTML.substring(tagStart, index);
+ }
+
+ /**
+ * Does the test for an apply execCommand.
+ */
+ function doApplyTest(command, styleWithCSS) {
+ try {
+ // Set styleWithCSS
+ try {
+ editorDoc.execCommand('styleWithCSS', false, styleWithCSS);
+ } catch (ex) {
+ // Ignore errors
+ }
+ resetIframe(TEST_CONTENT);
+ if (APPLY_TESTS[command].collapse) {
+ selectStart(TEST_WORD);
+ } else {
+ selectWord(TEST_WORD);
+ }
+ try {
+ editorDoc.execCommand(command, false, APPLY_TESTS[command].opt_arg);
+ } catch (ex) {
+ return UNSUPPORTED;
+ }
+ return getSurroundingTags(APPLY_TESTS[command].wholeline? TEST_CONTENT : TEST_WORD);
+ } catch (ex) {
+ return UNSUPPORTED;
+ }
+ }
+
+ /**
+ * Outputs the result of the apply command to a table.
+ * @return {boolean} success
+ */
+ function outputApplyResult(command, result, styleWithCSS) {
+ // The apply command "succeeded" if HTML was generated.
+ var success = (result != UNSUPPORTED) && result;
+ // Except for styleWithCSS commands, which only succeed if the
+ // expected style was applied.
+ if (styleWithCSS) {
+ success = result && result.toLowerCase().includes(APPLY_TESTS[command].styleWithCSS);
+ }
+ results.push('a-' + command + '-' + (styleWithCSS ? 1 : 0) + '=' + (success ? '1' : '0'));
+
+ // Each command is displayed as a table row with 3 columns
+ var tr = document.createElement('TR');
+ tr.className = success ? 'success' : 'fail';
+
+ // Column 1: command name
+ var td = document.createElement('TD');
+ td.innerHTML = command;
+ tr.appendChild(td);
+
+ // Column 2: styleWithCSS
+ var td = document.createElement('TD');
+ td.innerHTML = styleWithCSS ? 'true' : 'false';
+ tr.appendChild(td);
+
+ // Column 3: pass/fail
+ td = document.createElement('TD');
+ td.innerHTML = success ? 'PASS' : 'FAIL';
+ tr.appendChild(td);
+
+ // Column 4: generated HTML (for passing commands)
+ td = document.createElement('TD');
+ // Escape the HTML in the result for printing.
+ result = result.replace(/\</g, '<').replace(/\>/g, '>');
+ td.innerHTML = result;
+ tr.appendChild(td);
+ var table = document.getElementById('apply_output');
+ table.appendChild(tr);
+ return success;
+ }
+
+ /**
+ * Does the test for an unapply execCommand.
+ */
+ function doUnapplyTest(command, index) {
+ try {
+ var wordStart = TEST_CONTENT.indexOf(TEST_WORD);
+ resetIframe(
+ TEST_CONTENT.substring(0, wordStart) +
+ UNAPPLY_TESTS[command].tags[index][0] +
+ TEST_WORD +
+ UNAPPLY_TESTS[command].tags[index][1] +
+ TEST_CONTENT.substring(wordStart + TEST_WORD.length));
+ selectWord(TEST_WORD);
+ try {
+ editorDoc.execCommand(command, false, UNAPPLY_TESTS[command].opt_arg || null);
+ } catch (ex) {
+ return UNSUPPORTED;
+ }
+ return getSurroundingTags(TEST_WORD);
+ } catch (ex) {
+ return UNSUPPORTED;
+ }
+ }
+
+ /**
+ * Check if the given unapply execCommand succeeded. It succeeded if
+ * the following conditions are true:
+ * - The execCommand did not throw an exception
+ * - One of the following:
+ * - The html was removed after the execCommand
+ * - The html was block and the html was replaced with <p> or <br>
+ */
+ function unapplyCommandSucceeded(command, result) {
+ if (result != UNSUPPORTED) {
+ if (!result) {
+ return true;
+ } else if (UNAPPLY_TESTS[command].block) {
+ for (var i = 0; i < BLOCK_REMOVE_TAGS.length; i++) {
+ if (result.match(BLOCK_REMOVE_TAGS[i])) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Outputs the result of the unapply command to a table.
+ * @return {boolean} success
+ */
+ function outputUnapplyResult(command, result, index) {
+ // The apply command "succeeded" if HTML was removed.
+ var success = unapplyCommandSucceeded(command, result);
+ results.push('u-' + command + '-' + index + '=' + (success ? '1' : '0'));
+
+ // Each command is displayed as a table row with 5 columns
+ var tr = document.createElement('TR');
+ tr.className = success ? 'success' : 'fail';
+
+ // Column 1: command name
+ var td = document.createElement('TD');
+ td.innerHTML = command;
+ tr.appendChild(td);
+
+ // Column 2: command name being unapplied
+ var td = document.createElement('TD');
+ td.innerHTML = UNAPPLY_TESTS[command].unapply || command;
+ tr.appendChild(td);
+
+ // Column 3: pass/fail
+ td = document.createElement('TD');
+ td.innerHTML = success ? 'PASS' : 'FAIL';
+ tr.appendChild(td);
+
+ // Column 4: html being removed
+ td = document.createElement('TD');
+ // Escape the html for printing.
+ var htmlToRemove = UNAPPLY_TESTS[command].tags[index][0].replace(/\</g, '<').replace(/\>/g, '>');
+ td.innerHTML = htmlToRemove;
+ tr.appendChild(td);
+
+ // Column 5: resulting html
+ td = document.createElement('TD');
+ // Escape the HTML in the result for printing.
+ result = result.replace(/\</g, '<').replace(/\>/g, '>');
+ td.innerHTML = success ? ' ' : result;
+ tr.appendChild(td);
+ var table = document.getElementById('unapply_output');
+ table.appendChild(tr);
+ return success;
+ }
+
+ /**
+ * Does a queryCommandState or queryCommandValue test for an execCommand.
+ */
+ function doQueryTest(command, index) {
+ try {
+ resetIframe(QUERY_TESTS[command].tests[index].html);
+ selectWord(TEST_WORD);
+ // Dummy val that won't match any expected vals, including false.
+ var result = UNSUPPORTED;
+ if (QUERY_TESTS[command].type == 'state') {
+ try {
+ result = editorDoc.queryCommandState(command);
+ } catch (ex) {
+ result = UNSUPPORTED;
+ }
+ } else {
+ try {
+ // A return value of false indicates the command is not supported.
+ result = editorDoc.queryCommandValue(command) || UNSUPPORTED;
+ } catch (ex) {
+ result = UNSUPPORTED;
+ }
+ }
+ return result;
+ } catch (ex) {
+ return UNSUPPORTED;
+ }
+ }
+
+ /**
+ * Check if the given queryCommandState or queryCommandValue succeeded.
+ */
+ function queryCommandSucceeded(command, index, result) {
+ var expected = QUERY_TESTS[command].tests[index].expected;
+ if (expected instanceof Color) {
+ return expected.compare(new Color(result));
+ } else if (expected instanceof Size) {
+ return expected.compare(new Size(result));
+ } else {
+ return (result == expected);
+ }
+ }
+
+ /**
+ * @return {boolean} success
+ */
+ function outputQueryResult(command, index, result) {
+ // Create table row for results.
+ var tr = document.createElement('TR');
+ var success = queryCommandSucceeded(command, index, result);
+ tr.className = success ? 'success' : 'fail';
+ results.push('q-' + command + '-' + index + '=' + (success ? '1' : '0'));
+
+ // Column 1: command name
+ var td = document.createElement('TD');
+ td.innerHTML = command;
+ tr.appendChild(td);
+
+ // Column 2: pass/fail
+ td = document.createElement('TD');
+ td.innerHTML = success ? 'PASS' : 'FAIL';
+ tr.appendChild(td);
+
+ // Column 3: test HTML
+ td = document.createElement('TD');
+ var testHtml = QUERY_TESTS[command].tests[index].html.replace(/</g, '<').replace(/>/g, '>');
+ td.innerHTML = testHtml.substring(0, testHtml.indexOf(TEST_CONTENT));
+ tr.appendChild(td);
+
+ // Column 4: Expected result
+ td = document.createElement('TD');
+ td.innerHTML = QUERY_TESTS[command].tests[index].expected;
+ tr.appendChild(td);
+
+ // Column 5: Actual result
+ td = document.createElement('TD');
+ td.innerHTML = result;
+ tr.appendChild(td);
+
+ // Append result to the state or value table, depending on what
+ // type of command this is.
+ var table = document.getElementById(
+ QUERY_TESTS[command].type == 'state' ? 'querystate_output' : 'queryvalue_output');
+ table.appendChild(tr);
+ return success;
+ }
+
+ function doChangeTest(command, index) {
+ try {
+ resetIframe(CHANGE_TESTS[command].tests[index].html);
+ selectWord(TEST_CONTENT);
+ try {
+ editorDoc.execCommand(command, false, CHANGE_TESTS[command].tests[index].opt_arg);
+ } catch (ex) {
+ return UNSUPPORTED;
+ }
+ } catch (ex) {
+ return UNSUPPORTED;
+ }
+ }
+
+ function checkChangeSuccess(command, index) {
+ var textNode = findTextNode(TEST_CONTENT, editorDoc.body);
+ if (!textNode) {
+ // The text has been removed from the document, or split up for no reason.
+ return false;
+ }
+ var expected = null, attributeName = null, styleName = null;
+ switch (command) {
+ case 'backcolor':
+ case 'hilitecolor':
+ expected = new Color(CHANGE_TESTS[command].tests[index].opt_arg);
+ styleName = 'backgroundColor';
+ break;
+ case 'fontname':
+ expected = CHANGE_TESTS[command].tests[index].opt_arg;
+ attributeName = 'face';
+ styleName = 'fontFamily';
+ break;
+ case 'fontsize':
+ expected = new Size(CHANGE_TESTS[command].tests[index].opt_arg);
+ attributeName = 'size';
+ styleName = 'fontSize';
+ break;
+ case 'forecolor':
+ expected = new Color(CHANGE_TESTS[command].tests[index].opt_arg);
+ attributeName = 'color';
+ styleName = 'color';
+ }
+ var foundExpected = false;
+
+ // Loop through all the parent nodes that format the text node,
+ // checking that there is exactly one font attribute or
+ // style, and that it's set correctly.
+ var currentNode = textNode.parentNode;
+ while(currentNode && currentNode.nodeName != 'BODY') {
+ // Check font attribute.
+ if (attributeName && currentNode.nodeName == 'FONT' && currentNode.getAttribute(attributeName)) {
+ var foundAttribute = false;
+ switch(command) {
+ case 'backcolor':
+ case 'forecolor':
+ case 'hilitecolor':
+ foundAttribute = new Color(currentNode.getAttribute(attributeName)).compare(expected);
+ break;
+ case 'fontsize':
+ foundAttribute = new Size(currentNode.getAttribute(attributeName)).compare(expected);
+ break;
+ case 'fontname':
+ foundAttribute = (currentNode.getAttribute(attributeName).toLowerCase() == expected.toLowerCase());
+ }
+ if (foundAttribute && foundExpected) {
+ // This is the correct attribute, but the style has been applied
+ // twice. This makes it hard for other browsers to remove the
+ // style.
+ return false;
+ } else if (!foundAttribute) {
+ // This node has an incorrect font attribute.
+ return false;
+ }
+ // The expected font attribute was found.
+ foundExpected = true;
+ }
+ // Check node style.
+ if (currentNode.style[styleName]) {
+ var foundStyle = false;
+ switch(command) {
+ case 'backcolor':
+ case 'forecolor':
+ case 'hilitecolor':
+ foundStyle = new Color(currentNode.style[styleName]).compare(expected);
+ break;
+ case 'fontsize':
+ foundStyle = new Size(currentNode.style[styleName]).compare(expected);
+ break;
+ case 'fontname':
+ foundStyle = (currentNode.style[styleName].toLowerCase() == expected.toLowerCase());
+ }
+ if (foundStyle && foundExpected) {
+ // This is the correct style, but the style has been
+ // applied twice. This makes it hard for other browsers to
+ // remove the style.
+ return false;
+ } else if (!foundStyle) {
+ // This node has an incorrect font style.
+ return false;
+ }
+ foundExpected = true;
+ }
+ currentNode = currentNode.parentNode;
+ }
+ return foundExpected;
+ }
+
+ /**
+ * @return {boolean} success
+ */
+ function outputChangeResult(command, index) {
+ // Each command is displayed as a table row with 4 columns
+ var tr = document.createElement('TR');
+ var success = checkChangeSuccess(command, index);
+ tr.className = success ? 'success' : 'fail';
+ results.push('c-' + command + '-' + index + '=' + (success ? '1' : '0'));
+
+ // Column 1: command name
+ var td = document.createElement('TD');
+ td.innerHTML = command;
+ tr.appendChild(td);
+
+ // Column 2: status
+ td = document.createElement('TD');
+ td.innerHTML = (success == null) ? '?' : (success == true ? 'PASS' : 'FAIL');
+ tr.appendChild(td);
+
+ // Column 3: opt_arg
+ td = document.createElement('TD');
+ td.innerHTML = CHANGE_TESTS[command].tests[index].opt_arg;
+ tr.appendChild(td);
+
+ // Column 4: original html
+ td = document.createElement('TD');
+ td.innerHTML = CHANGE_TESTS[command].tests[index].html.replace(/\</g, '<').replace(/\>/g, '>');;
+ tr.appendChild(td);
+
+ // Column 5: resulting html
+ td = document.createElement('TD');
+ td.innerHTML = editorDoc.body.innerHTML.replace(/\</g, '<').replace(/\>/g, '>');;
+ tr.appendChild(td);
+
+ var table = document.getElementById('change_output');
+ table.appendChild(tr);
+ return success;
+ }
+
+ function runTests() {
+ // Wrap initialization code in a try/catch so we can fail gracefully
+ // on older browsers.
+ try {
+ editorDoc = document.getElementById('editor').contentWindow.document;
+ // Default styleWithCSS to false, since it's not supported by IE.
+ try {
+ editorDoc.execCommand('styleWithCSS', false, false);
+ } catch (ex) {
+ // Not supported by IE.
+ }
+ } catch (ex) {}
+
+ // Apply tests
+ var apply_score = 0;
+ var apply_count = 0;
+ var unapply_score= 0;
+ var unapply_count = 0;
+ var change_score = 0;
+ var change_count = 0;
+ var query_score = 0;
+ var query_count = 0;
+ for (var command in APPLY_TESTS) {
+ try {
+ var result = doApplyTest(command, false);
+ var success = outputApplyResult(command, result, false);
+ apply_score += success ? 1 : 0;
+ } catch (ex) {
+ // An exception is counted as a failed test, don't increment success.
+ }
+ apply_count++;
+ if (APPLY_TESTS[command].styleWithCSS) {
+ try {
+ var result = doApplyTest(command, true);
+ var success = outputApplyResult(command, result, true);
+ apply_score += success ? 1 : 0;
+ } catch (ex) {
+ // An exception is counted as a failed test, don't increment success.
+ }
+ apply_count++;
+ }
+ }
+
+ // Unapply tests
+ for (var command in UNAPPLY_TESTS) {
+ for (var i = 0; i < UNAPPLY_TESTS[command].tags.length; i++) {
+ try {
+ var result = doUnapplyTest(command, i);
+ var success = outputUnapplyResult(command, result, i);
+ unapply_score += success ? 1 : 0;
+ } catch (ex) {
+ // An exception is counted as a failed test, don't increment success.
+ }
+ unapply_count++;
+ }
+ }
+
+ // Query tests
+ for (var command in QUERY_TESTS) {
+ for (var i = 0; i < QUERY_TESTS[command].tests.length; i++) {
+ try {
+ var result = doQueryTest(command, i);
+ var success = outputQueryResult(command, i, result);
+ query_score += success ? 1 : 0;
+ } catch (ex) {
+ // An exception is counted as a failed test, don't increment success.
+ }
+ query_count++;
+ }
+ }
+
+ // Change tests
+ for (var command in CHANGE_TESTS) {
+ for (var i = 0; i < CHANGE_TESTS[command].tests.length; i++) {
+ try {
+ doChangeTest(command, i);
+ var success = outputChangeResult(command, i);
+ change_score += success ? 1 : 0;
+ } catch (ex) {
+ // An exception is counted as a failed test, don't increment success.
+ }
+ change_count++;
+ }
+ }
+
+ // Beacon all test results.
+ // and construct a shorter version for the results page.
+ try {
+ document.getElementById('apply-score').innerHTML =
+ apply_score + '/' + apply_count;
+ document.getElementById('unapply-score').innerHTML =
+ unapply_score + '/' + unapply_count;
+ document.getElementById('query-score').innerHTML =
+ query_score + '/' + query_count;
+ document.getElementById('change-score').innerHTML =
+ change_score + '/' + change_count;
+ } catch (ex) {}
+ var continueParams = [
+ 'apply=' + apply_score,
+ 'unapply=' + unapply_score,
+ 'query=' + query_score,
+ 'change=' + change_score
+ ];
+ parent.sendScore(results, continueParams);
+ }
+ </script>
+ <style>
+ .success {
+ background-color: #93c47d;
+ }
+ .fail {
+ background-color: #ea9999;
+ }
+ .score {
+ color: #666;
+ }
+ </style>
+</head>
+<body onload="runTests()">
+ <h1>Apply Formatting <span id="apply-score" class="score"></span></h1>
+ <table id="apply"><tbody id="apply_output"><tr><th>Command</th><th>styleWithCSS</th><th>Status</th><th>Output</th></tr></tbody></table>
+ <h1>Unapply Formatting <span id="unapply-score" class="score"></span></h1>
+ <table id="unapply">
+ <thead><tr><th>Command</th><th>Command unapplied</th><th>Status</th><th>HTML Attempted to Unapply</th><th>Resulting HTML</th></tr></thead>
+ <tbody id="unapply_output"></tbody></table>
+ <h1>Query Formatting State <span id="query-score" class="score"></span></h1>
+ <table id="querystate">
+ <thead><tr><th>Command</th><th>Status</th><th>HTML</th><th>Expected</th><th>Actual</th></tr></thead>
+ <tbody id="querystate_output"></tbody></table>
+ <h1>Query Formatting Value </h1>
+ <table id="queryvalue">
+ <thead><tr><th>Command</th><th>Status</th><th>HTML</th><th>Expected</th><th>Actual</th></tr></thead>
+ <tbody id="queryvalue_output"></tbody></table>
+ <h1>Change Formatting <span id="change-score" class="score"></span></h1>
+ <table id="change">
+ <thead><tr><th>Command</th><th>Status</th><th>Argument</th><th>Original HTML</th><th>Resulting HTML</th></tr></thead>
+ <tbody id="change_output"></tbody></table>
+ <iframe name="editor" id="editor" src="editable.html"></iframe>
+</body>
+</html>
diff --git a/editor/libeditor/tests/browserscope/lib/richtext/update_from_upstream b/editor/libeditor/tests/browserscope/lib/richtext/update_from_upstream new file mode 100644 index 0000000000..2071454a85 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext/update_from_upstream @@ -0,0 +1,16 @@ +#!/bin/sh + +set -x + +if test -d richtext; then + rm -drf richtext; +fi + +svn checkout http://browserscope.googlecode.com/svn/trunk/categories/richtext/static richtext | tail -1 | sed 's/[^0-9]//g' > current_revision + +find richtext -type d -name .svn -exec rm -drf \{\} \; 2> /dev/null + +hg add current_revision richtext + +hg stat . + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/LICENSE b/editor/libeditor/tests/browserscope/lib/richtext2/LICENSE new file mode 100644 index 0000000000..57bc88a15a --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/README b/editor/libeditor/tests/browserscope/lib/richtext2/README new file mode 100644 index 0000000000..a3bc3110f4 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/README @@ -0,0 +1,58 @@ +README FOR BROWSERSCOPE +----------------------- + +Hey there - thanks for downloading the code. This file has instructions +for getting setup so that you can run the codebase locally. + +This project is built on Google App Engine using the +Django web application framework and written in Python. + +To get started, you'll need to first download the App Engine SDK at: +http://code.google.com/appengine/downloads.html + +For local development, just startup the server: +./pathto/google_appengine/dev_appserver.py --port=8080 browserscope + +You should then be able to access the local application at: +http://localhost:8080/ + +Note: the first time you hit the homepage it may take a little +while - that's because it's trying to read out median times for all +of the tests from a nonexistent datastore and write to memcache. +Just be a lil patient. + +You can run the unit tests at: + http://localhost:8080/test + + +CONTRIBUTING +------------------ + +Most likely you are interested in adding new tests or creating +a new test category. If you are interested in adding tests to an existing +"category" you may want to get in touch with the maintainer for that +branch of the tree. We are really looking forward to receiving your +code in patch format. Currently the category maintainers are: +Network: Steve Souders <souders@gmail.com> +Reflow: Lindsey Simon <elsigh@gmail.com> +Security: Adam Barth <adam@adambarth.com> and Collin Jackson <collin@collinjackson.com> + + +To create a completely new test category: + * Copy one of the existing directories in categories/ + * Edit your test_set.py, handlers.py + * Add your files in templates/ and static/ + * Update urls.py and settings.CATEGORIES + * Follow the examples of other tests re: + * beaconing using/testdriver_base + * your GetScoreAndDisplayValue method + * your GetRowScoreAndDisplayValue method + +References: + * App Engine Docs - http://code.google.com/appengine/docs/python/overview.html + * App Engine Group - http://groups.google.com/group/google-appengine + * Python Docs - http://www.python.org/doc/ + * Django - http://www.djangoproject.com/ + + + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/README.Mozilla b/editor/libeditor/tests/browserscope/lib/richtext2/README.Mozilla new file mode 100644 index 0000000000..7d730874c6 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/README.Mozilla @@ -0,0 +1,27 @@ +The BrowserScope project provides a set of cross-browser HTML editor tests, +which we import in our test suite in order to run them as part of our +continuous integration system. + +We pull tests occasionally from their Subversion repository using the pull +script which can be found in this directory. We also record the revision ID +which we've used in the current_revision file inside this directory. + +Using the pull script is quite easy, just switch to this directory, and say: + +sh update_from_upstream + +There are tests which we're currently failing on, and there will probably be +more of those in the future. We should maintain a list of the failing tests +manually in currentStatus.js (which can also be found in this directory), to +make sure that the suite passes entirely, with failing tests marked as todo +items. + +The current status of the test suite needs to be updated whenever an editor +bug gets fixed, which makes us pass one of the tests. When that happens, +you should set the UPDATE_TEST_RESULTS constant to true in test_richtext2.html, +run the test suite, paste the result JSON string in a JSON beautifier (such +as http://jsbeautifier.org/), and use the result to update currentStatus.js. + +As a special case, if there are platform-specific failures, these are instead +recorded manually in platformFailures.js. (Currently, this applies only to +tests that are dependent on underlying platform support for the Thai script.) diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/currentStatus.js b/editor/libeditor/tests/browserscope/lib/richtext2/currentStatus.js new file mode 100644 index 0000000000..e0a4cc0fb0 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/currentStatus.js @@ -0,0 +1,1768 @@ +/** + * The current status of the test suite. + * + * See README.Mozilla for details on how to generate this. + */ +const knownFailures = { + value: { + "A-Proposed-CB:name_TEXT-1_SI-dM": true, + "A-Proposed-CB:name_TEXT-1_SI-body": true, + "A-Proposed-CB:name_TEXT-1_SI-div": true, + "A-Proposed-DECFS:2_TEXT-1_SI-dM": true, + "A-Proposed-DECFS:2_TEXT-1_SI-body": true, + "A-Proposed-DECFS:2_TEXT-1_SI-div": true, + "A-Proposed-FS:18px_TEXT-1_SI-dM": true, + "A-Proposed-FS:18px_TEXT-1_SI-body": true, + "A-Proposed-FS:18px_TEXT-1_SI-div": true, + "A-Proposed-FS:large_TEXT-1_SI-dM": true, + "A-Proposed-FS:large_TEXT-1_SI-body": true, + "A-Proposed-FS:large_TEXT-1_SI-div": true, + "A-Proposed-H:H1_TEXT-1_SC-dM": true, + "A-Proposed-H:H1_TEXT-1_SC-body": true, + "A-Proposed-H:H1_TEXT-1_SC-div": true, + "A-Proposed-INCFS:2_TEXT-1_SI-dM": true, + "A-Proposed-INCFS:2_TEXT-1_SI-body": true, + "A-Proposed-INCFS:2_TEXT-1_SI-div": true, + "AC-Proposed-SUB_TEXT-1_SI-dM": true, + "AC-Proposed-SUB_TEXT-1_SI-body": true, + "AC-Proposed-SUB_TEXT-1_SI-div": true, + "AC-Proposed-SUP_TEXT-1_SI-dM": true, + "AC-Proposed-SUP_TEXT-1_SI-body": true, + "AC-Proposed-SUP_TEXT-1_SI-div": true, + "AC-Proposed-FS:2_TEXT-1_SI-dM": true, + "AC-Proposed-FS:2_TEXT-1_SI-body": true, + "AC-Proposed-FS:2_TEXT-1_SI-div": true, + "AC-Proposed-FS:18px_TEXT-1_SI-dM": true, + "AC-Proposed-FS:18px_TEXT-1_SI-body": true, + "AC-Proposed-FS:18px_TEXT-1_SI-div": true, + "AC-Proposed-FS:large_TEXT-1_SI-dM": true, + "AC-Proposed-FS:large_TEXT-1_SI-body": true, + "AC-Proposed-FS:large_TEXT-1_SI-div": true, + + // Those tests expect that <font> elements can be nested, but they don't + // match with the other browsers' behavior. + "C-Proposed-FC:g_FONTc:b.sz:6-1_SI-dM": true, + "C-Proposed-FC:g_FONTc:b.sz:6-1_SI-body": true, + "C-Proposed-FC:g_FONTc:b.sz:6-1_SI-div": true, + + "C-Proposed-FS:1_SPAN.ass.s:fs:large-1_SW-dM": true, + "C-Proposed-FS:1_SPAN.ass.s:fs:large-1_SW-body": true, + "C-Proposed-FS:1_SPAN.ass.s:fs:large-1_SW-div": true, + + // Those tests expect that <font> elements can be nested, but they don't + // match with the other browsers' behavior. + "C-Proposed-FS:2_FONTc:b.sz:6-1_SI-dM": true, + "C-Proposed-FS:2_FONTc:b.sz:6-1_SI-body": true, + "C-Proposed-FS:2_FONTc:b.sz:6-1_SI-div": true, + + "C-Proposed-FS:larger_FONTsz:4-dM": true, + "C-Proposed-FS:larger_FONTsz:4-body": true, + "C-Proposed-FS:larger_FONTsz:4-div": true, + "C-Proposed-FS:smaller_FONTsz:4-dM": true, + "C-Proposed-FS:smaller_FONTsz:4-body": true, + "C-Proposed-FS:smaller_FONTsz:4-div": true, + "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SO-dM": true, + "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SO-body": true, + "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SO-div": true, + "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SW-dM": true, + "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SW-body": true, + "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SW-div": true, + "C-Proposed-FB:h1_ADDRESS-FONT.ass.sz:4-1_SW-dM": true, + "C-Proposed-FB:h1_ADDRESS-FONT.ass.sz:4-1_SW-body": true, + "C-Proposed-FB:h1_ADDRESS-FONT.ass.sz:4-1_SW-div": true, + "CC-Proposed-I_B-1_SW-dM": true, + "CC-Proposed-I_B-1_SW-body": true, + "CC-Proposed-I_B-1_SW-div": true, + "CC-Proposed-BC:gray_SPANs:bc:b-1_SI-dM": true, + "CC-Proposed-BC:gray_SPANs:bc:b-1_SI-body": true, + "CC-Proposed-BC:gray_SPANs:bc:b-1_SI-div": true, + "CC-Proposed-BC:gray_P-SPANs:bc:b-3_SL-dM": true, + "CC-Proposed-BC:gray_P-SPANs:bc:b-3_SL-body": true, + "CC-Proposed-BC:gray_P-SPANs:bc:b-3_SL-div": true, + "CC-Proposed-BC:gray_SPANs:bc:b-2_SL-dM": true, + "CC-Proposed-BC:gray_SPANs:bc:b-2_SL-body": true, + "CC-Proposed-BC:gray_SPANs:bc:b-2_SL-div": true, + "CC-Proposed-BC:gray_SPANs:bc:b-2_SR-dM": true, + "CC-Proposed-BC:gray_SPANs:bc:b-2_SR-body": true, + "CC-Proposed-BC:gray_SPANs:bc:b-2_SR-div": true, + "CC-Proposed-FS:1_SPANs:fs:l-1_SW-dM": true, + "CC-Proposed-FS:1_SPANs:fs:l-1_SW-body": true, + "CC-Proposed-FS:1_SPANs:fs:l-1_SW-div": true, + "CC-Proposed-FS:18px_SPANs:fs:l-1_SW-dM": true, + "CC-Proposed-FS:18px_SPANs:fs:l-1_SW-body": true, + "CC-Proposed-FS:18px_SPANs:fs:l-1_SW-div": true, + "CC-Proposed-FS:4_SPANs:fs:l-1_SW-dM": true, + "CC-Proposed-FS:4_SPANs:fs:l-1_SW-body": true, + "CC-Proposed-FS:4_SPANs:fs:l-1_SW-div": true, + "CC-Proposed-FS:4_SPANs:fs:18px-1_SW-dM": true, + "CC-Proposed-FS:4_SPANs:fs:18px-1_SW-body": true, + "CC-Proposed-FS:4_SPANs:fs:18px-1_SW-div": true, + "CC-Proposed-FS:larger_SPANs:fs:l-1_SI-dM": true, + "CC-Proposed-FS:larger_SPANs:fs:l-1_SI-body": true, + "CC-Proposed-FS:larger_SPANs:fs:l-1_SI-div": true, + "CC-Proposed-FS:smaller_SPANs:fs:l-1_SI-dM": true, + "CC-Proposed-FS:smaller_SPANs:fs:l-1_SI-body": true, + "CC-Proposed-FS:smaller_SPANs:fs:l-1_SI-div": true, + "U-RFC-UNLINK_A-1_SO-dM": true, + "U-RFC-UNLINK_A-1_SO-body": true, + "U-RFC-UNLINK_A-1_SO-div": true, + "U-RFC-UNLINK_A-1_SW-dM": true, + "U-RFC-UNLINK_A-1_SW-body": true, + "U-RFC-UNLINK_A-1_SW-div": true, + "U-RFC-UNLINK_A-2_SO-dM": true, + "U-RFC-UNLINK_A-2_SO-body": true, + "U-RFC-UNLINK_A-2_SO-div": true, + "U-RFC-UNLINK_A2-1_SO-dM": true, + "U-RFC-UNLINK_A2-1_SO-body": true, + "U-RFC-UNLINK_A2-1_SO-div": true, + "U-Proposed-B_B-P-I..P-1_SO-I-dM": true, + "U-Proposed-B_B-P-I..P-1_SO-I-body": true, + "U-Proposed-B_B-P-I..P-1_SO-I-div": true, + "U-Proposed-B_B-2_SL-dM": true, + "U-Proposed-B_B-2_SL-body": true, + "U-Proposed-B_B-2_SL-div": true, + "U-Proposed-B_B-2_SR-dM": true, + "U-Proposed-B_B-2_SR-body": true, + "U-Proposed-B_B-2_SR-div": true, + "U-Proposed-U_U-S-2_SI-dM": true, + "U-Proposed-U_U-S-2_SI-body": true, + "U-Proposed-U_U-S-2_SI-div": true, + "U-Proposed-S_DEL-1_SW-dM": true, + "U-Proposed-S_DEL-1_SW-body": true, + "U-Proposed-S_DEL-1_SW-div": true, + "U-Proposed-SUB_SPANs:va:sub-1_SW-dM": true, + "U-Proposed-SUB_SPANs:va:sub-1_SW-body": true, + "U-Proposed-SUB_SPANs:va:sub-1_SW-div": true, + "U-Proposed-SUP_SPANs:va:super-1_SW-dM": true, + "U-Proposed-SUP_SPANs:va:super-1_SW-body": true, + "U-Proposed-SUP_SPANs:va:super-1_SW-div": true, + "U-Proposed-UNLINK_A-1_SC-dM": true, + "U-Proposed-UNLINK_A-1_SC-body": true, + "U-Proposed-UNLINK_A-1_SC-div": true, + "U-Proposed-UNLINK_A-1_SI-dM": true, + "U-Proposed-UNLINK_A-1_SI-body": true, + "U-Proposed-UNLINK_A-1_SI-div": true, + "U-Proposed-UNLINK_A-2_SL-dM": true, + "U-Proposed-UNLINK_A-2_SL-body": true, + "U-Proposed-UNLINK_A-2_SL-div": true, + "U-Proposed-UNLINK_A-3_SR-dM": true, + "U-Proposed-UNLINK_A-3_SR-body": true, + "U-Proposed-UNLINK_A-3_SR-div": true, + "U-Proposed-OUTDENT_BQ-1_SW-dM": true, + "U-Proposed-OUTDENT_BQ-1_SW-body": true, + "U-Proposed-OUTDENT_BQ-1_SW-div": true, + "U-Proposed-OUTDENT_BQ.wibq.s:m:00040.b:n.p:0-1_SW-dM": true, + "U-Proposed-OUTDENT_BQ.wibq.s:m:00040.b:n.p:0-1_SW-body": true, + "U-Proposed-OUTDENT_BQ.wibq.s:m:00040.b:n.p:0-1_SW-div": true, + "U-Proposed-OUTDENT_OL-LI-1_SW-dM": true, + "U-Proposed-OUTDENT_OL-LI-1_SW-body": true, + "U-Proposed-OUTDENT_OL-LI-1_SW-div": true, + "U-Proposed-OUTDENT_UL-LI-1_SW-dM": true, + "U-Proposed-OUTDENT_UL-LI-1_SW-body": true, + "U-Proposed-OUTDENT_UL-LI-1_SW-div": true, + "U-Proposed-OUTDENT_DIV-1_SW-dM": true, + "U-Proposed-OUTDENT_DIV-1_SW-body": true, + "U-Proposed-OUTDENT_DIV-1_SW-div": true, + "U-Proposed-REMOVEFORMAT_Ahref:url-1_SW-dM": true, + "U-Proposed-REMOVEFORMAT_Ahref:url-1_SW-body": true, + "U-Proposed-REMOVEFORMAT_Ahref:url-1_SW-div": true, + "U-Proposed-REMOVEFORMAT_TABLE-TBODY-TR-TD-1_SW-dM": true, + "U-Proposed-REMOVEFORMAT_TABLE-TBODY-TR-TD-1_SW-body": true, + "U-Proposed-REMOVEFORMAT_TABLE-TBODY-TR-TD-1_SW-div": true, + "U-Proposed-UNBOOKMARK_An:name-1_SW-dM": true, + "U-Proposed-UNBOOKMARK_An:name-1_SW-body": true, + "U-Proposed-UNBOOKMARK_An:name-1_SW-div": true, + "UC-Proposed-S_SPANc:s-1_SW-dM": true, + "UC-Proposed-S_SPANc:s-1_SW-body": true, + "UC-Proposed-S_SPANc:s-1_SW-div": true, + "UC-Proposed-S_SPANc:s-2_SI-dM": true, + "UC-Proposed-S_SPANc:s-2_SI-body": true, + "UC-Proposed-S_SPANc:s-2_SI-div": true, + "D-Proposed-CHAR-3_SC-dM": true, + "D-Proposed-CHAR-3_SC-body": true, + "D-Proposed-CHAR-3_SC-div": true, + "D-Proposed-CHAR-4_SC-dM": true, + "D-Proposed-CHAR-4_SC-body": true, + "D-Proposed-CHAR-4_SC-div": true, + "D-Proposed-CHAR-5_SC-dM": true, + "D-Proposed-CHAR-5_SC-body": true, + "D-Proposed-CHAR-5_SC-div": true, + "D-Proposed-CHAR-5_SI-1-dM": true, + "D-Proposed-CHAR-5_SI-1-body": true, + "D-Proposed-CHAR-5_SI-1-div": true, + "D-Proposed-CHAR-5_SI-2-dM": true, + "D-Proposed-CHAR-5_SI-2-body": true, + "D-Proposed-CHAR-5_SI-2-div": true, + "D-Proposed-CHAR-5_SR-dM": true, + "D-Proposed-CHAR-5_SR-body": true, + "D-Proposed-CHAR-5_SR-div": true, + "D-Proposed-CHAR-6_SC-dM": true, + "D-Proposed-CHAR-6_SC-body": true, + "D-Proposed-CHAR-6_SC-div": true, + "D-Proposed-CHAR-7_SC-dM": true, + "D-Proposed-CHAR-7_SC-body": true, + "D-Proposed-CHAR-7_SC-div": true, + "D-Proposed-OL-LI-1_SW-dM": true, + "D-Proposed-OL-LI-1_SW-body": true, + "D-Proposed-OL-LI-1_SW-div": true, + "D-Proposed-OL-LI-1_SO-dM": true, + "D-Proposed-OL-LI-1_SO-body": true, + "D-Proposed-OL-LI-1_SO-div": true, + "D-Proposed-TR2rs:2-1_SO1-dM": true, + "D-Proposed-TR2rs:2-1_SO1-body": true, + "D-Proposed-TR2rs:2-1_SO1-div": true, + "D-Proposed-TR2rs:2-1_SO2-dM": true, + "D-Proposed-TR2rs:2-1_SO2-body": true, + "D-Proposed-TR2rs:2-1_SO2-div": true, + "D-Proposed-TR3rs:3-1_SO1-dM": true, + "D-Proposed-TR3rs:3-1_SO1-body": true, + "D-Proposed-TR3rs:3-1_SO1-div": true, + "D-Proposed-TR3rs:3-1_SO2-dM": true, + "D-Proposed-TR3rs:3-1_SO2-body": true, + "D-Proposed-TR3rs:3-1_SO2-div": true, + "D-Proposed-TR3rs:3-1_SO3-dM": true, + "D-Proposed-TR3rs:3-1_SO3-body": true, + "D-Proposed-TR3rs:3-1_SO3-div": true, + "D-Proposed-DIV:ce:false-1_SB-dM": true, + "D-Proposed-DIV:ce:false-1_SL-dM": true, + "D-Proposed-DIV:ce:false-1_SL-body": true, + "D-Proposed-DIV:ce:false-1_SL-div": true, + "D-Proposed-DIV:ce:false-1_SR-dM": true, + "D-Proposed-DIV:ce:false-1_SR-body": true, + "D-Proposed-DIV:ce:false-1_SR-div": true, + "D-Proposed-DIV:ce:false-1_SI-dM": true, + "FD-Proposed-OL-LI-1_SW-dM": true, + "FD-Proposed-OL-LI-1_SW-body": true, + "FD-Proposed-OL-LI-1_SW-div": true, + "FD-Proposed-OL-LI-1_SO-dM": true, + "FD-Proposed-OL-LI-1_SO-body": true, + "FD-Proposed-OL-LI-1_SO-div": true, + "FD-Proposed-TR2rs:2-1_SO1-dM": true, + "FD-Proposed-TR2rs:2-1_SO1-body": true, + "FD-Proposed-TR2rs:2-1_SO1-div": true, + "FD-Proposed-TR2rs:2-1_SO2-dM": true, + "FD-Proposed-TR2rs:2-1_SO2-body": true, + "FD-Proposed-TR2rs:2-1_SO2-div": true, + "FD-Proposed-TR3rs:3-1_SO1-dM": true, + "FD-Proposed-TR3rs:3-1_SO1-body": true, + "FD-Proposed-TR3rs:3-1_SO1-div": true, + "FD-Proposed-TR3rs:3-1_SO2-dM": true, + "FD-Proposed-TR3rs:3-1_SO2-body": true, + "FD-Proposed-TR3rs:3-1_SO2-div": true, + "FD-Proposed-TR3rs:3-1_SO3-dM": true, + "FD-Proposed-TR3rs:3-1_SO3-body": true, + "FD-Proposed-TR3rs:3-1_SO3-div": true, + "FD-Proposed-DIV:ce:false-1_SB-dM": true, + "FD-Proposed-DIV:ce:false-1_SL-dM": true, + "FD-Proposed-DIV:ce:false-1_SL-body": true, + "FD-Proposed-DIV:ce:false-1_SL-div": true, + "FD-Proposed-DIV:ce:false-1_SR-dM": true, + "FD-Proposed-DIV:ce:false-1_SR-body": true, + "FD-Proposed-DIV:ce:false-1_SR-div": true, + "FD-Proposed-DIV:ce:false-1_SI-dM": true, + "I-Proposed-IIMG:._SPAN-IMG-1_SO-dM": true, + "I-Proposed-IIMG:._SPAN-IMG-1_SO-body": true, + "I-Proposed-IIMG:._SPAN-IMG-1_SO-div": true, + "I-Proposed-IIMG:._IMG-1_SO-dM": true, + "I-Proposed-IIMG:._IMG-1_SO-body": true, + "I-Proposed-IIMG:._IMG-1_SO-div": true, + "Q-Proposed-CONTENTREADONLY_TEXT-1-dM": !SpecialPowers.getBoolPref("dom.document.edit_command.contentReadOnly.enabled", false), + "Q-Proposed-CONTENTREADONLY_TEXT-1-body": !SpecialPowers.getBoolPref("dom.document.edit_command.contentReadOnly.enabled", false), + "Q-Proposed-CONTENTREADONLY_TEXT-1-div": !SpecialPowers.getBoolPref("dom.document.edit_command.contentReadOnly.enabled", false), + "Q-Proposed-CREATEBOOKMARK_TEXT-1-dM": true, + "Q-Proposed-CREATEBOOKMARK_TEXT-1-body": true, + "Q-Proposed-CREATEBOOKMARK_TEXT-1-div": true, + "Q-Proposed-DECREASEFONTSIZE_TEXT-1-dM": true, + "Q-Proposed-DECREASEFONTSIZE_TEXT-1-body": true, + "Q-Proposed-DECREASEFONTSIZE_TEXT-1-div": true, + "Q-Proposed-HEADING_TEXT-1-dM": true, + "Q-Proposed-HEADING_TEXT-1-body": true, + "Q-Proposed-HEADING_TEXT-1-div": true, + "Q-Proposed-INCREASEFONTSIZE_TEXT-1-dM": true, + "Q-Proposed-INCREASEFONTSIZE_TEXT-1-body": true, + "Q-Proposed-INCREASEFONTSIZE_TEXT-1-div": true, + "Q-Proposed-PASTE_TEXT-1-dM": true, + "Q-Proposed-PASTE_TEXT-1-body": true, + "Q-Proposed-PASTE_TEXT-1-div": true, + "Q-Proposed-UNBOOKMARK_TEXT-1-dM": true, + "Q-Proposed-UNBOOKMARK_TEXT-1-body": true, + "Q-Proposed-UNBOOKMARK_TEXT-1-div": true, + "Q-Proposed-UNSELECT_TEXT-1-dM": true, + "Q-Proposed-UNSELECT_TEXT-1-body": true, + "Q-Proposed-UNSELECT_TEXT-1-div": true, + "QE-Proposed-CONTENTREADONLY_TEXT-1-dM": !SpecialPowers.getBoolPref("dom.document.edit_command.contentReadOnly.enabled", false), + "QE-Proposed-CONTENTREADONLY_TEXT-1-body": !SpecialPowers.getBoolPref("dom.document.edit_command.contentReadOnly.enabled", false), + "QE-Proposed-CONTENTREADONLY_TEXT-1-div": !SpecialPowers.getBoolPref("dom.document.edit_command.contentReadOnly.enabled", false), + "QE-Proposed-COPY_TEXT-1-dM": true, + "QE-Proposed-COPY_TEXT-1-body": true, + "QE-Proposed-COPY_TEXT-1-div": true, + "QE-Proposed-CREATEBOOKMARK_TEXT-1-dM": true, + "QE-Proposed-CREATEBOOKMARK_TEXT-1-body": true, + "QE-Proposed-CREATEBOOKMARK_TEXT-1-div": true, + "QE-Proposed-CUT_TEXT-1-dM": true, + "QE-Proposed-CUT_TEXT-1-body": true, + "QE-Proposed-CUT_TEXT-1-div": true, + "QE-Proposed-DECREASEFONTSIZE_TEXT-1-dM": true, + "QE-Proposed-DECREASEFONTSIZE_TEXT-1-body": true, + "QE-Proposed-DECREASEFONTSIZE_TEXT-1-div": true, + "QE-Proposed-HEADING_TEXT-1-dM": true, + "QE-Proposed-HEADING_TEXT-1-body": true, + "QE-Proposed-HEADING_TEXT-1-div": true, + "QE-Proposed-INCREASEFONTSIZE_TEXT-1-dM": true, + "QE-Proposed-INCREASEFONTSIZE_TEXT-1-body": true, + "QE-Proposed-INCREASEFONTSIZE_TEXT-1-div": true, + "QE-Proposed-PASTE_TEXT-1-dM": true, + "QE-Proposed-PASTE_TEXT-1-body": true, + "QE-Proposed-PASTE_TEXT-1-div": true, + "QE-Proposed-REDO_TEXT-1-dM": true, + "QE-Proposed-REDO_TEXT-1-body": true, + "QE-Proposed-REDO_TEXT-1-div": true, + "QE-Proposed-UNBOOKMARK_TEXT-1-dM": true, + "QE-Proposed-UNBOOKMARK_TEXT-1-body": true, + "QE-Proposed-UNBOOKMARK_TEXT-1-div": true, + "QE-Proposed-UNSELECT_TEXT-1-dM": true, + "QE-Proposed-UNSELECT_TEXT-1-body": true, + "QE-Proposed-UNSELECT_TEXT-1-div": true, + "QS-Proposed-SUB_SPAN.sub-1-SI-dM": true, + "QS-Proposed-SUB_SPAN.sub-1-SI-body": true, + "QS-Proposed-SUB_SPAN.sub-1-SI-div": true, + "QS-Proposed-SUB_MYSUB-1-SI-dM": true, + "QS-Proposed-SUB_MYSUB-1-SI-body": true, + "QS-Proposed-SUB_MYSUB-1-SI-div": true, + "QS-Proposed-SUP_SPAN.sup-1-SI-dM": true, + "QS-Proposed-SUP_SPAN.sup-1-SI-body": true, + "QS-Proposed-SUP_SPAN.sup-1-SI-div": true, + "QS-Proposed-SUP_MYSUP-1-SI-dM": true, + "QS-Proposed-SUP_MYSUP-1-SI-body": true, + "QS-Proposed-SUP_MYSUP-1-SI-div": true, + "QS-Proposed-JC_SPAN.jc-1-SI-dM": true, + "QS-Proposed-JC_SPAN.jc-1-SI-body": true, + "QS-Proposed-JC_SPAN.jc-1-SI-div": true, + "QS-Proposed-JC_MYJC-1-SI-dM": true, + "QS-Proposed-JC_MYJC-1-SI-body": true, + "QS-Proposed-JC_MYJC-1-SI-div": true, + "QS-Proposed-JF_SPANs:ta:j-1_SI-dM": true, + "QS-Proposed-JF_SPANs:ta:j-1_SI-body": true, + "QS-Proposed-JF_SPANs:ta:j-1_SI-div": true, + "QS-Proposed-JF_SPAN.jf-1-SI-dM": true, + "QS-Proposed-JF_SPAN.jf-1-SI-body": true, + "QS-Proposed-JF_SPAN.jf-1-SI-div": true, + "QS-Proposed-JF_MYJF-1-SI-dM": true, + "QS-Proposed-JF_MYJF-1-SI-body": true, + "QS-Proposed-JF_MYJF-1-SI-div": true, + "QS-Proposed-JL_TEXT_SI-dM": true, + "QS-Proposed-JL_TEXT_SI-body": true, + "QS-Proposed-JL_TEXT_SI-div": true, + "QS-Proposed-JR_SPANs:ta:r-1_SI-dM": true, + "QS-Proposed-JR_SPANs:ta:r-1_SI-body": true, + "QS-Proposed-JR_SPANs:ta:r-1_SI-div": true, + "QS-Proposed-JR_SPAN.jr-1-SI-dM": true, + "QS-Proposed-JR_SPAN.jr-1-SI-body": true, + "QS-Proposed-JR_SPAN.jr-1-SI-div": true, + "QS-Proposed-JR_MYJR-1-SI-dM": true, + "QS-Proposed-JR_MYJR-1-SI-body": true, + "QS-Proposed-JR_MYJR-1-SI-div": true, + "QV-Proposed-B_TEXT_SI-dM": true, + "QV-Proposed-B_TEXT_SI-body": true, + "QV-Proposed-B_TEXT_SI-div": true, + "QV-Proposed-B_B-1_SI-dM": true, + "QV-Proposed-B_B-1_SI-body": true, + "QV-Proposed-B_B-1_SI-div": true, + "QV-Proposed-B_STRONG-1_SI-dM": true, + "QV-Proposed-B_STRONG-1_SI-body": true, + "QV-Proposed-B_STRONG-1_SI-div": true, + "QV-Proposed-B_SPANs:fw:b-1_SI-dM": true, + "QV-Proposed-B_SPANs:fw:b-1_SI-body": true, + "QV-Proposed-B_SPANs:fw:b-1_SI-div": true, + "QV-Proposed-B_SPANs:fw:n-1_SI-dM": true, + "QV-Proposed-B_SPANs:fw:n-1_SI-body": true, + "QV-Proposed-B_SPANs:fw:n-1_SI-div": true, + "QV-Proposed-B_Bs:fw:n-1_SI-dM": true, + "QV-Proposed-B_Bs:fw:n-1_SI-body": true, + "QV-Proposed-B_Bs:fw:n-1_SI-div": true, + "QV-Proposed-B_SPAN.b-1_SI-dM": true, + "QV-Proposed-B_SPAN.b-1_SI-body": true, + "QV-Proposed-B_SPAN.b-1_SI-div": true, + "QV-Proposed-B_MYB-1-SI-dM": true, + "QV-Proposed-B_MYB-1-SI-body": true, + "QV-Proposed-B_MYB-1-SI-div": true, + "QV-Proposed-I_TEXT_SI-dM": true, + "QV-Proposed-I_TEXT_SI-body": true, + "QV-Proposed-I_TEXT_SI-div": true, + "QV-Proposed-I_I-1_SI-dM": true, + "QV-Proposed-I_I-1_SI-body": true, + "QV-Proposed-I_I-1_SI-div": true, + "QV-Proposed-I_EM-1_SI-dM": true, + "QV-Proposed-I_EM-1_SI-body": true, + "QV-Proposed-I_EM-1_SI-div": true, + "QV-Proposed-I_SPANs:fs:i-1_SI-dM": true, + "QV-Proposed-I_SPANs:fs:i-1_SI-body": true, + "QV-Proposed-I_SPANs:fs:i-1_SI-div": true, + "QV-Proposed-I_SPANs:fs:n-1_SI-dM": true, + "QV-Proposed-I_SPANs:fs:n-1_SI-body": true, + "QV-Proposed-I_SPANs:fs:n-1_SI-div": true, + "QV-Proposed-I_I-SPANs:fs:n-1_SI-dM": true, + "QV-Proposed-I_I-SPANs:fs:n-1_SI-body": true, + "QV-Proposed-I_I-SPANs:fs:n-1_SI-div": true, + "QV-Proposed-I_SPAN.i-1_SI-dM": true, + "QV-Proposed-I_SPAN.i-1_SI-body": true, + "QV-Proposed-I_SPAN.i-1_SI-div": true, + "QV-Proposed-I_MYI-1-SI-dM": true, + "QV-Proposed-I_MYI-1-SI-body": true, + "QV-Proposed-I_MYI-1-SI-div": true, + "QV-Proposed-FB_H1-H2-1_SL-dM": true, + "QV-Proposed-FB_H1-H2-1_SL-body": true, + "QV-Proposed-FB_H1-H2-1_SL-div": true, + "QV-Proposed-FB_H1-H2-1_SR-dM": true, + "QV-Proposed-FB_H1-H2-1_SR-body": true, + "QV-Proposed-FB_H1-H2-1_SR-div": true, + "QV-Proposed-FB_TEXT-ADDRESS-1_SL-dM": true, + "QV-Proposed-FB_TEXT-ADDRESS-1_SL-body": true, + "QV-Proposed-FB_TEXT-ADDRESS-1_SL-div": true, + "QV-Proposed-FB_H1-H2.TEXT.H2-1_SM-dM": true, + "QV-Proposed-FB_H1-H2.TEXT.H2-1_SM-body": true, + "QV-Proposed-FB_H1-H2.TEXT.H2-1_SM-div": true, + "QV-Proposed-H_H1-1_SC-dM": true, + "QV-Proposed-H_H1-1_SC-body": true, + "QV-Proposed-H_H1-1_SC-div": true, + "QV-Proposed-H_H3-1_SC-dM": true, + "QV-Proposed-H_H3-1_SC-body": true, + "QV-Proposed-H_H3-1_SC-div": true, + "QV-Proposed-H_H1-H2-H3-H4-1_SC-dM": true, + "QV-Proposed-H_H1-H2-H3-H4-1_SC-body": true, + "QV-Proposed-H_H1-H2-H3-H4-1_SC-div": true, + "QV-Proposed-H_P-1_SC-dM": false, + "QV-Proposed-H_P-1_SC-body": false, + "QV-Proposed-H_P-1_SC-div": false, + "QV-Proposed-FS_FONTs:fs:l-1_SI-dM": true, + "QV-Proposed-FS_FONTs:fs:l-1_SI-body": true, + "QV-Proposed-FS_FONTs:fs:l-1_SI-div": true, + "QV-Proposed-FS_FONT.ass.s:fs:l-1_SI-dM": true, + "QV-Proposed-FS_FONT.ass.s:fs:l-1_SI-body": true, + "QV-Proposed-FS_FONT.ass.s:fs:l-1_SI-div": true, + "QV-Proposed-FS_FONTsz:1.s:fs:xl-1_SI-dM": true, + "QV-Proposed-FS_FONTsz:1.s:fs:xl-1_SI-body": true, + "QV-Proposed-FS_FONTsz:1.s:fs:xl-1_SI-div": true, + "QV-Proposed-FS_SPAN.large-1_SI-dM": true, + "QV-Proposed-FS_SPAN.large-1_SI-body": true, + "QV-Proposed-FS_SPAN.large-1_SI-div": true, + "QV-Proposed-FS_SPAN.fs18px-1_SI-dM": true, + "QV-Proposed-FS_SPAN.fs18px-1_SI-body": true, + "QV-Proposed-FS_SPAN.fs18px-1_SI-div": true, + "QV-Proposed-FA_MYLARGE-1-SI-dM": true, + "QV-Proposed-FA_MYLARGE-1-SI-body": true, + "QV-Proposed-FA_MYLARGE-1-SI-div": true, + "QV-Proposed-FA_MYFS18PX-1-SI-dM": true, + "QV-Proposed-FA_MYFS18PX-1-SI-body": true, + "QV-Proposed-FA_MYFS18PX-1-SI-div": true, + "QV-Proposed-BC_FONTs:bc:fca-1_SI-dM": true, + "QV-Proposed-BC_FONTs:bc:fca-1_SI-body": true, + "QV-Proposed-BC_FONTs:bc:fca-1_SI-div": true, + "QV-Proposed-BC_SPANs:bc:abc-1_SI-dM": true, + "QV-Proposed-BC_SPANs:bc:abc-1_SI-body": true, + "QV-Proposed-BC_SPANs:bc:abc-1_SI-div": true, + "QV-Proposed-BC_FONTs:bc:084-SPAN-1_SI-dM": true, + "QV-Proposed-BC_FONTs:bc:084-SPAN-1_SI-body": true, + "QV-Proposed-BC_FONTs:bc:084-SPAN-1_SI-div": true, + "QV-Proposed-BC_SPANs:bc:cde-SPAN-1_SI-dM": true, + "QV-Proposed-BC_SPANs:bc:cde-SPAN-1_SI-body": true, + "QV-Proposed-BC_SPANs:bc:cde-SPAN-1_SI-div": true, + "QV-Proposed-BC_SPAN.ass.s:bc:rgb-1_SI-dM": true, + "QV-Proposed-BC_SPAN.ass.s:bc:rgb-1_SI-body": true, + "QV-Proposed-BC_SPAN.ass.s:bc:rgb-1_SI-div": true, + "QV-Proposed-BC_SPAN.bcred-1_SI-dM": true, + "QV-Proposed-BC_SPAN.bcred-1_SI-body": true, + "QV-Proposed-BC_SPAN.bcred-1_SI-div": true, + "QV-Proposed-BC_MYBCRED-1-SI-dM": true, + "QV-Proposed-BC_MYBCRED-1-SI-body": true, + "QV-Proposed-BC_MYBCRED-1-SI-div": true, + "QV-Proposed-HC_FONTs:bc:fc0-1_SI-dM": true, + "QV-Proposed-HC_FONTs:bc:fc0-1_SI-body": true, + "QV-Proposed-HC_FONTs:bc:fc0-1_SI-div": true, + "QV-Proposed-HC_SPANs:bc:a0c-1_SI-dM": true, + "QV-Proposed-HC_SPANs:bc:a0c-1_SI-body": true, + "QV-Proposed-HC_SPANs:bc:a0c-1_SI-div": true, + "QV-Proposed-HC_SPAN.ass.s:bc:rgb-1_SI-dM": true, + "QV-Proposed-HC_SPAN.ass.s:bc:rgb-1_SI-body": true, + "QV-Proposed-HC_SPAN.ass.s:bc:rgb-1_SI-div": true, + "QV-Proposed-HC_FONTs:bc:83e-SPAN-1_SI-dM": true, + "QV-Proposed-HC_FONTs:bc:83e-SPAN-1_SI-body": true, + "QV-Proposed-HC_FONTs:bc:83e-SPAN-1_SI-div": true, + "QV-Proposed-HC_SPANs:bc:b12-SPAN-1_SI-dM": true, + "QV-Proposed-HC_SPANs:bc:b12-SPAN-1_SI-body": true, + "QV-Proposed-HC_SPANs:bc:b12-SPAN-1_SI-div": true, + "QV-Proposed-HC_SPAN.bcred-1_SI-dM": true, + "QV-Proposed-HC_SPAN.bcred-1_SI-body": true, + "QV-Proposed-HC_SPAN.bcred-1_SI-div": true, + "QV-Proposed-HC_MYBCRED-1-SI-dM": true, + "QV-Proposed-HC_MYBCRED-1-SI-body": true, + "QV-Proposed-HC_MYBCRED-1-SI-div": true, + }, + select: { + "S-Proposed-UNSEL_TEXT-1_SI-dM": true, + "S-Proposed-UNSEL_TEXT-1_SI-body": true, + "S-Proposed-UNSEL_TEXT-1_SI-div": true, + "S-Proposed-SM:m.f.c_TEXT-1_SI-1-dM": true, + "S-Proposed-SM:m.f.c_TEXT-1_SI-1-body": true, + "S-Proposed-SM:m.f.c_TEXT-1_SI-1-div": true, + "S-Proposed-SM:m.b.c_TEXT-1_SI-1-dM": true, + "S-Proposed-SM:m.b.c_TEXT-1_SI-1-body": true, + "S-Proposed-SM:m.b.c_TEXT-1_SI-1-div": true, + "S-Proposed-SM:m.b.w_TEXT-1_SI-1-dM": true, + "S-Proposed-SM:m.b.w_TEXT-1_SI-1-body": true, + "S-Proposed-SM:m.b.w_TEXT-1_SI-1-div": true, + "S-Proposed-SM:m.b.w_TEXT-th_SC-2-dM": SpecialPowers.getBoolPref("intl.icu4x.segmenter.enabled", true), + "S-Proposed-SM:m.b.w_TEXT-th_SC-2-body": SpecialPowers.getBoolPref("intl.icu4x.segmenter.enabled", true), + "S-Proposed-SM:m.b.w_TEXT-th_SC-2-div": SpecialPowers.getBoolPref("intl.icu4x.segmenter.enabled", true), + "S-Proposed-SM:m.f.c_CHAR-5_SI-2-dM": true, + "S-Proposed-SM:m.f.c_CHAR-5_SI-2-body": true, + "S-Proposed-SM:m.f.c_CHAR-5_SI-2-div": true, + "S-Proposed-SM:m.f.c_CHAR-5_SR-dM": true, + "S-Proposed-SM:m.f.c_CHAR-5_SR-body": true, + "S-Proposed-SM:m.f.c_CHAR-5_SR-div": true, + "S-Proposed-SM:m.b.c_CHAR-5_SR-dM": true, + "S-Proposed-SM:m.b.c_CHAR-5_SR-body": true, + "S-Proposed-SM:m.b.c_CHAR-5_SR-div": true, + "S-Proposed-SM:m.f.w_TEXT-jp_SC-1-dM": !SpecialPowers.getBoolPref("intl.icu4x.segmenter.enabled", true), + "S-Proposed-SM:m.f.w_TEXT-jp_SC-1-body": !SpecialPowers.getBoolPref("intl.icu4x.segmenter.enabled", true), + "S-Proposed-SM:m.f.w_TEXT-jp_SC-1-div": !SpecialPowers.getBoolPref("intl.icu4x.segmenter.enabled", true), + "S-Proposed-SM:m.f.w_TEXT-jp_SC-2-dM": !SpecialPowers.getBoolPref("intl.icu4x.segmenter.enabled", true), + "S-Proposed-SM:m.f.w_TEXT-jp_SC-2-body": !SpecialPowers.getBoolPref("intl.icu4x.segmenter.enabled", true), + "S-Proposed-SM:m.f.w_TEXT-jp_SC-2-div": !SpecialPowers.getBoolPref("intl.icu4x.segmenter.enabled", true), + "S-Proposed-SM:m.f.w_TEXT-jp_SC-5-dM": true, + "S-Proposed-SM:m.f.w_TEXT-jp_SC-5-body": true, + "S-Proposed-SM:m.f.w_TEXT-jp_SC-5-div": true, + "S-Proposed-SM:e.b.w_TEXT-1_SI-3-dM": true, + "S-Proposed-SM:e.b.w_TEXT-1_SI-3-body": true, + "S-Proposed-SM:e.b.w_TEXT-1_SI-3-div": true, + "S-Proposed-SM:e.b.w_TEXT-1_SI-4-dM": true, + "S-Proposed-SM:e.b.w_TEXT-1_SI-4-body": true, + "S-Proposed-SM:e.b.w_TEXT-1_SI-4-div": true, + "S-Proposed-SM:e.b.w_TEXT-1_SI-5-dM": true, + "S-Proposed-SM:e.b.w_TEXT-1_SI-5-body": true, + "S-Proposed-SM:e.b.w_TEXT-1_SI-5-div": true, + "S-Proposed-SM:e.f.w_TEXT-1_SIR-1-dM": true, + "S-Proposed-SM:e.f.w_TEXT-1_SIR-1-body": true, + "S-Proposed-SM:e.f.w_TEXT-1_SIR-1-div": true, + "S-Proposed-SM:e.f.w_TEXT-1_SIR-3-dM": true, + "S-Proposed-SM:e.f.w_TEXT-1_SIR-3-body": true, + "S-Proposed-SM:e.f.w_TEXT-1_SIR-3-div": true, + "S-Proposed-SM:e.f.lb_BR.BR-1_SI-1-dM": true, + "S-Proposed-SM:e.f.lb_BR.BR-1_SI-1-body": true, + "S-Proposed-SM:e.f.lb_BR.BR-1_SI-1-div": true, + "S-Proposed-SM:e.f.lb_P.P.P-1_SI-1-dM": true, + "S-Proposed-SM:e.f.lb_P.P.P-1_SI-1-body": true, + "S-Proposed-SM:e.f.lb_P.P.P-1_SI-1-div": true, + "S-Proposed-SM:e.b.lb_BR.BR-1_SIR-2-dM": true, + "S-Proposed-SM:e.b.lb_BR.BR-1_SIR-2-body": true, + "S-Proposed-SM:e.b.lb_BR.BR-1_SIR-2-div": true, + "S-Proposed-SM:e.b.lb_P.P.P-1_SIR-2-dM": true, + "S-Proposed-SM:e.b.lb_P.P.P-1_SIR-2-body": true, + "S-Proposed-SM:e.b.lb_P.P.P-1_SIR-2-div": true, + "S-Proposed-SM:e.f.l_BR.BR-2_SI-1-dM": true, + "S-Proposed-SM:e.f.l_BR.BR-2_SI-1-body": true, + "S-Proposed-SM:e.f.l_BR.BR-2_SI-1-div": true, + "A-Proposed-B_TEXT-1_SIR-dM": true, + "A-Proposed-B_TEXT-1_SIR-body": true, + "A-Proposed-B_TEXT-1_SIR-div": true, + "A-Proposed-FS:18px_TEXT-1_SI-dM": true, + "A-Proposed-FS:18px_TEXT-1_SI-body": true, + "A-Proposed-FS:18px_TEXT-1_SI-div": true, + "A-Proposed-FS:large_TEXT-1_SI-dM": true, + "A-Proposed-FS:large_TEXT-1_SI-body": true, + "A-Proposed-FS:large_TEXT-1_SI-div": true, + "A-Proposed-INCFS:2_TEXT-1_SI-dM": true, + "A-Proposed-INCFS:2_TEXT-1_SI-body": true, + "A-Proposed-INCFS:2_TEXT-1_SI-div": true, + "A-Proposed-DECFS:2_TEXT-1_SI-dM": true, + "A-Proposed-DECFS:2_TEXT-1_SI-body": true, + "A-Proposed-DECFS:2_TEXT-1_SI-div": true, + "A-Proposed-CB:name_TEXT-1_SI-dM": true, + "A-Proposed-CB:name_TEXT-1_SI-body": true, + "A-Proposed-CB:name_TEXT-1_SI-div": true, + "A-Proposed-H:H1_TEXT-1_SC-dM": true, + "A-Proposed-H:H1_TEXT-1_SC-body": true, + "A-Proposed-H:H1_TEXT-1_SC-div": true, + "AC-Proposed-SUB_TEXT-1_SI-dM": true, + "AC-Proposed-SUB_TEXT-1_SI-body": true, + "AC-Proposed-SUB_TEXT-1_SI-div": true, + "AC-Proposed-SUP_TEXT-1_SI-dM": true, + "AC-Proposed-SUP_TEXT-1_SI-body": true, + "AC-Proposed-SUP_TEXT-1_SI-div": true, + "AC-Proposed-FS:2_TEXT-1_SI-dM": true, + "AC-Proposed-FS:2_TEXT-1_SI-body": true, + "AC-Proposed-FS:2_TEXT-1_SI-div": true, + "AC-Proposed-FS:18px_TEXT-1_SI-dM": true, + "AC-Proposed-FS:18px_TEXT-1_SI-body": true, + "AC-Proposed-FS:18px_TEXT-1_SI-div": true, + "AC-Proposed-FS:large_TEXT-1_SI-dM": true, + "AC-Proposed-FS:large_TEXT-1_SI-body": true, + "AC-Proposed-FS:large_TEXT-1_SI-div": true, + + // Those tests expect that <font> elements can be nested, but they don't + // match with the other browsers' behavior. + "C-Proposed-FC:g_FONTc:b.sz:6-1_SI-dM": true, + "C-Proposed-FC:g_FONTc:b.sz:6-1_SI-body": true, + "C-Proposed-FC:g_FONTc:b.sz:6-1_SI-div": true, + + "C-Proposed-FS:1_SPAN.ass.s:fs:large-1_SW-dM": true, + "C-Proposed-FS:1_SPAN.ass.s:fs:large-1_SW-body": true, + "C-Proposed-FS:1_SPAN.ass.s:fs:large-1_SW-div": true, + + // Those tests expect that <font> elements can be nested, but they don't + // match with the other browsers' behavior. + "C-Proposed-FS:2_FONTc:b.sz:6-1_SI-dM": true, + "C-Proposed-FS:2_FONTc:b.sz:6-1_SI-body": true, + "C-Proposed-FS:2_FONTc:b.sz:6-1_SI-div": true, + + "C-Proposed-FS:larger_FONTsz:4-dM": true, + "C-Proposed-FS:larger_FONTsz:4-body": true, + "C-Proposed-FS:larger_FONTsz:4-div": true, + "C-Proposed-FS:smaller_FONTsz:4-dM": true, + "C-Proposed-FS:smaller_FONTsz:4-body": true, + "C-Proposed-FS:smaller_FONTsz:4-div": true, + "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SO-dM": true, + "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SO-body": true, + "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SO-div": true, + "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SW-dM": true, + "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SW-body": true, + "C-Proposed-FB:h1_ADDRESS-FONTsz:4-1_SW-div": true, + "C-Proposed-FB:h1_ADDRESS-FONT.ass.sz:4-1_SW-dM": true, + "C-Proposed-FB:h1_ADDRESS-FONT.ass.sz:4-1_SW-body": true, + "C-Proposed-FB:h1_ADDRESS-FONT.ass.sz:4-1_SW-div": true, + "CC-Proposed-BC:gray_P-SPANs:bc:b-3_SL-dM": true, + "CC-Proposed-BC:gray_P-SPANs:bc:b-3_SL-body": true, + "CC-Proposed-BC:gray_P-SPANs:bc:b-3_SL-div": true, + "CC-Proposed-BC:gray_SPANs:bc:b-2_SL-dM": true, + "CC-Proposed-BC:gray_SPANs:bc:b-2_SL-body": true, + "CC-Proposed-BC:gray_SPANs:bc:b-2_SL-div": true, + "CC-Proposed-BC:gray_SPANs:bc:b-2_SR-dM": true, + "CC-Proposed-BC:gray_SPANs:bc:b-2_SR-body": true, + "CC-Proposed-BC:gray_SPANs:bc:b-2_SR-div": true, + "CC-Proposed-FS:1_SPANs:fs:l-1_SW-dM": true, + "CC-Proposed-FS:1_SPANs:fs:l-1_SW-body": true, + "CC-Proposed-FS:1_SPANs:fs:l-1_SW-div": true, + "CC-Proposed-FS:18px_SPANs:fs:l-1_SW-dM": true, + "CC-Proposed-FS:18px_SPANs:fs:l-1_SW-body": true, + "CC-Proposed-FS:18px_SPANs:fs:l-1_SW-div": true, + "CC-Proposed-FS:4_SPANs:fs:l-1_SW-dM": true, + "CC-Proposed-FS:4_SPANs:fs:l-1_SW-body": true, + "CC-Proposed-FS:4_SPANs:fs:l-1_SW-div": true, + "CC-Proposed-FS:4_SPANs:fs:18px-1_SW-dM": true, + "CC-Proposed-FS:4_SPANs:fs:18px-1_SW-body": true, + "CC-Proposed-FS:4_SPANs:fs:18px-1_SW-div": true, + "CC-Proposed-FS:larger_SPANs:fs:l-1_SI-dM": true, + "CC-Proposed-FS:larger_SPANs:fs:l-1_SI-body": true, + "CC-Proposed-FS:larger_SPANs:fs:l-1_SI-div": true, + "CC-Proposed-FS:smaller_SPANs:fs:l-1_SI-dM": true, + "CC-Proposed-FS:smaller_SPANs:fs:l-1_SI-body": true, + "CC-Proposed-FS:smaller_SPANs:fs:l-1_SI-div": true, + "U-RFC-UNLINK_A-1_SO-dM": true, + "U-RFC-UNLINK_A-1_SO-body": true, + "U-RFC-UNLINK_A-1_SO-div": true, + "U-RFC-UNLINK_A-1_SW-dM": true, + "U-RFC-UNLINK_A-1_SW-body": true, + "U-RFC-UNLINK_A-1_SW-div": true, + "U-RFC-UNLINK_A-2_SO-dM": true, + "U-RFC-UNLINK_A-2_SO-body": true, + "U-RFC-UNLINK_A-2_SO-div": true, + "U-RFC-UNLINK_A2-1_SO-dM": true, + "U-RFC-UNLINK_A2-1_SO-body": true, + "U-RFC-UNLINK_A2-1_SO-div": true, + "U-Proposed-B_B-P3-1_SO12-dM": true, + "U-Proposed-B_B-P3-1_SO12-body": true, + "U-Proposed-B_B-P3-1_SO12-div": true, + "U-Proposed-B_B-P-I..P-1_SO-I-dM": true, + "U-Proposed-B_B-P-I..P-1_SO-I-body": true, + "U-Proposed-B_B-P-I..P-1_SO-I-div": true, + "U-Proposed-B_B-2_SL-dM": true, + "U-Proposed-B_B-2_SL-body": true, + "U-Proposed-B_B-2_SL-div": true, + "U-Proposed-B_B-2_SR-dM": true, + "U-Proposed-B_B-2_SR-body": true, + "U-Proposed-B_B-2_SR-div": true, + "U-Proposed-I_I-P3-1_SO2-dM": true, + "U-Proposed-I_I-P3-1_SO2-body": true, + "U-Proposed-I_I-P3-1_SO2-div": true, + "U-Proposed-U_U-S-2_SI-dM": true, + "U-Proposed-U_U-S-2_SI-body": true, + "U-Proposed-U_U-S-2_SI-div": true, + "U-Proposed-U_U-P3-1_SO-dM": true, + "U-Proposed-U_U-P3-1_SO-body": true, + "U-Proposed-U_U-P3-1_SO-div": true, + "U-Proposed-S_DEL-1_SW-dM": true, + "U-Proposed-S_DEL-1_SW-body": true, + "U-Proposed-S_DEL-1_SW-div": true, + "U-Proposed-SUB_SPANs:va:sub-1_SW-dM": true, + "U-Proposed-SUB_SPANs:va:sub-1_SW-body": true, + "U-Proposed-SUB_SPANs:va:sub-1_SW-div": true, + "U-Proposed-SUP_SPANs:va:super-1_SW-dM": true, + "U-Proposed-SUP_SPANs:va:super-1_SW-body": true, + "U-Proposed-SUP_SPANs:va:super-1_SW-div": true, + "U-Proposed-UNLINK_A-1_SC-dM": true, + "U-Proposed-UNLINK_A-1_SC-body": true, + "U-Proposed-UNLINK_A-1_SC-div": true, + "U-Proposed-UNLINK_A-1_SI-dM": true, + "U-Proposed-UNLINK_A-1_SI-body": true, + "U-Proposed-UNLINK_A-1_SI-div": true, + "U-Proposed-UNLINK_A-2_SL-dM": true, + "U-Proposed-UNLINK_A-2_SL-body": true, + "U-Proposed-UNLINK_A-2_SL-div": true, + "U-Proposed-UNLINK_A-3_SR-dM": true, + "U-Proposed-UNLINK_A-3_SR-body": true, + "U-Proposed-UNLINK_A-3_SR-div": true, + "U-Proposed-OUTDENT_DIV-1_SW-dM": true, + "U-Proposed-OUTDENT_DIV-1_SW-body": true, + "U-Proposed-OUTDENT_DIV-1_SW-div": true, + "U-Proposed-REMOVEFORMAT_Ahref:url-1_SW-dM": true, + "U-Proposed-REMOVEFORMAT_Ahref:url-1_SW-body": true, + "U-Proposed-REMOVEFORMAT_Ahref:url-1_SW-div": true, + "U-Proposed-REMOVEFORMAT_TABLE-TBODY-TR-TD-1_SW-dM": true, + "U-Proposed-REMOVEFORMAT_TABLE-TBODY-TR-TD-1_SW-body": true, + "U-Proposed-REMOVEFORMAT_TABLE-TBODY-TR-TD-1_SW-div": true, + "U-Proposed-UNBOOKMARK_An:name-1_SW-dM": true, + "U-Proposed-UNBOOKMARK_An:name-1_SW-body": true, + "U-Proposed-UNBOOKMARK_An:name-1_SW-div": true, + "UC-Proposed-S_SPANc:s-1_SW-dM": true, + "UC-Proposed-S_SPANc:s-1_SW-body": true, + "UC-Proposed-S_SPANc:s-1_SW-div": true, + "UC-Proposed-S_SPANc:s-2_SI-dM": true, + "UC-Proposed-S_SPANc:s-2_SI-body": true, + "UC-Proposed-S_SPANc:s-2_SI-div": true, + "D-Proposed-CHAR-3_SC-dM": true, + "D-Proposed-CHAR-3_SC-body": true, + "D-Proposed-CHAR-3_SC-div": true, + "D-Proposed-CHAR-4_SC-dM": true, + "D-Proposed-CHAR-4_SC-body": true, + "D-Proposed-CHAR-4_SC-div": true, + "D-Proposed-CHAR-5_SC-dM": true, + "D-Proposed-CHAR-5_SC-body": true, + "D-Proposed-CHAR-5_SC-div": true, + "D-Proposed-CHAR-5_SI-1-dM": true, + "D-Proposed-CHAR-5_SI-1-body": true, + "D-Proposed-CHAR-5_SI-1-div": true, + "D-Proposed-CHAR-5_SI-2-dM": true, + "D-Proposed-CHAR-5_SI-2-body": true, + "D-Proposed-CHAR-5_SI-2-div": true, + "D-Proposed-CHAR-5_SR-dM": true, + "D-Proposed-CHAR-5_SR-body": true, + "D-Proposed-CHAR-5_SR-div": true, + "D-Proposed-CHAR-6_SC-dM": true, + "D-Proposed-CHAR-6_SC-body": true, + "D-Proposed-CHAR-6_SC-div": true, + "D-Proposed-CHAR-7_SC-dM": true, + "D-Proposed-CHAR-7_SC-body": true, + "D-Proposed-CHAR-7_SC-div": true, + "D-Proposed-B-1_SW-div": true, + "D-Proposed-B-1_SL-dM": true, + "D-Proposed-B-1_SL-body": true, + "D-Proposed-B-1_SL-div": true, + "D-Proposed-B-1_SR-dM": true, + "D-Proposed-B-1_SR-body": true, + "D-Proposed-B-1_SR-div": true, + "D-Proposed-B.I-1_SM-dM": true, + "D-Proposed-B.I-1_SM-body": true, + "D-Proposed-B.I-1_SM-div": true, + "D-Proposed-OL-LI2-1_SO1-dM": true, + "D-Proposed-OL-LI2-1_SO1-body": true, + "D-Proposed-OL-LI2-1_SO1-div": true, + "D-Proposed-OL-LI-1_SW-dM": true, + "D-Proposed-OL-LI-1_SW-body": true, + "D-Proposed-OL-LI-1_SW-div": true, + "D-Proposed-OL-LI-1_SO-dM": true, + "D-Proposed-OL-LI-1_SO-body": true, + "D-Proposed-OL-LI-1_SO-div": true, + "D-Proposed-HR.BR-1_SM-dM": true, + "D-Proposed-HR.BR-1_SM-body": true, + "D-Proposed-HR.BR-1_SM-div": true, + "D-Proposed-TR2rs:2-1_SO1-dM": true, + "D-Proposed-TR2rs:2-1_SO1-body": true, + "D-Proposed-TR2rs:2-1_SO1-div": true, + "D-Proposed-TR2rs:2-1_SO2-dM": true, + "D-Proposed-TR2rs:2-1_SO2-body": true, + "D-Proposed-TR2rs:2-1_SO2-div": true, + "D-Proposed-TR3rs:3-1_SO1-dM": true, + "D-Proposed-TR3rs:3-1_SO1-body": true, + "D-Proposed-TR3rs:3-1_SO1-div": true, + "D-Proposed-TR3rs:3-1_SO2-dM": true, + "D-Proposed-TR3rs:3-1_SO2-body": true, + "D-Proposed-TR3rs:3-1_SO2-div": true, + "D-Proposed-TR3rs:3-1_SO3-dM": true, + "D-Proposed-TR3rs:3-1_SO3-body": true, + "D-Proposed-TR3rs:3-1_SO3-div": true, + "D-Proposed-DIV:ce:false-1_SB-dM": true, + "D-Proposed-DIV:ce:false-1_SL-dM": true, + "D-Proposed-DIV:ce:false-1_SL-body": true, + "D-Proposed-DIV:ce:false-1_SL-div": true, + "D-Proposed-DIV:ce:false-1_SR-dM": true, + "D-Proposed-DIV:ce:false-1_SR-body": true, + "D-Proposed-DIV:ce:false-1_SR-div": true, + "D-Proposed-DIV:ce:false-1_SI-dM": true, + "D-Proposed-SPAN:d:ib-2_SL-dM": true, + "D-Proposed-SPAN:d:ib-2_SL-body": true, + "D-Proposed-SPAN:d:ib-2_SL-div": true, + "D-Proposed-SPAN:d:ib-3_SR-dM": true, + "D-Proposed-SPAN:d:ib-3_SR-body": true, + "D-Proposed-SPAN:d:ib-3_SR-div": true, + "FD-Proposed-B-1_SW-div": true, + "FD-Proposed-OL-LI-1_SW-dM": true, + "FD-Proposed-OL-LI-1_SW-body": true, + "FD-Proposed-OL-LI-1_SW-div": true, + "FD-Proposed-OL-LI-1_SO-dM": true, + "FD-Proposed-OL-LI-1_SO-body": true, + "FD-Proposed-OL-LI-1_SO-div": true, + "FD-Proposed-TABLE-1_SB-dM": true, + "FD-Proposed-TABLE-1_SB-body": true, + "FD-Proposed-TABLE-1_SB-div": true, + "FD-Proposed-TD-1_SE-dM": true, + "FD-Proposed-TD-1_SE-body": true, + "FD-Proposed-TD-1_SE-div": true, + "FD-Proposed-TD2-1_SE1-dM": true, + "FD-Proposed-TD2-1_SE1-body": true, + "FD-Proposed-TD2-1_SE1-div": true, + "FD-Proposed-TD2-1_SM-dM": true, + "FD-Proposed-TD2-1_SM-body": true, + "FD-Proposed-TD2-1_SM-div": true, + "FD-Proposed-TR2rs:2-1_SO1-dM": true, + "FD-Proposed-TR2rs:2-1_SO1-body": true, + "FD-Proposed-TR2rs:2-1_SO1-div": true, + "FD-Proposed-TR2rs:2-1_SO2-dM": true, + "FD-Proposed-TR2rs:2-1_SO2-body": true, + "FD-Proposed-TR2rs:2-1_SO2-div": true, + "FD-Proposed-TR3rs:3-1_SO1-dM": true, + "FD-Proposed-TR3rs:3-1_SO1-body": true, + "FD-Proposed-TR3rs:3-1_SO1-div": true, + "FD-Proposed-TR3rs:3-1_SO2-dM": true, + "FD-Proposed-TR3rs:3-1_SO2-body": true, + "FD-Proposed-TR3rs:3-1_SO2-div": true, + "FD-Proposed-TR3rs:3-1_SO3-dM": true, + "FD-Proposed-TR3rs:3-1_SO3-body": true, + "FD-Proposed-TR3rs:3-1_SO3-div": true, + "FD-Proposed-DIV:ce:false-1_SB-dM": true, + "FD-Proposed-DIV:ce:false-1_SL-dM": true, + "FD-Proposed-DIV:ce:false-1_SL-body": true, + "FD-Proposed-DIV:ce:false-1_SL-div": true, + "FD-Proposed-DIV:ce:false-1_SR-dM": true, + "FD-Proposed-DIV:ce:false-1_SR-body": true, + "FD-Proposed-DIV:ce:false-1_SR-div": true, + "FD-Proposed-DIV:ce:false-1_SI-dM": true, + "I-Proposed-IHR_TEXT-1_SC-dM": true, + "I-Proposed-IHR_TEXT-1_SC-body": true, + "I-Proposed-IHR_TEXT-1_SC-div": true, + "I-Proposed-IHR_TEXT-1_SI-dM": true, + "I-Proposed-IHR_TEXT-1_SI-body": true, + "I-Proposed-IHR_TEXT-1_SI-div": true, + "I-Proposed-IHR_B-1_SC-dM": true, + "I-Proposed-IHR_B-1_SC-body": true, + "I-Proposed-IHR_B-1_SC-div": true, + "I-Proposed-IHR_B-1_SS-dM": true, + "I-Proposed-IHR_B-1_SS-body": true, + "I-Proposed-IHR_B-1_SS-div": true, + "I-Proposed-IHR_B-I-1_SMR-dM": true, + "I-Proposed-IHR_B-I-1_SMR-body": true, + "I-Proposed-IHR_B-I-1_SMR-div": true, + "I-Proposed-IIMG:._SPAN-IMG-1_SO-dM": true, + "I-Proposed-IIMG:._SPAN-IMG-1_SO-body": true, + "I-Proposed-IIMG:._SPAN-IMG-1_SO-div": true, + "I-Proposed-IIMG:._IMG-1_SO-dM": true, + "I-Proposed-IIMG:._IMG-1_SO-body": true, + "I-Proposed-IIMG:._IMG-1_SO-div": true, + "I-Proposed-IHTML:BR_TEXT-1_SC-dM": true, + "I-Proposed-IHTML:BR_TEXT-1_SC-body": true, + "I-Proposed-IHTML:BR_TEXT-1_SC-div": true, + "I-Proposed-IHTML:S_TEXT-1_SI-dM": true, + "I-Proposed-IHTML:S_TEXT-1_SI-body": true, + "I-Proposed-IHTML:S_TEXT-1_SI-div": true, + "I-Proposed-IHTML:H1.H2_TEXT-1_SI-dM": true, + "I-Proposed-IHTML:H1.H2_TEXT-1_SI-body": true, + "I-Proposed-IHTML:H1.H2_TEXT-1_SI-div": true, + "I-Proposed-IHTML:P-B_TEXT-1_SI-dM": true, + "I-Proposed-IHTML:P-B_TEXT-1_SI-body": true, + "I-Proposed-IHTML:P-B_TEXT-1_SI-div": true, + "Q-Proposed-SELECTALL_TEXT-1-dM": true, + "Q-Proposed-SELECTALL_TEXT-1-body": true, + "Q-Proposed-SELECTALL_TEXT-1-div": true, + "Q-Proposed-UNSELECT_TEXT-1-dM": true, + "Q-Proposed-UNSELECT_TEXT-1-body": true, + "Q-Proposed-UNSELECT_TEXT-1-div": true, + "Q-Proposed-UNDO_TEXT-1-dM": true, + "Q-Proposed-UNDO_TEXT-1-body": true, + "Q-Proposed-UNDO_TEXT-1-div": true, + "Q-Proposed-REDO_TEXT-1-dM": true, + "Q-Proposed-REDO_TEXT-1-body": true, + "Q-Proposed-REDO_TEXT-1-div": true, + "Q-Proposed-BOLD_TEXT-1-dM": true, + "Q-Proposed-BOLD_TEXT-1-body": true, + "Q-Proposed-BOLD_TEXT-1-div": true, + "Q-Proposed-BOLD_B-dM": true, + "Q-Proposed-BOLD_B-body": true, + "Q-Proposed-BOLD_B-div": true, + "Q-Proposed-ITALIC_TEXT-1-dM": true, + "Q-Proposed-ITALIC_TEXT-1-body": true, + "Q-Proposed-ITALIC_TEXT-1-div": true, + "Q-Proposed-ITALIC_I-dM": true, + "Q-Proposed-ITALIC_I-body": true, + "Q-Proposed-ITALIC_I-div": true, + "Q-Proposed-UNDERLINE_TEXT-1-dM": true, + "Q-Proposed-UNDERLINE_TEXT-1-body": true, + "Q-Proposed-UNDERLINE_TEXT-1-div": true, + "Q-Proposed-STRIKETHROUGH_TEXT-1-dM": true, + "Q-Proposed-STRIKETHROUGH_TEXT-1-body": true, + "Q-Proposed-STRIKETHROUGH_TEXT-1-div": true, + "Q-Proposed-SUBSCRIPT_TEXT-1-dM": true, + "Q-Proposed-SUBSCRIPT_TEXT-1-body": true, + "Q-Proposed-SUBSCRIPT_TEXT-1-div": true, + "Q-Proposed-SUPERSCRIPT_TEXT-1-dM": true, + "Q-Proposed-SUPERSCRIPT_TEXT-1-body": true, + "Q-Proposed-SUPERSCRIPT_TEXT-1-div": true, + "Q-Proposed-FORMATBLOCK_TEXT-1-dM": true, + "Q-Proposed-FORMATBLOCK_TEXT-1-body": true, + "Q-Proposed-FORMATBLOCK_TEXT-1-div": true, + "Q-Proposed-CREATELINK_TEXT-1-dM": true, + "Q-Proposed-CREATELINK_TEXT-1-body": true, + "Q-Proposed-CREATELINK_TEXT-1-div": true, + "Q-Proposed-UNLINK_TEXT-1-dM": true, + "Q-Proposed-UNLINK_TEXT-1-body": true, + "Q-Proposed-UNLINK_TEXT-1-div": true, + "Q-Proposed-INSERTHTML_TEXT-1-dM": true, + "Q-Proposed-INSERTHTML_TEXT-1-body": true, + "Q-Proposed-INSERTHTML_TEXT-1-div": true, + "Q-Proposed-INSERTHORIZONTALRULE_TEXT-1-dM": true, + "Q-Proposed-INSERTHORIZONTALRULE_TEXT-1-body": true, + "Q-Proposed-INSERTHORIZONTALRULE_TEXT-1-div": true, + "Q-Proposed-INSERTIMAGE_TEXT-1-dM": true, + "Q-Proposed-INSERTIMAGE_TEXT-1-body": true, + "Q-Proposed-INSERTIMAGE_TEXT-1-div": true, + "Q-Proposed-INSERTLINEBREAK_TEXT-1-dM": true, + "Q-Proposed-INSERTLINEBREAK_TEXT-1-body": true, + "Q-Proposed-INSERTLINEBREAK_TEXT-1-div": true, + "Q-Proposed-INSERTPARAGRAPH_TEXT-1-dM": true, + "Q-Proposed-INSERTPARAGRAPH_TEXT-1-body": true, + "Q-Proposed-INSERTPARAGRAPH_TEXT-1-div": true, + "Q-Proposed-INSERTORDEREDLIST_TEXT-1-dM": true, + "Q-Proposed-INSERTORDEREDLIST_TEXT-1-body": true, + "Q-Proposed-INSERTORDEREDLIST_TEXT-1-div": true, + "Q-Proposed-INSERTUNORDEREDLIST_TEXT-1-dM": true, + "Q-Proposed-INSERTUNORDEREDLIST_TEXT-1-body": true, + "Q-Proposed-INSERTUNORDEREDLIST_TEXT-1-div": true, + "Q-Proposed-INSERTTEXT_TEXT-1-dM": true, + "Q-Proposed-INSERTTEXT_TEXT-1-body": true, + "Q-Proposed-INSERTTEXT_TEXT-1-div": true, + "Q-Proposed-DELETE_TEXT-1-dM": true, + "Q-Proposed-DELETE_TEXT-1-body": true, + "Q-Proposed-DELETE_TEXT-1-div": true, + "Q-Proposed-FORWARDDELETE_TEXT-1-dM": true, + "Q-Proposed-FORWARDDELETE_TEXT-1-body": true, + "Q-Proposed-FORWARDDELETE_TEXT-1-div": true, + "Q-Proposed-STYLEWITHCSS_TEXT-1-dM": true, + "Q-Proposed-STYLEWITHCSS_TEXT-1-body": true, + "Q-Proposed-STYLEWITHCSS_TEXT-1-div": true, + "Q-Proposed-CONTENTREADONLY_TEXT-1-dM": true, + "Q-Proposed-CONTENTREADONLY_TEXT-1-body": true, + "Q-Proposed-CONTENTREADONLY_TEXT-1-div": true, + "Q-Proposed-BACKCOLOR_TEXT-1-dM": true, + "Q-Proposed-BACKCOLOR_TEXT-1-body": true, + "Q-Proposed-BACKCOLOR_TEXT-1-div": true, + "Q-Proposed-FORECOLOR_TEXT-1-dM": true, + "Q-Proposed-FORECOLOR_TEXT-1-body": true, + "Q-Proposed-FORECOLOR_TEXT-1-div": true, + "Q-Proposed-HILITECOLOR_TEXT-1-dM": true, + "Q-Proposed-HILITECOLOR_TEXT-1-body": true, + "Q-Proposed-HILITECOLOR_TEXT-1-div": true, + "Q-Proposed-FONTNAME_TEXT-1-dM": true, + "Q-Proposed-FONTNAME_TEXT-1-body": true, + "Q-Proposed-FONTNAME_TEXT-1-div": true, + "Q-Proposed-FONTSIZE_TEXT-1-dM": true, + "Q-Proposed-FONTSIZE_TEXT-1-body": true, + "Q-Proposed-FONTSIZE_TEXT-1-div": true, + "Q-Proposed-INCREASEFONTSIZE_TEXT-1-dM": true, + "Q-Proposed-INCREASEFONTSIZE_TEXT-1-body": true, + "Q-Proposed-INCREASEFONTSIZE_TEXT-1-div": true, + "Q-Proposed-DECREASEFONTSIZE_TEXT-1-dM": true, + "Q-Proposed-DECREASEFONTSIZE_TEXT-1-body": true, + "Q-Proposed-DECREASEFONTSIZE_TEXT-1-div": true, + "Q-Proposed-HEADING_TEXT-1-dM": true, + "Q-Proposed-HEADING_TEXT-1-body": true, + "Q-Proposed-HEADING_TEXT-1-div": true, + "Q-Proposed-INDENT_TEXT-1-dM": true, + "Q-Proposed-INDENT_TEXT-1-body": true, + "Q-Proposed-INDENT_TEXT-1-div": true, + "Q-Proposed-OUTDENT_TEXT-1-dM": true, + "Q-Proposed-OUTDENT_TEXT-1-body": true, + "Q-Proposed-OUTDENT_TEXT-1-div": true, + "Q-Proposed-CREATEBOOKMARK_TEXT-1-dM": true, + "Q-Proposed-CREATEBOOKMARK_TEXT-1-body": true, + "Q-Proposed-CREATEBOOKMARK_TEXT-1-div": true, + "Q-Proposed-UNBOOKMARK_TEXT-1-dM": true, + "Q-Proposed-UNBOOKMARK_TEXT-1-body": true, + "Q-Proposed-UNBOOKMARK_TEXT-1-div": true, + "Q-Proposed-JUSTIFYCENTER_TEXT-1-dM": true, + "Q-Proposed-JUSTIFYCENTER_TEXT-1-body": true, + "Q-Proposed-JUSTIFYCENTER_TEXT-1-div": true, + "Q-Proposed-JUSTIFYFULL_TEXT-1-dM": true, + "Q-Proposed-JUSTIFYFULL_TEXT-1-body": true, + "Q-Proposed-JUSTIFYFULL_TEXT-1-div": true, + "Q-Proposed-JUSTIFYLEFT_TEXT-1-dM": true, + "Q-Proposed-JUSTIFYLEFT_TEXT-1-body": true, + "Q-Proposed-JUSTIFYLEFT_TEXT-1-div": true, + "Q-Proposed-JUSTIFYRIGHT_TEXT-1-dM": true, + "Q-Proposed-JUSTIFYRIGHT_TEXT-1-body": true, + "Q-Proposed-JUSTIFYRIGHT_TEXT-1-div": true, + "Q-Proposed-REMOVEFORMAT_TEXT-1-dM": true, + "Q-Proposed-REMOVEFORMAT_TEXT-1-body": true, + "Q-Proposed-REMOVEFORMAT_TEXT-1-div": true, + "Q-Proposed-COPY_TEXT-1-dM": true, + "Q-Proposed-COPY_TEXT-1-body": true, + "Q-Proposed-COPY_TEXT-1-div": true, + "Q-Proposed-CUT_TEXT-1-dM": true, + "Q-Proposed-CUT_TEXT-1-body": true, + "Q-Proposed-CUT_TEXT-1-div": true, + "Q-Proposed-PASTE_TEXT-1-dM": true, + "Q-Proposed-PASTE_TEXT-1-body": true, + "Q-Proposed-PASTE_TEXT-1-div": true, + "Q-Proposed-garbage-1_TEXT-1-dM": true, + "Q-Proposed-garbage-1_TEXT-1-body": true, + "Q-Proposed-garbage-1_TEXT-1-div": true, + "QE-Proposed-SELECTALL_TEXT-1-dM": true, + "QE-Proposed-SELECTALL_TEXT-1-body": true, + "QE-Proposed-SELECTALL_TEXT-1-div": true, + "QE-Proposed-UNSELECT_TEXT-1-dM": true, + "QE-Proposed-UNSELECT_TEXT-1-body": true, + "QE-Proposed-UNSELECT_TEXT-1-div": true, + "QE-Proposed-UNDO_TEXT-1-dM": true, + "QE-Proposed-UNDO_TEXT-1-body": true, + "QE-Proposed-UNDO_TEXT-1-div": true, + "QE-Proposed-REDO_TEXT-1-dM": true, + "QE-Proposed-REDO_TEXT-1-body": true, + "QE-Proposed-REDO_TEXT-1-div": true, + "QE-Proposed-BOLD_TEXT-1-dM": true, + "QE-Proposed-BOLD_TEXT-1-body": true, + "QE-Proposed-BOLD_TEXT-1-div": true, + "QE-Proposed-ITALIC_TEXT-1-dM": true, + "QE-Proposed-ITALIC_TEXT-1-body": true, + "QE-Proposed-ITALIC_TEXT-1-div": true, + "QE-Proposed-UNDERLINE_TEXT-1-dM": true, + "QE-Proposed-UNDERLINE_TEXT-1-body": true, + "QE-Proposed-UNDERLINE_TEXT-1-div": true, + "QE-Proposed-STRIKETHROUGH_TEXT-1-dM": true, + "QE-Proposed-STRIKETHROUGH_TEXT-1-body": true, + "QE-Proposed-STRIKETHROUGH_TEXT-1-div": true, + "QE-Proposed-SUBSCRIPT_TEXT-1-dM": true, + "QE-Proposed-SUBSCRIPT_TEXT-1-body": true, + "QE-Proposed-SUBSCRIPT_TEXT-1-div": true, + "QE-Proposed-SUPERSCRIPT_TEXT-1-dM": true, + "QE-Proposed-SUPERSCRIPT_TEXT-1-body": true, + "QE-Proposed-SUPERSCRIPT_TEXT-1-div": true, + "QE-Proposed-FORMATBLOCK_TEXT-1-dM": true, + "QE-Proposed-FORMATBLOCK_TEXT-1-body": true, + "QE-Proposed-FORMATBLOCK_TEXT-1-div": true, + "QE-Proposed-CREATELINK_TEXT-1-dM": true, + "QE-Proposed-CREATELINK_TEXT-1-body": true, + "QE-Proposed-CREATELINK_TEXT-1-div": true, + "QE-Proposed-UNLINK_TEXT-1-dM": true, + "QE-Proposed-UNLINK_TEXT-1-body": true, + "QE-Proposed-UNLINK_TEXT-1-div": true, + "QE-Proposed-INSERTHTML_TEXT-1-dM": true, + "QE-Proposed-INSERTHTML_TEXT-1-body": true, + "QE-Proposed-INSERTHTML_TEXT-1-div": true, + "QE-Proposed-INSERTHORIZONTALRULE_TEXT-1-dM": true, + "QE-Proposed-INSERTHORIZONTALRULE_TEXT-1-body": true, + "QE-Proposed-INSERTHORIZONTALRULE_TEXT-1-div": true, + "QE-Proposed-INSERTIMAGE_TEXT-1-dM": true, + "QE-Proposed-INSERTIMAGE_TEXT-1-body": true, + "QE-Proposed-INSERTIMAGE_TEXT-1-div": true, + "QE-Proposed-INSERTLINEBREAK_TEXT-1-dM": true, + "QE-Proposed-INSERTLINEBREAK_TEXT-1-body": true, + "QE-Proposed-INSERTLINEBREAK_TEXT-1-div": true, + "QE-Proposed-INSERTPARAGRAPH_TEXT-1-dM": true, + "QE-Proposed-INSERTPARAGRAPH_TEXT-1-body": true, + "QE-Proposed-INSERTPARAGRAPH_TEXT-1-div": true, + "QE-Proposed-INSERTORDEREDLIST_TEXT-1-dM": true, + "QE-Proposed-INSERTORDEREDLIST_TEXT-1-body": true, + "QE-Proposed-INSERTORDEREDLIST_TEXT-1-div": true, + "QE-Proposed-INSERTUNORDEREDLIST_TEXT-1-dM": true, + "QE-Proposed-INSERTUNORDEREDLIST_TEXT-1-body": true, + "QE-Proposed-INSERTUNORDEREDLIST_TEXT-1-div": true, + "QE-Proposed-INSERTTEXT_TEXT-1-dM": true, + "QE-Proposed-INSERTTEXT_TEXT-1-body": true, + "QE-Proposed-INSERTTEXT_TEXT-1-div": true, + "QE-Proposed-DELETE_TEXT-1-dM": true, + "QE-Proposed-DELETE_TEXT-1-body": true, + "QE-Proposed-DELETE_TEXT-1-div": true, + "QE-Proposed-FORWARDDELETE_TEXT-1-dM": true, + "QE-Proposed-FORWARDDELETE_TEXT-1-body": true, + "QE-Proposed-FORWARDDELETE_TEXT-1-div": true, + "QE-Proposed-STYLEWITHCSS_TEXT-1-dM": true, + "QE-Proposed-STYLEWITHCSS_TEXT-1-body": true, + "QE-Proposed-STYLEWITHCSS_TEXT-1-div": true, + "QE-Proposed-CONTENTREADONLY_TEXT-1-dM": true, + "QE-Proposed-CONTENTREADONLY_TEXT-1-body": true, + "QE-Proposed-CONTENTREADONLY_TEXT-1-div": true, + "QE-Proposed-BACKCOLOR_TEXT-1-dM": true, + "QE-Proposed-BACKCOLOR_TEXT-1-body": true, + "QE-Proposed-BACKCOLOR_TEXT-1-div": true, + "QE-Proposed-FORECOLOR_TEXT-1-dM": true, + "QE-Proposed-FORECOLOR_TEXT-1-body": true, + "QE-Proposed-FORECOLOR_TEXT-1-div": true, + "QE-Proposed-HILITECOLOR_TEXT-1-dM": true, + "QE-Proposed-HILITECOLOR_TEXT-1-body": true, + "QE-Proposed-HILITECOLOR_TEXT-1-div": true, + "QE-Proposed-FONTNAME_TEXT-1-dM": true, + "QE-Proposed-FONTNAME_TEXT-1-body": true, + "QE-Proposed-FONTNAME_TEXT-1-div": true, + "QE-Proposed-FONTSIZE_TEXT-1-dM": true, + "QE-Proposed-FONTSIZE_TEXT-1-body": true, + "QE-Proposed-FONTSIZE_TEXT-1-div": true, + "QE-Proposed-INCREASEFONTSIZE_TEXT-1-dM": true, + "QE-Proposed-INCREASEFONTSIZE_TEXT-1-body": true, + "QE-Proposed-INCREASEFONTSIZE_TEXT-1-div": true, + "QE-Proposed-DECREASEFONTSIZE_TEXT-1-dM": true, + "QE-Proposed-DECREASEFONTSIZE_TEXT-1-body": true, + "QE-Proposed-DECREASEFONTSIZE_TEXT-1-div": true, + "QE-Proposed-HEADING_TEXT-1-dM": true, + "QE-Proposed-HEADING_TEXT-1-body": true, + "QE-Proposed-HEADING_TEXT-1-div": true, + "QE-Proposed-INDENT_TEXT-1-dM": true, + "QE-Proposed-INDENT_TEXT-1-body": true, + "QE-Proposed-INDENT_TEXT-1-div": true, + "QE-Proposed-OUTDENT_TEXT-1-dM": true, + "QE-Proposed-OUTDENT_TEXT-1-body": true, + "QE-Proposed-OUTDENT_TEXT-1-div": true, + "QE-Proposed-CREATEBOOKMARK_TEXT-1-dM": true, + "QE-Proposed-CREATEBOOKMARK_TEXT-1-body": true, + "QE-Proposed-CREATEBOOKMARK_TEXT-1-div": true, + "QE-Proposed-UNBOOKMARK_TEXT-1-dM": true, + "QE-Proposed-UNBOOKMARK_TEXT-1-body": true, + "QE-Proposed-UNBOOKMARK_TEXT-1-div": true, + "QE-Proposed-JUSTIFYCENTER_TEXT-1-dM": true, + "QE-Proposed-JUSTIFYCENTER_TEXT-1-body": true, + "QE-Proposed-JUSTIFYCENTER_TEXT-1-div": true, + "QE-Proposed-JUSTIFYFULL_TEXT-1-dM": true, + "QE-Proposed-JUSTIFYFULL_TEXT-1-body": true, + "QE-Proposed-JUSTIFYFULL_TEXT-1-div": true, + "QE-Proposed-JUSTIFYLEFT_TEXT-1-dM": true, + "QE-Proposed-JUSTIFYLEFT_TEXT-1-body": true, + "QE-Proposed-JUSTIFYLEFT_TEXT-1-div": true, + "QE-Proposed-JUSTIFYRIGHT_TEXT-1-dM": true, + "QE-Proposed-JUSTIFYRIGHT_TEXT-1-body": true, + "QE-Proposed-JUSTIFYRIGHT_TEXT-1-div": true, + "QE-Proposed-REMOVEFORMAT_TEXT-1-dM": true, + "QE-Proposed-REMOVEFORMAT_TEXT-1-body": true, + "QE-Proposed-REMOVEFORMAT_TEXT-1-div": true, + "QE-Proposed-COPY_TEXT-1-dM": true, + "QE-Proposed-COPY_TEXT-1-body": true, + "QE-Proposed-COPY_TEXT-1-div": true, + "QE-Proposed-CUT_TEXT-1-dM": true, + "QE-Proposed-CUT_TEXT-1-body": true, + "QE-Proposed-CUT_TEXT-1-div": true, + "QE-Proposed-PASTE_TEXT-1-dM": true, + "QE-Proposed-PASTE_TEXT-1-body": true, + "QE-Proposed-PASTE_TEXT-1-div": true, + "QE-Proposed-garbage-1_TEXT-1-dM": true, + "QE-Proposed-garbage-1_TEXT-1-body": true, + "QE-Proposed-garbage-1_TEXT-1-div": true, + "QI-Proposed-SELECTALL_TEXT-1-dM": true, + "QI-Proposed-SELECTALL_TEXT-1-body": true, + "QI-Proposed-SELECTALL_TEXT-1-div": true, + "QI-Proposed-UNSELECT_TEXT-1-dM": true, + "QI-Proposed-UNSELECT_TEXT-1-body": true, + "QI-Proposed-UNSELECT_TEXT-1-div": true, + "QI-Proposed-UNDO_TEXT-1-dM": true, + "QI-Proposed-UNDO_TEXT-1-body": true, + "QI-Proposed-UNDO_TEXT-1-div": true, + "QI-Proposed-REDO_TEXT-1-dM": true, + "QI-Proposed-REDO_TEXT-1-body": true, + "QI-Proposed-REDO_TEXT-1-div": true, + "QI-Proposed-BOLD_TEXT-1-dM": true, + "QI-Proposed-BOLD_TEXT-1-body": true, + "QI-Proposed-BOLD_TEXT-1-div": true, + "QI-Proposed-ITALIC_TEXT-1-dM": true, + "QI-Proposed-ITALIC_TEXT-1-body": true, + "QI-Proposed-ITALIC_TEXT-1-div": true, + "QI-Proposed-UNDERLINE_TEXT-1-dM": true, + "QI-Proposed-UNDERLINE_TEXT-1-body": true, + "QI-Proposed-UNDERLINE_TEXT-1-div": true, + "QI-Proposed-STRIKETHROUGH_TEXT-1-dM": true, + "QI-Proposed-STRIKETHROUGH_TEXT-1-body": true, + "QI-Proposed-STRIKETHROUGH_TEXT-1-div": true, + "QI-Proposed-SUBSCRIPT_TEXT-1-dM": true, + "QI-Proposed-SUBSCRIPT_TEXT-1-body": true, + "QI-Proposed-SUBSCRIPT_TEXT-1-div": true, + "QI-Proposed-SUPERSCRIPT_TEXT-1-dM": true, + "QI-Proposed-SUPERSCRIPT_TEXT-1-body": true, + "QI-Proposed-SUPERSCRIPT_TEXT-1-div": true, + "QI-Proposed-FORMATBLOCK_TEXT-1-dM": true, + "QI-Proposed-FORMATBLOCK_TEXT-1-body": true, + "QI-Proposed-FORMATBLOCK_TEXT-1-div": true, + "QI-Proposed-CREATELINK_TEXT-1-dM": true, + "QI-Proposed-CREATELINK_TEXT-1-body": true, + "QI-Proposed-CREATELINK_TEXT-1-div": true, + "QI-Proposed-UNLINK_TEXT-1-dM": true, + "QI-Proposed-UNLINK_TEXT-1-body": true, + "QI-Proposed-UNLINK_TEXT-1-div": true, + "QI-Proposed-INSERTHTML_TEXT-1-dM": true, + "QI-Proposed-INSERTHTML_TEXT-1-body": true, + "QI-Proposed-INSERTHTML_TEXT-1-div": true, + "QI-Proposed-INSERTHORIZONTALRULE_TEXT-1-dM": true, + "QI-Proposed-INSERTHORIZONTALRULE_TEXT-1-body": true, + "QI-Proposed-INSERTHORIZONTALRULE_TEXT-1-div": true, + "QI-Proposed-INSERTIMAGE_TEXT-1-dM": true, + "QI-Proposed-INSERTIMAGE_TEXT-1-body": true, + "QI-Proposed-INSERTIMAGE_TEXT-1-div": true, + "QI-Proposed-INSERTLINEBREAK_TEXT-1-dM": true, + "QI-Proposed-INSERTLINEBREAK_TEXT-1-body": true, + "QI-Proposed-INSERTLINEBREAK_TEXT-1-div": true, + "QI-Proposed-INSERTPARAGRAPH_TEXT-1-dM": true, + "QI-Proposed-INSERTPARAGRAPH_TEXT-1-body": true, + "QI-Proposed-INSERTPARAGRAPH_TEXT-1-div": true, + "QI-Proposed-INSERTORDEREDLIST_TEXT-1-dM": true, + "QI-Proposed-INSERTORDEREDLIST_TEXT-1-body": true, + "QI-Proposed-INSERTORDEREDLIST_TEXT-1-div": true, + "QI-Proposed-INSERTUNORDEREDLIST_TEXT-1-dM": true, + "QI-Proposed-INSERTUNORDEREDLIST_TEXT-1-body": true, + "QI-Proposed-INSERTUNORDEREDLIST_TEXT-1-div": true, + "QI-Proposed-INSERTTEXT_TEXT-1-dM": true, + "QI-Proposed-INSERTTEXT_TEXT-1-body": true, + "QI-Proposed-INSERTTEXT_TEXT-1-div": true, + "QI-Proposed-DELETE_TEXT-1-dM": true, + "QI-Proposed-DELETE_TEXT-1-body": true, + "QI-Proposed-DELETE_TEXT-1-div": true, + "QI-Proposed-FORWARDDELETE_TEXT-1-dM": true, + "QI-Proposed-FORWARDDELETE_TEXT-1-body": true, + "QI-Proposed-FORWARDDELETE_TEXT-1-div": true, + "QI-Proposed-STYLEWITHCSS_TEXT-1-dM": true, + "QI-Proposed-STYLEWITHCSS_TEXT-1-body": true, + "QI-Proposed-STYLEWITHCSS_TEXT-1-div": true, + "QI-Proposed-CONTENTREADONLY_TEXT-1-dM": true, + "QI-Proposed-CONTENTREADONLY_TEXT-1-body": true, + "QI-Proposed-CONTENTREADONLY_TEXT-1-div": true, + "QI-Proposed-BACKCOLOR_TEXT-1-dM": true, + "QI-Proposed-BACKCOLOR_TEXT-1-body": true, + "QI-Proposed-BACKCOLOR_TEXT-1-div": true, + "QI-Proposed-FORECOLOR_TEXT-1-dM": true, + "QI-Proposed-FORECOLOR_TEXT-1-body": true, + "QI-Proposed-FORECOLOR_TEXT-1-div": true, + "QI-Proposed-HILITECOLOR_TEXT-1-dM": true, + "QI-Proposed-HILITECOLOR_TEXT-1-body": true, + "QI-Proposed-HILITECOLOR_TEXT-1-div": true, + "QI-Proposed-FONTNAME_TEXT-1-dM": true, + "QI-Proposed-FONTNAME_TEXT-1-body": true, + "QI-Proposed-FONTNAME_TEXT-1-div": true, + "QI-Proposed-FONTSIZE_TEXT-1-dM": true, + "QI-Proposed-FONTSIZE_TEXT-1-body": true, + "QI-Proposed-FONTSIZE_TEXT-1-div": true, + "QI-Proposed-INCREASEFONTSIZE_TEXT-1-dM": true, + "QI-Proposed-INCREASEFONTSIZE_TEXT-1-body": true, + "QI-Proposed-INCREASEFONTSIZE_TEXT-1-div": true, + "QI-Proposed-DECREASEFONTSIZE_TEXT-1-dM": true, + "QI-Proposed-DECREASEFONTSIZE_TEXT-1-body": true, + "QI-Proposed-DECREASEFONTSIZE_TEXT-1-div": true, + "QI-Proposed-HEADING_TEXT-1-dM": true, + "QI-Proposed-HEADING_TEXT-1-body": true, + "QI-Proposed-HEADING_TEXT-1-div": true, + "QI-Proposed-INDENT_TEXT-1-dM": true, + "QI-Proposed-INDENT_TEXT-1-body": true, + "QI-Proposed-INDENT_TEXT-1-div": true, + "QI-Proposed-OUTDENT_TEXT-1-dM": true, + "QI-Proposed-OUTDENT_TEXT-1-body": true, + "QI-Proposed-OUTDENT_TEXT-1-div": true, + "QI-Proposed-CREATEBOOKMARK_TEXT-1-dM": true, + "QI-Proposed-CREATEBOOKMARK_TEXT-1-body": true, + "QI-Proposed-CREATEBOOKMARK_TEXT-1-div": true, + "QI-Proposed-UNBOOKMARK_TEXT-1-dM": true, + "QI-Proposed-UNBOOKMARK_TEXT-1-body": true, + "QI-Proposed-UNBOOKMARK_TEXT-1-div": true, + "QI-Proposed-JUSTIFYCENTER_TEXT-1-dM": true, + "QI-Proposed-JUSTIFYCENTER_TEXT-1-body": true, + "QI-Proposed-JUSTIFYCENTER_TEXT-1-div": true, + "QI-Proposed-JUSTIFYFULL_TEXT-1-dM": true, + "QI-Proposed-JUSTIFYFULL_TEXT-1-body": true, + "QI-Proposed-JUSTIFYFULL_TEXT-1-div": true, + "QI-Proposed-JUSTIFYLEFT_TEXT-1-dM": true, + "QI-Proposed-JUSTIFYLEFT_TEXT-1-body": true, + "QI-Proposed-JUSTIFYLEFT_TEXT-1-div": true, + "QI-Proposed-JUSTIFYRIGHT_TEXT-1-dM": true, + "QI-Proposed-JUSTIFYRIGHT_TEXT-1-body": true, + "QI-Proposed-JUSTIFYRIGHT_TEXT-1-div": true, + "QI-Proposed-REMOVEFORMAT_TEXT-1-dM": true, + "QI-Proposed-REMOVEFORMAT_TEXT-1-body": true, + "QI-Proposed-REMOVEFORMAT_TEXT-1-div": true, + "QI-Proposed-COPY_TEXT-1-dM": true, + "QI-Proposed-COPY_TEXT-1-body": true, + "QI-Proposed-COPY_TEXT-1-div": true, + "QI-Proposed-CUT_TEXT-1-dM": true, + "QI-Proposed-CUT_TEXT-1-body": true, + "QI-Proposed-CUT_TEXT-1-div": true, + "QI-Proposed-PASTE_TEXT-1-dM": true, + "QI-Proposed-PASTE_TEXT-1-body": true, + "QI-Proposed-PASTE_TEXT-1-div": true, + "QI-Proposed-garbage-1_TEXT-1-dM": true, + "QI-Proposed-garbage-1_TEXT-1-body": true, + "QI-Proposed-garbage-1_TEXT-1-div": true, + "QS-Proposed-B_TEXT_SI-dM": true, + "QS-Proposed-B_TEXT_SI-body": true, + "QS-Proposed-B_TEXT_SI-div": true, + "QS-Proposed-B_B-1_SI-dM": true, + "QS-Proposed-B_B-1_SI-body": true, + "QS-Proposed-B_B-1_SI-div": true, + "QS-Proposed-B_STRONG-1_SI-dM": true, + "QS-Proposed-B_STRONG-1_SI-body": true, + "QS-Proposed-B_STRONG-1_SI-div": true, + "QS-Proposed-B_SPANs:fw:b-1_SI-dM": true, + "QS-Proposed-B_SPANs:fw:b-1_SI-body": true, + "QS-Proposed-B_SPANs:fw:b-1_SI-div": true, + "QS-Proposed-B_SPANs:fw:n-1_SI-dM": true, + "QS-Proposed-B_SPANs:fw:n-1_SI-body": true, + "QS-Proposed-B_SPANs:fw:n-1_SI-div": true, + "QS-Proposed-B_Bs:fw:n-1_SI-dM": true, + "QS-Proposed-B_Bs:fw:n-1_SI-body": true, + "QS-Proposed-B_Bs:fw:n-1_SI-div": true, + "QS-Proposed-B_B-SPANs:fw:n-1_SI-dM": true, + "QS-Proposed-B_B-SPANs:fw:n-1_SI-body": true, + "QS-Proposed-B_B-SPANs:fw:n-1_SI-div": true, + "QS-Proposed-B_SPAN.b-1-SI-dM": true, + "QS-Proposed-B_SPAN.b-1-SI-body": true, + "QS-Proposed-B_SPAN.b-1-SI-div": true, + "QS-Proposed-B_MYB-1-SI-dM": true, + "QS-Proposed-B_MYB-1-SI-body": true, + "QS-Proposed-B_MYB-1-SI-div": true, + "QS-Proposed-B_B-I-1_SC-dM": true, + "QS-Proposed-B_B-I-1_SC-body": true, + "QS-Proposed-B_B-I-1_SC-div": true, + "QS-Proposed-B_B-I-1_SL-dM": true, + "QS-Proposed-B_B-I-1_SL-body": true, + "QS-Proposed-B_B-I-1_SL-div": true, + "QS-Proposed-B_B-I-1_SR-dM": true, + "QS-Proposed-B_B-I-1_SR-body": true, + "QS-Proposed-B_B-I-1_SR-div": true, + "QS-Proposed-B_STRONG-I-1_SC-dM": true, + "QS-Proposed-B_STRONG-I-1_SC-body": true, + "QS-Proposed-B_STRONG-I-1_SC-div": true, + "QS-Proposed-B_B-I-U-1_SC-dM": true, + "QS-Proposed-B_B-I-U-1_SC-body": true, + "QS-Proposed-B_B-I-U-1_SC-div": true, + "QS-Proposed-B_B-I-U-1_SM-dM": true, + "QS-Proposed-B_B-I-U-1_SM-body": true, + "QS-Proposed-B_B-I-U-1_SM-div": true, + "QS-Proposed-B_TEXT-B-1_SO-1-dM": true, + "QS-Proposed-B_TEXT-B-1_SO-1-body": true, + "QS-Proposed-B_TEXT-B-1_SO-1-div": true, + "QS-Proposed-B_TEXT-B-1_SO-2-dM": true, + "QS-Proposed-B_TEXT-B-1_SO-2-body": true, + "QS-Proposed-B_TEXT-B-1_SO-2-div": true, + "QS-Proposed-B_TEXT-B-1_SL-dM": true, + "QS-Proposed-B_TEXT-B-1_SL-body": true, + "QS-Proposed-B_TEXT-B-1_SL-div": true, + "QS-Proposed-B_TEXT-B-1_SR-dM": true, + "QS-Proposed-B_TEXT-B-1_SR-body": true, + "QS-Proposed-B_TEXT-B-1_SR-div": true, + "QS-Proposed-B_TEXT-B-1_SO-3-dM": true, + "QS-Proposed-B_TEXT-B-1_SO-3-body": true, + "QS-Proposed-B_TEXT-B-1_SO-3-div": true, + "QS-Proposed-B_B.TEXT.B-1_SM-dM": true, + "QS-Proposed-B_B.TEXT.B-1_SM-body": true, + "QS-Proposed-B_B.TEXT.B-1_SM-div": true, + "QS-Proposed-B_B.B.B-1_SM-dM": true, + "QS-Proposed-B_B.B.B-1_SM-body": true, + "QS-Proposed-B_B.B.B-1_SM-div": true, + "QS-Proposed-B_B.STRONG.B-1_SM-dM": true, + "QS-Proposed-B_B.STRONG.B-1_SM-body": true, + "QS-Proposed-B_B.STRONG.B-1_SM-div": true, + "QS-Proposed-B_SPAN.b.MYB.SPANs:fw:b-1_SM-dM": true, + "QS-Proposed-B_SPAN.b.MYB.SPANs:fw:b-1_SM-body": true, + "QS-Proposed-B_SPAN.b.MYB.SPANs:fw:b-1_SM-div": true, + "QS-Proposed-I_TEXT_SI-dM": true, + "QS-Proposed-I_TEXT_SI-body": true, + "QS-Proposed-I_TEXT_SI-div": true, + "QS-Proposed-I_I-1_SI-dM": true, + "QS-Proposed-I_I-1_SI-body": true, + "QS-Proposed-I_I-1_SI-div": true, + "QS-Proposed-I_EM-1_SI-dM": true, + "QS-Proposed-I_EM-1_SI-body": true, + "QS-Proposed-I_EM-1_SI-div": true, + "QS-Proposed-I_SPANs:fs:i-1_SI-dM": true, + "QS-Proposed-I_SPANs:fs:i-1_SI-body": true, + "QS-Proposed-I_SPANs:fs:i-1_SI-div": true, + "QS-Proposed-I_SPANs:fs:n-1_SI-dM": true, + "QS-Proposed-I_SPANs:fs:n-1_SI-body": true, + "QS-Proposed-I_SPANs:fs:n-1_SI-div": true, + "QS-Proposed-I_I-SPANs:fs:n-1_SI-dM": true, + "QS-Proposed-I_I-SPANs:fs:n-1_SI-body": true, + "QS-Proposed-I_I-SPANs:fs:n-1_SI-div": true, + "QS-Proposed-I_SPAN.i-1-SI-dM": true, + "QS-Proposed-I_SPAN.i-1-SI-body": true, + "QS-Proposed-I_SPAN.i-1-SI-div": true, + "QS-Proposed-I_MYI-1-SI-dM": true, + "QS-Proposed-I_MYI-1-SI-body": true, + "QS-Proposed-I_MYI-1-SI-div": true, + "QS-Proposed-U_TEXT_SI-dM": true, + "QS-Proposed-U_TEXT_SI-body": true, + "QS-Proposed-U_TEXT_SI-div": true, + "QS-Proposed-U_U-1_SI-dM": true, + "QS-Proposed-U_U-1_SI-body": true, + "QS-Proposed-U_U-1_SI-div": true, + "QS-Proposed-U_Us:td:n-1_SI-dM": true, + "QS-Proposed-U_Us:td:n-1_SI-body": true, + "QS-Proposed-U_Us:td:n-1_SI-div": true, + "QS-Proposed-U_Ah:url-1_SI-dM": true, + "QS-Proposed-U_Ah:url-1_SI-body": true, + "QS-Proposed-U_Ah:url-1_SI-div": true, + "QS-Proposed-U_Ah:url.s:td:n-1_SI-dM": true, + "QS-Proposed-U_Ah:url.s:td:n-1_SI-body": true, + "QS-Proposed-U_Ah:url.s:td:n-1_SI-div": true, + "QS-Proposed-U_SPANs:td:u-1_SI-dM": true, + "QS-Proposed-U_SPANs:td:u-1_SI-body": true, + "QS-Proposed-U_SPANs:td:u-1_SI-div": true, + "QS-Proposed-U_SPAN.u-1-SI-dM": true, + "QS-Proposed-U_SPAN.u-1-SI-body": true, + "QS-Proposed-U_SPAN.u-1-SI-div": true, + "QS-Proposed-U_MYU-1-SI-dM": true, + "QS-Proposed-U_MYU-1-SI-body": true, + "QS-Proposed-U_MYU-1-SI-div": true, + "QS-Proposed-S_TEXT_SI-dM": true, + "QS-Proposed-S_TEXT_SI-body": true, + "QS-Proposed-S_TEXT_SI-div": true, + "QS-Proposed-S_S-1_SI-dM": true, + "QS-Proposed-S_S-1_SI-body": true, + "QS-Proposed-S_S-1_SI-div": true, + "QS-Proposed-S_STRIKE-1_SI-dM": true, + "QS-Proposed-S_STRIKE-1_SI-body": true, + "QS-Proposed-S_STRIKE-1_SI-div": true, + "QS-Proposed-S_STRIKEs:td:n-1_SI-dM": true, + "QS-Proposed-S_STRIKEs:td:n-1_SI-body": true, + "QS-Proposed-S_STRIKEs:td:n-1_SI-div": true, + "QS-Proposed-S_DEL-1_SI-dM": true, + "QS-Proposed-S_DEL-1_SI-body": true, + "QS-Proposed-S_DEL-1_SI-div": true, + "QS-Proposed-S_SPANs:td:lt-1_SI-dM": true, + "QS-Proposed-S_SPANs:td:lt-1_SI-body": true, + "QS-Proposed-S_SPANs:td:lt-1_SI-div": true, + "QS-Proposed-S_SPAN.s-1-SI-dM": true, + "QS-Proposed-S_SPAN.s-1-SI-body": true, + "QS-Proposed-S_SPAN.s-1-SI-div": true, + "QS-Proposed-S_MYS-1-SI-dM": true, + "QS-Proposed-S_MYS-1-SI-body": true, + "QS-Proposed-S_MYS-1-SI-div": true, + "QS-Proposed-S_S.STRIKE.DEL-1_SM-dM": true, + "QS-Proposed-S_S.STRIKE.DEL-1_SM-body": true, + "QS-Proposed-S_S.STRIKE.DEL-1_SM-div": true, + "QS-Proposed-SUB_TEXT_SI-dM": true, + "QS-Proposed-SUB_TEXT_SI-body": true, + "QS-Proposed-SUB_TEXT_SI-div": true, + "QS-Proposed-SUB_SUB-1_SI-dM": true, + "QS-Proposed-SUB_SUB-1_SI-body": true, + "QS-Proposed-SUB_SUB-1_SI-div": true, + "QS-Proposed-SUB_SPAN.sub-1-SI-dM": true, + "QS-Proposed-SUB_SPAN.sub-1-SI-body": true, + "QS-Proposed-SUB_SPAN.sub-1-SI-div": true, + "QS-Proposed-SUB_MYSUB-1-SI-dM": true, + "QS-Proposed-SUB_MYSUB-1-SI-body": true, + "QS-Proposed-SUB_MYSUB-1-SI-div": true, + "QS-Proposed-SUP_TEXT_SI-dM": true, + "QS-Proposed-SUP_TEXT_SI-body": true, + "QS-Proposed-SUP_TEXT_SI-div": true, + "QS-Proposed-SUP_SUP-1_SI-dM": true, + "QS-Proposed-SUP_SUP-1_SI-body": true, + "QS-Proposed-SUP_SUP-1_SI-div": true, + "QS-Proposed-IOL_TEXT_SI-dM": true, + "QS-Proposed-IOL_TEXT_SI-body": true, + "QS-Proposed-IOL_TEXT_SI-div": true, + "QS-Proposed-SUP_SPAN.sup-1-SI-dM": true, + "QS-Proposed-SUP_SPAN.sup-1-SI-body": true, + "QS-Proposed-SUP_SPAN.sup-1-SI-div": true, + "QS-Proposed-SUP_MYSUP-1-SI-dM": true, + "QS-Proposed-SUP_MYSUP-1-SI-body": true, + "QS-Proposed-SUP_MYSUP-1-SI-div": true, + "QS-Proposed-IOL_TEXT-1_SI-dM": true, + "QS-Proposed-IOL_TEXT-1_SI-body": true, + "QS-Proposed-IOL_TEXT-1_SI-div": true, + "QS-Proposed-IOL_OL-LI-1_SI-dM": true, + "QS-Proposed-IOL_OL-LI-1_SI-body": true, + "QS-Proposed-IOL_OL-LI-1_SI-div": true, + "QS-Proposed-IOL_UL_LI-1_SI-dM": true, + "QS-Proposed-IOL_UL_LI-1_SI-body": true, + "QS-Proposed-IOL_UL_LI-1_SI-div": true, + "QS-Proposed-IUL_TEXT_SI-dM": true, + "QS-Proposed-IUL_TEXT_SI-body": true, + "QS-Proposed-IUL_TEXT_SI-div": true, + "QS-Proposed-IUL_OL-LI-1_SI-dM": true, + "QS-Proposed-IUL_OL-LI-1_SI-body": true, + "QS-Proposed-IUL_OL-LI-1_SI-div": true, + "QS-Proposed-IUL_UL-LI-1_SI-dM": true, + "QS-Proposed-IUL_UL-LI-1_SI-body": true, + "QS-Proposed-IUL_UL-LI-1_SI-div": true, + "QS-Proposed-JC_TEXT_SI-dM": true, + "QS-Proposed-JC_TEXT_SI-body": true, + "QS-Proposed-JC_TEXT_SI-div": true, + "QS-Proposed-JC_DIVa:c-1_SI-dM": true, + "QS-Proposed-JC_DIVa:c-1_SI-body": true, + "QS-Proposed-JC_DIVa:c-1_SI-div": true, + "QS-Proposed-JC_Pa:c-1_SI-dM": true, + "QS-Proposed-JC_Pa:c-1_SI-body": true, + "QS-Proposed-JC_Pa:c-1_SI-div": true, + "QS-Proposed-JC_SPANs:ta:c-1_SI-dM": true, + "QS-Proposed-JC_SPANs:ta:c-1_SI-body": true, + "QS-Proposed-JC_SPANs:ta:c-1_SI-div": true, + "QS-Proposed-JC_SPAN.jc-1-SI-dM": true, + "QS-Proposed-JC_SPAN.jc-1-SI-body": true, + "QS-Proposed-JC_SPAN.jc-1-SI-div": true, + "QS-Proposed-JC_MYJC-1-SI-dM": true, + "QS-Proposed-JC_MYJC-1-SI-body": true, + "QS-Proposed-JC_MYJC-1-SI-div": true, + "QS-Proposed-JF_TEXT_SI-dM": true, + "QS-Proposed-JF_TEXT_SI-body": true, + "QS-Proposed-JF_TEXT_SI-div": true, + "QS-Proposed-JF_DIVa:j-1_SI-dM": true, + "QS-Proposed-JF_DIVa:j-1_SI-body": true, + "QS-Proposed-JF_DIVa:j-1_SI-div": true, + "QS-Proposed-JF_Pa:j-1_SI-dM": true, + "QS-Proposed-JF_Pa:j-1_SI-body": true, + "QS-Proposed-JF_Pa:j-1_SI-div": true, + "QS-Proposed-JF_SPANs:ta:j-1_SI-dM": true, + "QS-Proposed-JF_SPANs:ta:j-1_SI-body": true, + "QS-Proposed-JF_SPANs:ta:j-1_SI-div": true, + "QS-Proposed-JF_SPAN.jf-1-SI-dM": true, + "QS-Proposed-JF_SPAN.jf-1-SI-body": true, + "QS-Proposed-JF_SPAN.jf-1-SI-div": true, + "QS-Proposed-JF_MYJF-1-SI-dM": true, + "QS-Proposed-JF_MYJF-1-SI-body": true, + "QS-Proposed-JF_MYJF-1-SI-div": true, + "QS-Proposed-JL_TEXT_SI-dM": true, + "QS-Proposed-JL_TEXT_SI-body": true, + "QS-Proposed-JL_TEXT_SI-div": true, + "QS-Proposed-JL_DIVa:l-1_SI-dM": true, + "QS-Proposed-JL_DIVa:l-1_SI-body": true, + "QS-Proposed-JL_DIVa:l-1_SI-div": true, + "QS-Proposed-JL_Pa:l-1_SI-dM": true, + "QS-Proposed-JL_Pa:l-1_SI-body": true, + "QS-Proposed-JL_Pa:l-1_SI-div": true, + "QS-Proposed-JL_SPANs:ta:l-1_SI-dM": true, + "QS-Proposed-JL_SPANs:ta:l-1_SI-body": true, + "QS-Proposed-JL_SPANs:ta:l-1_SI-div": true, + "QS-Proposed-JL_SPAN.jl-1-SI-dM": true, + "QS-Proposed-JL_SPAN.jl-1-SI-body": true, + "QS-Proposed-JL_SPAN.jl-1-SI-div": true, + "QS-Proposed-JL_MYJL-1-SI-dM": true, + "QS-Proposed-JL_MYJL-1-SI-body": true, + "QS-Proposed-JL_MYJL-1-SI-div": true, + "QS-Proposed-JR_TEXT_SI-dM": true, + "QS-Proposed-JR_TEXT_SI-body": true, + "QS-Proposed-JR_TEXT_SI-div": true, + "QS-Proposed-JR_DIVa:r-1_SI-dM": true, + "QS-Proposed-JR_DIVa:r-1_SI-body": true, + "QS-Proposed-JR_DIVa:r-1_SI-div": true, + "QS-Proposed-JR_Pa:r-1_SI-dM": true, + "QS-Proposed-JR_Pa:r-1_SI-body": true, + "QS-Proposed-JR_Pa:r-1_SI-div": true, + "QS-Proposed-JR_SPANs:ta:r-1_SI-dM": true, + "QS-Proposed-JR_SPANs:ta:r-1_SI-body": true, + "QS-Proposed-JR_SPANs:ta:r-1_SI-div": true, + "QS-Proposed-JR_SPAN.jr-1-SI-dM": true, + "QS-Proposed-JR_SPAN.jr-1-SI-body": true, + "QS-Proposed-JR_SPAN.jr-1-SI-div": true, + "QS-Proposed-JR_MYJR-1-SI-dM": true, + "QS-Proposed-JR_MYJR-1-SI-body": true, + "QS-Proposed-JR_MYJR-1-SI-div": true, + "QV-Proposed-B_TEXT_SI-dM": true, + "QV-Proposed-B_TEXT_SI-body": true, + "QV-Proposed-B_TEXT_SI-div": true, + "QV-Proposed-B_B-1_SI-dM": true, + "QV-Proposed-B_B-1_SI-body": true, + "QV-Proposed-B_B-1_SI-div": true, + "QV-Proposed-B_STRONG-1_SI-dM": true, + "QV-Proposed-B_STRONG-1_SI-body": true, + "QV-Proposed-B_STRONG-1_SI-div": true, + "QV-Proposed-B_SPANs:fw:b-1_SI-dM": true, + "QV-Proposed-B_SPANs:fw:b-1_SI-body": true, + "QV-Proposed-B_SPANs:fw:b-1_SI-div": true, + "QV-Proposed-B_SPANs:fw:n-1_SI-dM": true, + "QV-Proposed-B_SPANs:fw:n-1_SI-body": true, + "QV-Proposed-B_SPANs:fw:n-1_SI-div": true, + "QV-Proposed-B_Bs:fw:n-1_SI-dM": true, + "QV-Proposed-B_Bs:fw:n-1_SI-body": true, + "QV-Proposed-B_Bs:fw:n-1_SI-div": true, + "QV-Proposed-B_SPAN.b-1_SI-dM": true, + "QV-Proposed-B_SPAN.b-1_SI-body": true, + "QV-Proposed-B_SPAN.b-1_SI-div": true, + "QV-Proposed-B_MYB-1-SI-dM": true, + "QV-Proposed-B_MYB-1-SI-body": true, + "QV-Proposed-B_MYB-1-SI-div": true, + "QV-Proposed-I_TEXT_SI-dM": true, + "QV-Proposed-I_TEXT_SI-body": true, + "QV-Proposed-I_TEXT_SI-div": true, + "QV-Proposed-I_I-1_SI-dM": true, + "QV-Proposed-I_I-1_SI-body": true, + "QV-Proposed-I_I-1_SI-div": true, + "QV-Proposed-I_EM-1_SI-dM": true, + "QV-Proposed-I_EM-1_SI-body": true, + "QV-Proposed-I_EM-1_SI-div": true, + "QV-Proposed-I_SPANs:fs:i-1_SI-dM": true, + "QV-Proposed-I_SPANs:fs:i-1_SI-body": true, + "QV-Proposed-I_SPANs:fs:i-1_SI-div": true, + "QV-Proposed-I_SPANs:fs:n-1_SI-dM": true, + "QV-Proposed-I_SPANs:fs:n-1_SI-body": true, + "QV-Proposed-I_SPANs:fs:n-1_SI-div": true, + "QV-Proposed-I_I-SPANs:fs:n-1_SI-dM": true, + "QV-Proposed-I_I-SPANs:fs:n-1_SI-body": true, + "QV-Proposed-I_I-SPANs:fs:n-1_SI-div": true, + "QV-Proposed-I_SPAN.i-1_SI-dM": true, + "QV-Proposed-I_SPAN.i-1_SI-body": true, + "QV-Proposed-I_SPAN.i-1_SI-div": true, + "QV-Proposed-I_MYI-1-SI-dM": true, + "QV-Proposed-I_MYI-1-SI-body": true, + "QV-Proposed-I_MYI-1-SI-div": true, + "QV-Proposed-FB_TEXT-1_SC-dM": true, + "QV-Proposed-FB_TEXT-1_SC-body": true, + "QV-Proposed-FB_TEXT-1_SC-div": true, + "QV-Proposed-FB_H1-1_SC-dM": true, + "QV-Proposed-FB_H1-1_SC-body": true, + "QV-Proposed-FB_H1-1_SC-div": true, + "QV-Proposed-FB_PRE-1_SC-dM": true, + "QV-Proposed-FB_PRE-1_SC-body": true, + "QV-Proposed-FB_PRE-1_SC-div": true, + "QV-Proposed-FB_BQ-1_SC-dM": true, + "QV-Proposed-FB_BQ-1_SC-body": true, + "QV-Proposed-FB_BQ-1_SC-div": true, + "QV-Proposed-FB_ADDRESS-1_SC-dM": true, + "QV-Proposed-FB_ADDRESS-1_SC-body": true, + "QV-Proposed-FB_ADDRESS-1_SC-div": true, + "QV-Proposed-FB_H1-H2-1_SC-dM": true, + "QV-Proposed-FB_H1-H2-1_SC-body": true, + "QV-Proposed-FB_H1-H2-1_SC-div": true, + "QV-Proposed-FB_H1-H2-1_SL-dM": true, + "QV-Proposed-FB_H1-H2-1_SL-body": true, + "QV-Proposed-FB_H1-H2-1_SL-div": true, + "QV-Proposed-FB_H1-H2-1_SR-dM": true, + "QV-Proposed-FB_H1-H2-1_SR-body": true, + "QV-Proposed-FB_H1-H2-1_SR-div": true, + "QV-Proposed-FB_TEXT-ADDRESS-1_SL-dM": true, + "QV-Proposed-FB_TEXT-ADDRESS-1_SL-body": true, + "QV-Proposed-FB_TEXT-ADDRESS-1_SL-div": true, + "QV-Proposed-FB_TEXT-ADDRESS-1_SR-dM": true, + "QV-Proposed-FB_TEXT-ADDRESS-1_SR-body": true, + "QV-Proposed-FB_TEXT-ADDRESS-1_SR-div": true, + "QV-Proposed-FB_H1-H2.TEXT.H2-1_SM-dM": true, + "QV-Proposed-FB_H1-H2.TEXT.H2-1_SM-body": true, + "QV-Proposed-FB_H1-H2.TEXT.H2-1_SM-div": true, + "QV-Proposed-H_H1-1_SC-dM": true, + "QV-Proposed-H_H1-1_SC-body": true, + "QV-Proposed-H_H1-1_SC-div": true, + "QV-Proposed-H_H3-1_SC-dM": true, + "QV-Proposed-H_H3-1_SC-body": true, + "QV-Proposed-H_H3-1_SC-div": true, + "QV-Proposed-H_H1-H2-H3-H4-1_SC-dM": true, + "QV-Proposed-H_H1-H2-H3-H4-1_SC-body": true, + "QV-Proposed-H_H1-H2-H3-H4-1_SC-div": true, + "QV-Proposed-H_P-1_SC-dM": true, + "QV-Proposed-H_P-1_SC-body": true, + "QV-Proposed-H_P-1_SC-div": true, + "QV-Proposed-FN_FONTf:a-1_SI-dM": true, + "QV-Proposed-FN_FONTf:a-1_SI-body": true, + "QV-Proposed-FN_FONTf:a-1_SI-div": true, + "QV-Proposed-FN_SPANs:ff:a-1_SI-dM": true, + "QV-Proposed-FN_SPANs:ff:a-1_SI-body": true, + "QV-Proposed-FN_SPANs:ff:a-1_SI-div": true, + "QV-Proposed-FN_FONTf:a.s:ff:c-1_SI-dM": true, + "QV-Proposed-FN_FONTf:a.s:ff:c-1_SI-body": true, + "QV-Proposed-FN_FONTf:a.s:ff:c-1_SI-div": true, + "QV-Proposed-FN_FONTf:a-FONTf:c-1_SI-dM": true, + "QV-Proposed-FN_FONTf:a-FONTf:c-1_SI-body": true, + "QV-Proposed-FN_FONTf:a-FONTf:c-1_SI-div": true, + "QV-Proposed-FN_SPANs:ff:c-FONTf:a-1_SI-dM": true, + "QV-Proposed-FN_SPANs:ff:c-FONTf:a-1_SI-body": true, + "QV-Proposed-FN_SPANs:ff:c-FONTf:a-1_SI-div": true, + "QV-Proposed-FN_SPAN.fs18px-1_SI-dM": true, + "QV-Proposed-FN_SPAN.fs18px-1_SI-body": true, + "QV-Proposed-FN_SPAN.fs18px-1_SI-div": true, + "QV-Proposed-FN_MYCOURIER-1-SI-dM": true, + "QV-Proposed-FN_MYCOURIER-1-SI-body": true, + "QV-Proposed-FN_MYCOURIER-1-SI-div": true, + "QV-Proposed-FS_FONTsz:4-1_SI-dM": true, + "QV-Proposed-FS_FONTsz:4-1_SI-body": true, + "QV-Proposed-FS_FONTsz:4-1_SI-div": true, + "QV-Proposed-FS_FONTs:fs:l-1_SI-dM": true, + "QV-Proposed-FS_FONTs:fs:l-1_SI-body": true, + "QV-Proposed-FS_FONTs:fs:l-1_SI-div": true, + "QV-Proposed-FS_FONT.ass.s:fs:l-1_SI-dM": true, + "QV-Proposed-FS_FONT.ass.s:fs:l-1_SI-body": true, + "QV-Proposed-FS_FONT.ass.s:fs:l-1_SI-div": true, + "QV-Proposed-FS_FONTsz:1.s:fs:xl-1_SI-dM": true, + "QV-Proposed-FS_FONTsz:1.s:fs:xl-1_SI-body": true, + "QV-Proposed-FS_FONTsz:1.s:fs:xl-1_SI-div": true, + "QV-Proposed-FS_SPAN.large-1_SI-dM": true, + "QV-Proposed-FS_SPAN.large-1_SI-body": true, + "QV-Proposed-FS_SPAN.large-1_SI-div": true, + "QV-Proposed-FS_SPAN.fs18px-1_SI-dM": true, + "QV-Proposed-FS_SPAN.fs18px-1_SI-body": true, + "QV-Proposed-FS_SPAN.fs18px-1_SI-div": true, + "QV-Proposed-FA_MYLARGE-1-SI-dM": true, + "QV-Proposed-FA_MYLARGE-1-SI-body": true, + "QV-Proposed-FA_MYLARGE-1-SI-div": true, + "QV-Proposed-FA_MYFS18PX-1-SI-dM": true, + "QV-Proposed-FA_MYFS18PX-1-SI-body": true, + "QV-Proposed-FA_MYFS18PX-1-SI-div": true, + "QV-Proposed-BC_FONTs:bc:fca-1_SI-dM": true, + "QV-Proposed-BC_FONTs:bc:fca-1_SI-body": true, + "QV-Proposed-BC_FONTs:bc:fca-1_SI-div": true, + "QV-Proposed-BC_SPANs:bc:abc-1_SI-dM": true, + "QV-Proposed-BC_SPANs:bc:abc-1_SI-body": true, + "QV-Proposed-BC_SPANs:bc:abc-1_SI-div": true, + "QV-Proposed-BC_FONTs:bc:084-SPAN-1_SI-dM": true, + "QV-Proposed-BC_FONTs:bc:084-SPAN-1_SI-body": true, + "QV-Proposed-BC_FONTs:bc:084-SPAN-1_SI-div": true, + "QV-Proposed-BC_SPANs:bc:cde-SPAN-1_SI-dM": true, + "QV-Proposed-BC_SPANs:bc:cde-SPAN-1_SI-body": true, + "QV-Proposed-BC_SPANs:bc:cde-SPAN-1_SI-div": true, + "QV-Proposed-BC_SPAN.ass.s:bc:rgb-1_SI-dM": true, + "QV-Proposed-BC_SPAN.ass.s:bc:rgb-1_SI-body": true, + "QV-Proposed-BC_SPAN.ass.s:bc:rgb-1_SI-div": true, + "QV-Proposed-BC_SPAN.bcred-1_SI-dM": true, + "QV-Proposed-BC_SPAN.bcred-1_SI-body": true, + "QV-Proposed-BC_SPAN.bcred-1_SI-div": true, + "QV-Proposed-BC_MYBCRED-1-SI-dM": true, + "QV-Proposed-BC_MYBCRED-1-SI-body": true, + "QV-Proposed-BC_MYBCRED-1-SI-div": true, + "QV-Proposed-FC_FONTc:f00-1_SI-dM": true, + "QV-Proposed-FC_FONTc:f00-1_SI-body": true, + "QV-Proposed-FC_FONTc:f00-1_SI-div": true, + "QV-Proposed-FC_SPANs:c:0f0-1_SI-dM": true, + "QV-Proposed-FC_SPANs:c:0f0-1_SI-body": true, + "QV-Proposed-FC_SPANs:c:0f0-1_SI-div": true, + "QV-Proposed-FC_FONTc:333.s:c:999-1_SI-dM": true, + "QV-Proposed-FC_FONTc:333.s:c:999-1_SI-body": true, + "QV-Proposed-FC_FONTc:333.s:c:999-1_SI-div": true, + "QV-Proposed-FC_FONTc:641-SPAN-1_SI-dM": true, + "QV-Proposed-FC_FONTc:641-SPAN-1_SI-body": true, + "QV-Proposed-FC_FONTc:641-SPAN-1_SI-div": true, + "QV-Proposed-FC_SPANs:c:d95-SPAN-1_SI-dM": true, + "QV-Proposed-FC_SPANs:c:d95-SPAN-1_SI-body": true, + "QV-Proposed-FC_SPANs:c:d95-SPAN-1_SI-div": true, + "QV-Proposed-FC_SPAN.red-1_SI-dM": true, + "QV-Proposed-FC_SPAN.red-1_SI-body": true, + "QV-Proposed-FC_SPAN.red-1_SI-div": true, + "QV-Proposed-FC_MYRED-1-SI-dM": true, + "QV-Proposed-FC_MYRED-1-SI-body": true, + "QV-Proposed-FC_MYRED-1-SI-div": true, + "QV-Proposed-HC_FONTs:bc:fc0-1_SI-dM": true, + "QV-Proposed-HC_FONTs:bc:fc0-1_SI-body": true, + "QV-Proposed-HC_FONTs:bc:fc0-1_SI-div": true, + "QV-Proposed-HC_SPANs:bc:a0c-1_SI-dM": true, + "QV-Proposed-HC_SPANs:bc:a0c-1_SI-body": true, + "QV-Proposed-HC_SPANs:bc:a0c-1_SI-div": true, + "QV-Proposed-HC_SPAN.ass.s:bc:rgb-1_SI-dM": true, + "QV-Proposed-HC_SPAN.ass.s:bc:rgb-1_SI-body": true, + "QV-Proposed-HC_SPAN.ass.s:bc:rgb-1_SI-div": true, + "QV-Proposed-HC_FONTs:bc:83e-SPAN-1_SI-dM": true, + "QV-Proposed-HC_FONTs:bc:83e-SPAN-1_SI-body": true, + "QV-Proposed-HC_FONTs:bc:83e-SPAN-1_SI-div": true, + "QV-Proposed-HC_SPANs:bc:b12-SPAN-1_SI-dM": true, + "QV-Proposed-HC_SPANs:bc:b12-SPAN-1_SI-body": true, + "QV-Proposed-HC_SPANs:bc:b12-SPAN-1_SI-div": true, + "QV-Proposed-HC_SPAN.bcred-1_SI-dM": true, + "QV-Proposed-HC_SPAN.bcred-1_SI-body": true, + "QV-Proposed-HC_SPAN.bcred-1_SI-div": true, + "QV-Proposed-HC_MYBCRED-1-SI-dM": true, + "QV-Proposed-HC_MYBCRED-1-SI-body": true, + "QV-Proposed-HC_MYBCRED-1-SI-div": true, + }, +}; diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/current_revision b/editor/libeditor/tests/browserscope/lib/richtext2/current_revision new file mode 100644 index 0000000000..cc34bb3975 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/current_revision @@ -0,0 +1 @@ +805 diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/platformFailures.js b/editor/libeditor/tests/browserscope/lib/richtext2/platformFailures.js new file mode 100644 index 0000000000..61e1dad671 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/platformFailures.js @@ -0,0 +1,28 @@ +/** + * Platform-specific failures not included in the main currentStatus.js list. + */ +var platformFailures; +if (navigator.appVersion.includes("Android")) { + platformFailures = { + "value": {}, + "select": { + "S-Proposed-SM:m.f.w_TEXT-th_SC-1-dM": true, + "S-Proposed-SM:m.f.w_TEXT-th_SC-1-body": true, + "S-Proposed-SM:m.f.w_TEXT-th_SC-1-div": true, + "S-Proposed-SM:m.f.w_TEXT-th_SC-2-dM": true, + "S-Proposed-SM:m.f.w_TEXT-th_SC-2-body": true, + "S-Proposed-SM:m.f.w_TEXT-th_SC-2-div": true, + "S-Proposed-SM:m.b.w_TEXT-th_SC-1-dM": true, + "S-Proposed-SM:m.b.w_TEXT-th_SC-1-body": true, + "S-Proposed-SM:m.b.w_TEXT-th_SC-1-div": true, + "S-Proposed-SM:m.b.w_TEXT-th_SC-2-dM": true, + "S-Proposed-SM:m.b.w_TEXT-th_SC-2-body": true, + "S-Proposed-SM:m.b.w_TEXT-th_SC-2-div": true + } + } +} else { + platformFailures = { + "value": {}, + "select": {} + } +} diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/__init__.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/__init__.py diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/common.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/common.py new file mode 100644 index 0000000000..345f9bbb00 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/common.py @@ -0,0 +1,25 @@ +#!/usr/bin/python2.5 +# +# Copyright 2010 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the 'License') +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Common constants""" + +__author__ = 'rolandsteiner@google.com (Roland Steiner)' + +CATEGORY = 'richtext2' + +TEST_ID_PREFIX = 'RTE2' + +CLASSES = ['Finalized', 'RFC', 'Proposed']
\ No newline at end of file diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/handlers.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/handlers.py new file mode 100644 index 0000000000..2ee1e79ad3 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/handlers.py @@ -0,0 +1,107 @@ +#!/usr/bin/python2.5 +# +# Copyright 2010 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the 'License') +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Handlers for New Rich Text Tests""" + +__author__ = 'rolandsteiner@google.com (Roland Steiner)' + +from google.appengine.api import users +from google.appengine.ext import db +from google.appengine.api import memcache +from google.appengine.ext import webapp +from google.appengine.ext.webapp import template + +import django +from django import http +from django import shortcuts + +from django.template import add_to_builtins +add_to_builtins('base.custom_filters') + +# Shared stuff +from categories import all_test_sets +from base import decorators +from base import util + +# common to the RichText2 suite +from categories.richtext2 import common + +# tests +from categories.richtext2.tests.apply import APPLY_TESTS +from categories.richtext2.tests.applyCSS import APPLY_TESTS_CSS +from categories.richtext2.tests.change import CHANGE_TESTS +from categories.richtext2.tests.changeCSS import CHANGE_TESTS_CSS +from categories.richtext2.tests.delete import DELETE_TESTS +from categories.richtext2.tests.forwarddelete import FORWARDDELETE_TESTS +from categories.richtext2.tests.insert import INSERT_TESTS +from categories.richtext2.tests.selection import SELECTION_TESTS +from categories.richtext2.tests.unapply import UNAPPLY_TESTS +from categories.richtext2.tests.unapplyCSS import UNAPPLY_TESTS_CSS + +from categories.richtext2.tests.querySupported import QUERYSUPPORTED_TESTS +from categories.richtext2.tests.queryEnabled import QUERYENABLED_TESTS +from categories.richtext2.tests.queryIndeterm import QUERYINDETERM_TESTS +from categories.richtext2.tests.queryState import QUERYSTATE_TESTS, QUERYSTATE_TESTS_CSS +from categories.richtext2.tests.queryValue import QUERYVALUE_TESTS, QUERYVALUE_TESTS_CSS + + +def About(request): + """About page.""" + overview = """These tests cover browers' implementations of + <a href="http://blog.whatwg.org/the-road-to-html-5-contenteditable">contenteditable</a> + for basic rich text formatting commands. Most browser implementations do very + well at editing the HTML which is generated by their own execCommands. But a + big problem happens when developers try to make cross-browser web + applications using contenteditable - most browsers are not able to correctly + change formatting generated by other browsers. On top of that, most browsers + allow users to to paste arbitrary HTML from other webpages into a + contenteditable region, which is even harder for browsers to properly + format. These tests check how well the execCommand, queryCommandState, + and queryCommandValue functions work with different types of HTML.""" + return util.About(request, common.CATEGORY, category_title='Rich Text', + overview=overview, show_hidden=False) + + +def RunRichText2Tests(request): + params = { + 'classes': common.CLASSES, + 'commonIDPrefix': common.TEST_ID_PREFIX, + 'strict': False, + 'suites': [ + SELECTION_TESTS, + APPLY_TESTS, + APPLY_TESTS_CSS, + CHANGE_TESTS, + CHANGE_TESTS_CSS, + UNAPPLY_TESTS, + UNAPPLY_TESTS_CSS, + DELETE_TESTS, + FORWARDDELETE_TESTS, + INSERT_TESTS, + + QUERYSUPPORTED_TESTS, + QUERYENABLED_TESTS, + QUERYINDETERM_TESTS, + QUERYSTATE_TESTS, + QUERYSTATE_TESTS_CSS, + QUERYVALUE_TESTS, + QUERYVALUE_TESTS_CSS + ] + } + return shortcuts.render_to_response('%s/templates/richtext2.html' % common.CATEGORY, params) + + + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/common.css b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/common.css new file mode 100644 index 0000000000..77c6bb8726 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/common.css @@ -0,0 +1,116 @@ +.framed { + vertical-align: top; + margin: 8px; + border: 1px solid black; +} + +.legend { + padding: 12px; + background-color: #f8f8ff; +} + +.legendHdr { + font-size: large; + text-decoration: underline; +} + +table.legend { + display: inline-table; +} + +.suite-thead { + text-align: left; +} + +.lo { + background-color: #dddddd; +} +.hi { + background-color: #eeeeee; +} + +.lo .grey { + background-color: #dddddd; +} +.lo .na { + background-color: #dddddd; +} +.lo .pass { + background-color: #d4ffc0; +} +.lo .canary { + background-color: #ffcccc; +} +.lo .fail { + background-color: #ffcccc; +} +.lo .accept { + background-color: #ffffc0; +} +.lo .exception { + background-color: #f0d0f4; +} +.lo .unsupported { + background-color: #f0d0f4; +} + +.hi .grey { + background-color: #eeeeee; +} +.hi .na { + background-color: #eeeeee; +} +.hi .pass { + background-color: #e0ffdc; +} +.hi .canary { + background-color: #ffd8d8; +} +.hi .fail { + background-color: #ffd8d8; +} +.hi .accept { + background-color: #ffffd8; +} +.hi .exception { + background-color: #f4dcf8; +} +.hi .unsupported { + background-color: #f4dcf8; +} + + +.sel { + color: blue; +} + +.txt { + padding: 1px; + margin: 1px; + border: 1px solid #b0b0b0; +} + +.idLabel { + font-size: small; +} + +.fade { + color: grey; +} +.accexp { + color: #606070; +} +.comment { + color: grey; +} + +.score { + color: #666666; +} + +.fatalerror { + color: red; + font-size: large; + font-weight: bold; +} + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-body.html b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-body.html new file mode 100644 index 0000000000..a254adc03e --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-body.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html> +<head> + <meta http-equiv="content-type" content="text/html; charset=utf-8" /> + <meta http-equiv="X-UA-Compatible" content="IE=edge" /> + + <link rel="stylesheet" href="editable.css" type="text/css"> +</head> +<body contentEditable="true"> +</body> +</html> diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-dM.html b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-dM.html new file mode 100644 index 0000000000..e16de3ab9f --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-dM.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> +<head> + <meta http-equiv="content-type" content="text/html; charset=utf-8" /> + <meta http-equiv="X-UA-Compatible" content="IE=edge" /> + + <link rel="stylesheet" href="editable.css" type="text/css"> + + <script> + function setDesignMode() { + window.document.designMode = "On"; + } + </script> +</head> +<body onload="setDesignMode()"> +</body> +</html> diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-div.html b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-div.html new file mode 100644 index 0000000000..7dd600dbd8 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable-div.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html> +<head> + <meta http-equiv="content-type" content="text/html; charset=utf-8" /> + <meta http-equiv="X-UA-Compatible" content="IE=edge" /> + + <link rel="stylesheet" href="editable.css" type="text/css"> +</head> +<body> +</body> +</html> diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable.css b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable.css new file mode 100644 index 0000000000..99fec49506 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/editable.css @@ -0,0 +1,66 @@ +.b, myb { + font-weight: bold; +} + +.i, myi { + font-style: italic; +} + +.s, mys { + text-decoration: line-through; +} + +.u, myu { + text-decoration: underline; +} + +.sub, mysub { + vertical-align: sub; +} + +.sup, mysup { + vertical-align: super; +} + +.jc, myjc { + text-align: center; +} + +.jf, myjf { + text-align: justify; +} + +.jl, myjl { + text-align: left; +} + +.jr, myjr { + text-align: right; +} + +.red, myred { + color: red; +} + +.bcred, mybcred { + background-color: red; +} + +.large, mylarge { + font-size: large; +} + +.fs18px, myfs18px { + font-size: 18px; +} + +.courier, mycourier { + font-family: courier; +} + +gen::before { + content: "[BEFORE]"; +} +gen::after { + content: "[AFTER]"; +} diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/canonicalize.js b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/canonicalize.js new file mode 100644 index 0000000000..2236d9dfc5 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/canonicalize.js @@ -0,0 +1,436 @@ +/** + * @fileoverview + * Canonicalization functions used in the RTE test suite. + * + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the 'License') + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @version 0.1 + * @author rolandsteiner@google.com + */ + +/** + * Canonicalize HTML entities to their actual character + * + * @param str {String} the HTML string to be canonicalized + * @return {String} the canonicalized string + */ + +function canonicalizeEntities(str) { + // TODO(rolandsteiner): this function is very much not optimized, but that shouldn't + // theoretically matter too much - look into it at some point. + var match; + while (match = str.match(/&#x([0-9A-F]+);/i)) { + str = str.replace('&#x' + match[1] + ';', String.fromCharCode(parseInt(match[1], 16))); + } + while (match = str.match(/&#([0-9]+);/)) { + str = str.replace('&#' + match[1] + ';', String.fromCharCode(Number(match[1]))); + } + return str; +} + +/** + * Canonicalize the contents of the HTML 'style' attribute. + * I.e. sorts the CSS attributes alphabetically and canonicalizes the values + * CSS attributes where necessary. + * + * If this would return an empty string, return null instead to suppress the + * whole 'style' attribute. + * + * Avoid tests that contain {, } or : within CSS values! + * + * Note that this function relies on the spaces of the input string already + * having been normalized by canonicalizeSpaces! + * + * FIXME: does not canonicalize the contents of compound attributes + * (e.g., 'border'). + * + * @param str {String} contents of the 'style' attribute + * @param emitFlags {Object} flags used for this output + * @return {String/null} canonicalized string, null instead of the empty string + */ +function canonicalizeStyle(str, emitFlags) { + // Remove any enclosing curly brackets + str = str.replace(/ ?[\{\}] ?/g, ''); + + var attributes = str.split(';'); + var count = attributes.length; + var resultArr = []; + + for (var a = 0; a < count; ++a) { + // Retrieve "name: value" pair + // Note: may expectedly fail if the last pair was terminated with ';' + var avPair = attributes[a].match(/ ?([^ :]+) ?: ?(.+)/); + if (!avPair) + continue; + + var name = avPair[1]; + var value = avPair[2].replace(/ $/, ''); // Remove any trailing space. + + switch (name) { + case 'color': + case 'background-color': + case 'border-color': + if (emitFlags.canonicalizeUnits) { + resultArr.push(name + ': #' + new Color(value).toHexString()); + } else { + resultArr.push(name + ': ' + value); + } + break; + + case 'font-family': + if (emitFlags.canonicalizeUnits) { + resultArr.push(name + ': ' + new FontName(value).toString()); + } else { + resultArr.push(name + ': ' + value); + } + break; + + case 'font-size': + if (emitFlags.canonicalizeUnits) { + resultArr.push(name + ': ' + new FontSize(value).toString()); + } else { + resultArr.push(name + ': ' + value); + } + break; + + default: + resultArr.push(name + ': ' + value); + } + } + + // Sort by name, assuming no duplicate CSS attribute names. + resultArr.sort(); + + return resultArr.join('; ') || null; +} + +/** + * Canonicalize a single attribute value. + * + * Note that this function relies on the spaces of the input string already + * having been normalized by canonicalizeSpaces! + * + * @param elemName {String} the name of the element + * @param attrName {String} the name of the attribute + * @param attrValue {String} the value of the attribute + * @param emitFlags {Object} flags used for this output + * @return {String/null} the canonicalized value, or null if the attribute should be skipped. + */ +function canonicalizeSingleAttribute(elemName, attrName, attrValue, emitFlags) { + // We emit attributes as name="value", so change any contained apostrophes + // to quote marks. + attrValue = attrValue.replace(/\x22/, '\x27'); + + switch (attrName) { + case 'class': + return emitFlags.emitClass ? attrValue : null; + + case 'id': + if (!emitFlags.emitID) { + return null; + } + if (attrValue && attrValue.substr(0, 7) == 'editor-') { + return null; + } + return attrValue; + + // Remove empty style attributes, canonicalize the contents otherwise, + // provided the test cares for styles. + case 'style': + return (emitFlags.emitStyle && attrValue) + ? canonicalizeStyle(attrValue, emitFlags) + : null; + + // Never output onload handlers as they are set by the test environment. + case 'onload': + return null; + + // Canonicalize colors. + case 'bgcolor': + case 'color': + if (!attrValue) { + return null; + } + return emitFlags.canonicalizeUnits ? new Color(attrValue).toString() : attrValue; + + // Canonicalize font names. + case 'face': + return emitFlags.canonicalizeUnits ? new FontName(attrValue).toString() : attrValue; + + // Canonicalize font sizes (leave other 'size' attributes as-is). + case 'size': + if (!attrValue) { + return null; + } + switch (elemName) { + case 'basefont': + case 'font': + return emitFlags.canonicalizeUnits ? new FontSize(attrValue).toString() : attrValue; + } + return attrValue; + + // Remove spans with value 1. Retain spans with other values, even if + // empty or with a value 0, since those indicate a flawed implementation. + case 'colspan': + case 'rowspan': + case 'span': + return (attrValue == '1' || attrValue === '') ? null : attrValue; + + // Boolean attributes: presence equals true. If present, the value must be + // the empty string or the attribute's canonical name. + // (http://www.whatwg.org/specs/web-apps/current-work/#boolean-attributes) + // Below we only normalize empty string to the canonical name for + // comparison purposes. All other values are not touched and will therefore + // in all likelihood result in a failed test (even if they may be accepted + // by the UA). + case 'async': + case 'autofocus': + case 'checked': + case 'compact': + case 'declare': + case 'defer': + case 'disabled': + case 'formnovalidate': + case 'frameborder': + case 'ismap': + case 'loop': + case 'multiple': + case 'nohref': + case 'nosize': + case 'noshade': + case 'novalidate': + case 'nowrap': + case 'open': + case 'readonly': + case 'required': + case 'reversed': + case 'seamless': + case 'selected': + return attrValue ? attrValue : attrName; + + default: + return attrValue; + } +} + +/** + * Canonicalize the contents of an element tag. + * + * I.e. sorts the attributes alphabetically and canonicalizes their + * values where necessary. Also removes attributes we're not interested in. + * + * Note that this function relies on the spaces of the input string already + * having been normalized by canonicalizeSpaces! + * + * @param str {String} the contens of the element tag, excluding < and >. + * @param emitFlags {Object} flags used for this output + * @return {String} the canonicalized contents. + */ +function canonicalizeElementTag(str, emitFlags) { + // FIXME: lowercase only if emitFlags.lowercase is set + str = str.toLowerCase(); + + var pos = str.search(' '); + + // element name only + if (pos == -1) { + return str; + } + + var elemName = str.substr(0, pos); + str = str.substr(pos + 1); + + // Even if emitFlags.emitAttrs is not set, we must iterate over the + // attributes to catch the special selection attribute and/or selection + // markers. :( + + // Iterate over attributes, add them to an array, canonicalize their + // contents, and finally output the (remaining) attributes in sorted order. + // Note: We can't do a simple split on space here, because the value of, + // e.g., 'style' attributes may also contain spaces. + var attrs = []; + var selStartInTag = false; + var selEndInTag = false; + + while (str) { + var attrName; + var attrValue = ''; + + pos = str.search(/[ =]/); + if (pos >= 0) { + attrName = str.substr(0, pos); + if (str.charAt(pos) == ' ') { + ++pos; + } + if (str.charAt(pos) == '=') { + ++pos; + if (str.charAt(pos) == ' ') { + ++pos; + } + str = str.substr(pos); + switch (str.charAt(0)) { + case '"': + case "'": + pos = str.indexOf(str.charAt(0), 1); + pos = (pos < 0) ? str.length : pos; + attrValue = str.substring(1, pos); + ++pos; + break; + + default: + pos = str.indexOf(' ', 0); + pos = (pos < 0) ? str.length : pos; + attrValue = (pos == -1) ? str : str.substr(0, pos); + break; + } + attrValue = attrValue.replace(/^ /, ''); + attrValue = attrValue.replace(/ $/, ''); + } + } else { + attrName = str; + } + str = (pos == -1 || pos >= str.length) ? '' : str.substr(pos + 1); + + // Remove special selection attributes. + switch (attrName) { + case ATTRNAME_SEL_START: + selStartInTag = true; + continue; + + case ATTRNAME_SEL_END: + selEndInTag = true; + continue; + } + + switch (attrName) { + case '': + case 'onload': + case 'xmlns': + break; + + default: + if (!emitFlags.emitAttrs) { + break; + } + // >>> fall through >>> + + case 'contenteditable': + attrValue = canonicalizeEntities(attrValue); + attrValue = canonicalizeSingleAttribute(elemName, attrName, attrValue, emitFlags); + if (attrValue !== null) { + attrs.push(attrName + '="' + attrValue + '"'); + } + } + } + + var result = elemName; + + // Sort alphabetically (on full string rather than just attribute value for + // simplicity. Also, attribute names will differ when encountering the '='). + if (attrs.length > 0) { + attrs.sort(); + result += ' ' + attrs.join(' '); + } + + // Add intra-tag selection marker(s) or attribute(s), if any, at the end. + if (selStartInTag && selEndInTag) { + result += ' |'; + } else if (selStartInTag) { + result += ' {'; + } else if (selEndInTag) { + result += ' }'; + } + + return result; +} + +/** + * Canonicalize elements and attributes to facilitate comparison to the + * expectation string: sort attributes, canonicalize values and remove chaff. + * + * Note that this function relies on the spaces of the input string already + * having been normalized by canonicalizeSpaces! + * + * @param str {String} the HTML string to be canonicalized + * @param emitFlags {Object} flags used for this output + * @return {String} the canonicalized string + */ +function canonicalizeElementsAndAttributes(str, emitFlags) { + var tagStart = str.indexOf('<'); + var tagEnd = 0; + var result = ''; + + while (tagStart >= 0) { + ++tagStart; + if (str.charAt(tagStart) == '/') { + ++tagStart; + } + result = result + canonicalizeEntities(str.substring(tagEnd, tagStart)); + tagEnd = str.indexOf('>', tagStart); + if (tagEnd < 0) { + tagEnd = str.length - 1; + } + if (str.charAt(tagEnd - 1) == '/') { + --tagEnd; + } + var elemStr = str.substring(tagStart, tagEnd); + elemStr = canonicalizeElementTag(elemStr, emitFlags); + result = result + elemStr; + tagStart = str.indexOf('<', tagEnd); + } + return result + canonicalizeEntities(str.substring(tagEnd)); +} + +/** + * Canonicalize an innerHTML string to uniform single whitespaces. + * + * FIXME: running this prevents testing for pre-formatted content + * and the CSS 'white-space' attribute. + * + * @param str {String} the HTML string to be canonicalized + * @return {String} the canonicalized string + */ +function canonicalizeSpaces(str) { + // Collapse sequential whitespace. + str = str.replace(/\s+/g, ' '); + + // Remove spaces immediately inside angle brackets <, >, </ and />. + // While doing this also canonicalize <.../> to <...>. + str = str.replace(/\< ?/g, '<'); + str = str.replace(/\<\/ ?/g, '</'); + str = str.replace(/ ?\/?\>/g, '>'); + + return str; +} + +/** + * Canonicalize an innerHTML string to uniform single whitespaces. + * Also remove comments to retain only embedded selection markers, and + * remove </br> and </hr> if present. + * + * FIXME: running this prevents testing for pre-formatted content + * and the CSS 'white-space' attribute. + * + * @param str {String} the HTML string to be canonicalized + * @return {String} the canonicalized string + */ +function initialCanonicalizationOf(str) { + str = canonicalizeSpaces(str); + str = str.replace(/ ?<!-- ?/g, ''); + str = str.replace(/ ?--> ?/g, ''); + str = str.replace(/<\/[bh]r>/g, ''); + + return str; +} diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/compare.js b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/compare.js new file mode 100644 index 0000000000..be059cfc86 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/compare.js @@ -0,0 +1,489 @@ +/** + * @fileoverview + * Comparison functions used in the RTE test suite. + * + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the 'License') + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @version 0.1 + * @author rolandsteiner@google.com + */ + +/** + * constants used only in the compare functions. + */ +var RESULT_DIFF = 0; // actual result doesn't match expectation +var RESULT_SEL = 1; // actual result matches expectation in HTML only +var RESULT_EQUAL = 2; // actual result matches expectation in both HTML and selection + +/** + * Gets the test expectations as an array from the passed-in field. + * + * @param {Array|String} the test expectation(s) as string or array. + * @return {Array} test expectations as an array. + */ +function getExpectationArray(expected) { + if (expected === undefined) { + return []; + } + if (expected === null) { + return [null]; + } + switch (typeof expected) { + case 'string': + case 'boolean': + case 'number': + return [expected]; + } + // Assume it's already an array. + return expected; +} + +/** + * Compare a test result to a single expectation string. + * + * FIXME: add support for optional elements/attributes. + * + * @param expected {String} the already canonicalized (with the exception of selection marks) expectation string + * @param actual {String} the already canonicalized (with the exception of selection marks) actual result + * @return {Integer} one of the RESULT_... return values + * @see variables.js for return values + */ +function compareHTMLToSingleExpectation(expected, actual) { + // If the test checks the selection, then the actual string must match the + // expectation exactly. + if (expected == actual) { + return RESULT_EQUAL; + } + + // Remove selection markers and see if strings match then. + expected = expected.replace(/ [{}\|]>/g, '>'); // intra-tag + expected = expected.replace(/[\[\]\^{}\|]/g, ''); // outside tag + actual = actual.replace(/ [{}\|]>/g, '>'); // intra-tag + actual = actual.replace(/[\[\]\^{}\|]/g, ''); // outside tag + + return (expected == actual) ? RESULT_SEL : RESULT_DIFF; +} + +/** + * Compare the current HTMLtest result to the expectation string(s). + * + * @param actual {String/Boolean} actual value + * @param expected {String/Array} expectation(s) + * @param emitFlags {Object} flags to use for canonicalization + * @return {Integer} one of the RESULT_... return values + * @see variables.js for return values + */ +function compareHTMLToExpectation(actual, expected, emitFlags) { + // Find the most favorable result among the possible expectation strings. + var expectedArr = getExpectationArray(expected); + var count = expectedArr ? expectedArr.length : 0; + var best = RESULT_DIFF; + + for (var idx = 0; idx < count && best < RESULT_EQUAL; ++idx) { + var expected = expectedArr[idx]; + expected = canonicalizeSpaces(expected); + expected = canonicalizeElementsAndAttributes(expected, emitFlags); + + var singleResult = compareHTMLToSingleExpectation(expected, actual); + + best = Math.max(best, singleResult); + } + return best; +} + +/** + * Compare the current HTMLtest result to expected and acceptable results + * + * @param expected {String/Array} expected result(s) + * @param accepted {String/Array} accepted result(s) + * @param actual {String} actual result + * @param emitFlags {Object} how to canonicalize the HTML strings + * @param result {Object} [out] object recieving the result of the comparison. + */ +function compareHTMLTestResultTo(expected, accepted, actual, emitFlags, result) { + actual = actual.replace(/[\x60\xb4]/g, ''); + actual = canonicalizeElementsAndAttributes(actual, emitFlags); + + var bestExpected = compareHTMLToExpectation(actual, expected, emitFlags); + + if (bestExpected == RESULT_EQUAL) { + // Shortcut - it doesn't get any better + result.valresult = VALRESULT_EQUAL; + result.selresult = SELRESULT_EQUAL; + return; + } + + var bestAccepted = compareHTMLToExpectation(actual, accepted, emitFlags); + + switch (bestExpected) { + case RESULT_SEL: + switch (bestAccepted) { + case RESULT_EQUAL: + // The HTML was equal to the/an expected HTML result as well + // (just not the selection there), therefore the difference + // between expected and accepted can only lie in the selection. + result.valresult = VALRESULT_EQUAL; + result.selresult = SELRESULT_ACCEPT; + return; + + case RESULT_SEL: + case RESULT_DIFF: + // The acceptable expectations did not yield a better result + // -> stay with the original (i.e., comparison to 'expected') result. + result.valresult = VALRESULT_EQUAL; + result.selresult = SELRESULT_DIFF; + return; + } + break; + + case RESULT_DIFF: + switch (bestAccepted) { + case RESULT_EQUAL: + result.valresult = VALRESULT_ACCEPT; + result.selresult = SELRESULT_EQUAL; + return; + + case RESULT_SEL: + result.valresult = VALRESULT_ACCEPT; + result.selresult = SELRESULT_DIFF; + return; + + case RESULT_DIFF: + result.valresult = VALRESULT_DIFF; + result.selresult = SELRESULT_NA; + return; + } + break; + } + + throw INTERNAL_ERR + HTML_COMPARISON; +} + +/** + * Verify that the canaries are unviolated. + * + * @param container {Object} the test container descriptor as object reference + * @param result {Object} object reference that contains the result data + * @return {Boolean} whether the canaries' HTML is OK (selection flagged, but not fatal) + */ +function verifyCanaries(container, result) { + if (!container.canary) { + return true; + } + + var str = canonicalizeElementsAndAttributes(result.bodyInnerHTML, emitFlagsForCanary); + + if (str.length < 2 * container.canary.length) { + result.valresult = VALRESULT_CANARY; + result.selresult = SELRESULT_NA; + result.output = result.bodyOuterHTML; + return false; + } + + var strBefore = str.substr(0, container.canary.length); + var strAfter = str.substr(str.length - container.canary.length); + + // Verify that the canary stretch doesn't contain any selection markers + if (SELECTION_MARKERS.test(strBefore) || SELECTION_MARKERS.test(strAfter)) { + str = str.replace(SELECTION_MARKERS, ''); + if (str.length < 2 * container.canary.length) { + result.valresult = VALRESULT_CANARY; + result.selresult = SELRESULT_NA; + result.output = result.bodyOuterHTML; + return false; + } + + // Selection escaped contentEditable element, but HTML may still be ok. + result.selresult = SELRESULT_CANARY; + strBefore = str.substr(0, container.canary.length); + strAfter = str.substr(str.length - container.canary.length); + } + + if (strBefore !== container.canary || strAfter !== container.canary) { + result.valresult = VALRESULT_CANARY; + result.selresult = SELRESULT_NA; + result.output = result.bodyOuterHTML; + return false; + } + + return true; +} + +/** + * Compare the current HTMLtest result to the expectation string(s). + * Sets the global result variables. + * + * @param suite {Object} the test suite as object reference + * @param group {Object} group of tests within the suite the test belongs to + * @param test {Object} the test as object reference + * @param container {Object} the test container description + * @param result {Object} [in/out] the result description, incl. HTML strings + * @see variables.js for result values + */ +function compareHTMLTestResult(suite, group, test, container, result) { + if (!verifyCanaries(container, result)) { + return; + } + + var emitFlags = { + emitAttrs: getTestParameter(suite, group, test, PARAM_CHECK_ATTRIBUTES), + emitStyle: getTestParameter(suite, group, test, PARAM_CHECK_STYLE), + emitClass: getTestParameter(suite, group, test, PARAM_CHECK_CLASS), + emitID: getTestParameter(suite, group, test, PARAM_CHECK_ID), + lowercase: true, + canonicalizeUnits: true + }; + + // 2a.) Compare opening tag - + // decide whether to compare vs. outer or inner HTML based on this. + var openingTagEnd = result.outerHTML.indexOf('>') + 1; + var openingTag = result.outerHTML.substr(0, openingTagEnd); + + openingTag = canonicalizeElementsAndAttributes(openingTag, emitFlags); + var tagCmp = compareHTMLToExpectation(openingTag, container.tagOpen, emitFlags); + + if (tagCmp == RESULT_EQUAL) { + result.output = result.innerHTML; + compareHTMLTestResultTo( + getTestParameter(suite, group, test, PARAM_EXPECTED), + getTestParameter(suite, group, test, PARAM_ACCEPT), + result.innerHTML, + emitFlags, + result) + } else { + result.output = result.outerHTML; + compareHTMLTestResultTo( + getContainerParameter(suite, group, test, container, PARAM_EXPECTED_OUTER), + getContainerParameter(suite, group, test, container, PARAM_ACCEPT_OUTER), + result.outerHTML, + emitFlags, + result) + } +} + +/** + * Insert a selection position indicator. + * + * @param node {DOMNode} the node where to insert the selection indicator + * @param offs {Integer} the offset of the selection indicator + * @param textInd {String} the indicator to use if the node is a text node + * @param elemInd {String} the indicator to use if the node is an element node + */ +function insertSelectionIndicator(node, offs, textInd, elemInd) { + switch (node.nodeType) { + case DOM_NODE_TYPE_TEXT: + // Insert selection marker for text node into text content. + var text = node.data; + node.data = text.substring(0, offs) + textInd + text.substring(offs); + break; + + case DOM_NODE_TYPE_ELEMENT: + var child = node.firstChild; + try { + // node has other children: insert marker as comment node + var comment = document.createComment(elemInd); + while (child && offs) { + --offs; + child = child.nextSibling; + } + if (child) { + node.insertBefore(comment, child); + } else { + node.appendChild(comment); + } + } catch (ex) { + // can't append child comment -> insert as special attribute(s) + switch (elemInd) { + case '|': + node.setAttribute(ATTRNAME_SEL_START, '1'); + node.setAttribute(ATTRNAME_SEL_END, '1'); + break; + + case '{': + node.setAttribute(ATTRNAME_SEL_START, '1'); + break; + + case '}': + node.setAttribute(ATTRNAME_SEL_END, '1'); + break; + } + } + break; + } +} + +/** + * Adds quotes around all text nodes to show cases with non-normalized + * text nodes. Those are not a bug, but may still be usefil in helping to + * debug erroneous cases. + * + * @param node {DOMNode} root node from which to descend + */ +function encloseTextNodesWithQuotes(node) { + switch (node.nodeType) { + case DOM_NODE_TYPE_ELEMENT: + for (var i = 0; i < node.childNodes.length; ++i) { + encloseTextNodesWithQuotes(node.childNodes[i]); + } + break; + + case DOM_NODE_TYPE_TEXT: + node.data = '\x60' + node.data + '\xb4'; + break; + } +} + +/** + * Retrieve the result of a test run and do some preliminary canonicalization. + * + * @param container {Object} the container where to retrieve the result from as object reference + * @param result {Object} object reference that contains the result data + * @return {String} a preliminarily canonicalized innerHTML with selection markers + */ +function prepareHTMLTestResult(container, result) { + // Start with empty strings in case any of the below throws. + result.innerHTML = ''; + result.outerHTML = ''; + + // 1.) insert selection markers + var selRange = createFromWindow(container.win); + if (selRange) { + // save values, since range object gets auto-modified + var node1 = selRange.getAnchorNode(); + var offs1 = selRange.getAnchorOffset(); + var node2 = selRange.getFocusNode(); + var offs2 = selRange.getFocusOffset(); + + // add markers + if (node1 && node1 == node2 && offs1 == offs2) { + // collapsed selection + insertSelectionIndicator(node1, offs1, '^', '|'); + } else { + // Start point and end point are different + if (node1) { + insertSelectionIndicator(node1, offs1, '[', '{'); + } + + if (node2) { + if (node1 == node2 && offs1 < offs2) { + // Anchor indicator was inserted under the same node, so we need + // to shift the offset by 1 + ++offs2; + } + insertSelectionIndicator(node2, offs2, ']', '}'); + } + } + } + + // 2.) insert markers for text node boundaries; + encloseTextNodesWithQuotes(container.editor); + + // 3.) retrieve inner and outer HTML + result.innerHTML = initialCanonicalizationOf(container.editor.innerHTML); + result.bodyInnerHTML = initialCanonicalizationOf(container.body.innerHTML); + if (goog.userAgent.IE) { + result.outerHTML = initialCanonicalizationOf(container.editor.outerHTML); + result.bodyOuterHTML = initialCanonicalizationOf(container.body.outerHTML); + result.outerHTML = result.outerHTML.replace(/^\s+/, ''); + result.outerHTML = result.outerHTML.replace(/\s+$/, ''); + result.bodyOuterHTML = result.bodyOuterHTML.replace(/^\s+/, ''); + result.bodyOuterHTML = result.bodyOuterHTML.replace(/\s+$/, ''); + } else { + result.outerHTML = initialCanonicalizationOf(new XMLSerializer().serializeToString(container.editor)); + result.bodyOuterHTML = initialCanonicalizationOf(new XMLSerializer().serializeToString(container.body)); + } +} + +/** + * Compare a text test result to the expectation string(s). + * + * @param suite {Object} the test suite as object reference + * @param group {Object} group of tests within the suite the test belongs to + * @param test {Object} the test as object reference + * @param actual {String/Boolean} actual value + * @param expected {String/Array} expectation(s) + * @return {Boolean} whether we found a match + */ +function compareTextTestResultWith(suite, group, test, actual, expected) { + var expectedArr = getExpectationArray(expected); + // Find the most favorable result among the possible expectation strings. + var count = expectedArr.length; + + // If the value matches the expectation exactly, then we're fine. + for (var idx = 0; idx < count; ++idx) { + if (actual === expectedArr[idx]) + return true; + } + + // Otherwise see if we should canonicalize specific value types. + // + // We only need to look at font name, color and size units if the originating + // test was both a) queryCommandValue and b) querying a font name/color/size + // specific criterion. + // + // TODO(rolandsteiner): This is ugly! Refactor! + switch (getTestParameter(suite, group, test, PARAM_QUERYCOMMANDVALUE)) { + case 'backcolor': + case 'forecolor': + case 'hilitecolor': + for (var idx = 0; idx < count; ++idx) { + if (new Color(actual).compare(new Color(expectedArr[idx]))) + return true; + } + return false; + + case 'fontname': + for (var idx = 0; idx < count; ++idx) { + if (new FontName(actual).compare(new FontName(expectedArr[idx]))) + return true; + } + return false; + + case 'fontsize': + for (var idx = 0; idx < count; ++idx) { + if (new FontSize(actual).compare(new FontSize(expectedArr[idx]))) + return true; + } + return false; + } + + return false; +} + +/** + * Compare the passed-in text test result to the expectation string(s). + * Sets the global result variables. + * + * @param suite {Object} the test suite as object reference + * @param group {Object} group of tests within the suite the test belongs to + * @param test {Object} the test as object reference + * @param actual {String/Boolean} actual value + * @return {Integer} a RESUTLHTML... result value + * @see variables.js for result values + */ +function compareTextTestResult(suite, group, test, result) { + var expected = getTestParameter(suite, group, test, PARAM_EXPECTED); + if (compareTextTestResultWith(suite, group, test, result.output, expected)) { + result.valresult = VALRESULT_EQUAL; + return; + } + var accepted = getTestParameter(suite, group, test, PARAM_ACCEPT); + if (accepted && compareTextTestResultWith(suite, group, test, result.output, accepted)) { + result.valresult = VALRESULT_ACCEPT; + return; + } + result.valresult = VALRESULT_DIFF; +} + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/output.js b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/output.js new file mode 100644 index 0000000000..897efa0112 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/output.js @@ -0,0 +1,456 @@ +/** + * @fileoverview + * Functions used to format the test result output. + * + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the 'License') + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @version 0.1 + * @author rolandsteiner@google.com + */ + +/** + * Writes a fatal error to the output (replaces alert box) + * + * @param text {String} text to output + */ +function writeFatalError(text) { + var errorsStart = document.getElementById('errors'); + var divider = document.getElementById('divider'); + if (!errorsStart) { + errorsStart = document.createElement('hr'); + errorsStart.id = 'errors'; + divider.parentNode.insertBefore(errorsStart, divider); + } + var error = document.createElement('div'); + error.className = 'fatalerror'; + error.innerHTML = 'FATAL ERROR: ' + escapeOutput(text); + errorsStart.parentNode.insertBefore(error, divider); +} + +/** + * Generates a unique ID for a given single test out of the suite ID and + * test ID. + * + * @param suiteID {string} ID string of the suite + * @param testID {string} ID string of the individual tests + * @return {string} globally unique ID + */ +function generateOutputID(suiteID, testID) { + return commonIDPrefix + '-' + suiteID + '_' + testID; +} + +/** + * Function to highlight the selection markers + * + * @param str {String} a HTML string containing selection markers + * @return {String} the HTML string with highlighting tags around the markers + */ +function highlightSelectionMarkers(str) { + str = str.replace(/\[/g, '<span class="sel">[</span>'); + str = str.replace(/\]/g, '<span class="sel">]</span>'); + str = str.replace(/\^/g, '<span class="sel">^</span>'); + str = str.replace(/{/g, '<span class="sel">{</span>'); + str = str.replace(/}/g, '<span class="sel">}</span>'); + str = str.replace(/\|/g, '<b class="sel">|</b>'); + return str; +} + +/** + * Function to highlight the selection markers + * + * @param str {String} a HTML string containing selection markers + * @return {String} the HTML string with highlighting tags around the markers + */ +function highlightSelectionMarkersAndTextNodes(str) { + str = highlightSelectionMarkers(str); + str = str.replace(/\x60/g, '<span class="txt">'); + str = str.replace(/\xb4/g, '</span>'); + return str; +} + +/** + * Function to format output according to type + * + * @param value {String/Boolean} string or value to format + * @return {String} HTML-formatted string + */ +function formatValueOrString(value) { + if (value === undefined) + return '<i>undefined</i>'; + if (value === null) + return '<i>null</i>'; + + switch (typeof value) { + case 'boolean': + return '<i>' + value.toString() + '</i>'; + + case 'number': + return value.toString(); + + case 'string': + return "'" + escapeOutput(value) + "'"; + + default: + return '<i>(' + escapeOutput(value.toString()) + ')</i>'; + } +} + +/** + * Function to highlight text nodes + * + * @param suite {Object} the suite the test belongs to + * @param group {Object} the group within the suite the test belongs to + * @param test {Object} the test description as object reference + * @param actual {String} a HTML string containing text nodes with markers + * @return {String} string with highlighting tags around the text node parts + */ +function formatActualResult(suite, group, test, actual) { + if (typeof actual != 'string') + return formatValueOrString(actual); + + actual = escapeOutput(actual); + + // Fade attributes (or just style) if not actually tested for + if (!getTestParameter(suite, group, test, PARAM_CHECK_ATTRIBUTES)) { + actual = actual.replace(/([^ =]+)=\x22([^\x22]*)\x22/g, '<span class="fade">$1="$2"</span>'); + } else { + // NOTE: convert 'class="..."' first, before adding other <span class="fade">...</span> !!! + if (!getTestParameter(suite, group, test, PARAM_CHECK_CLASS)) { + actual = actual.replace(/class=\x22([^\x22]*)\x22/g, '<span class="fade">class="$1"</span>'); + } + if (!getTestParameter(suite, group, test, PARAM_CHECK_STYLE)) { + actual = actual.replace(/style=\x22([^\x22]*)\x22/g, '<span class="fade">style="$1"</span>'); + } + if (!getTestParameter(suite, group, test, PARAM_CHECK_ID)) { + actual = actual.replace(/id=\x22([^\x22]*)\x22/g, '<span class="fade">id="$1"</span>'); + } else { + // fade out contenteditable host element's 'editor-<xyz>' ID. + actual = actual.replace(/id=\x22editor-([^\x22]*)\x22/g, '<span class="fade">id="editor-$1"</span>'); + } + // grey out 'xmlns' + actual = actual.replace(/xmlns=\x22([^\x22]*)\x22/g, '<span class="fade">xmlns="$1"</span>'); + // remove 'onload' + actual = actual.replace(/onload=\x22[^\x22]*\x22 ?/g, ''); + } + // Highlight selection markers and text nodes. + actual = highlightSelectionMarkersAndTextNodes(actual); + + return actual; +} + +/** + * Escape text content for use with .innerHTML. + * + * @param str {String} HTML text to displayed + * @return {String} the escaped HTML + */ +function escapeOutput(str) { + return str ? str.replace(/\</g, '<').replace(/\>/g, '>') : ''; +} + +/** + * Fills in a single output table cell + * + * @param id {String} ID of the table cell + * @param val {String} inner HTML to set + * @param ttl {String, optional} value of the 'title' attribute + * @param cls {String, optional} class name for the cell + */ +function setTD(id, val, ttl, cls) { + var td = document.getElementById(id); + if (td) { + td.innerHTML = val; + if (ttl) { + td.title = ttl; + } + if (cls) { + td.className = cls; + } + } +} + +/** + * Outputs the results of a single test suite + * + * @param suite {Object} test suite as object reference + * @param clsID {String} test class ID ('Proposed', 'RFC', 'Final') + * @param group {Object} the group of tests within the suite the test belongs to + * @param testIdx {Object} the test as object reference + */ +function outputTestResults(suite, clsID, group, test) { + var suiteID = suite.id; + var cls = suite[clsID]; + var trID = generateOutputID(suiteID, test.id); + var testResult = results[suiteID][clsID][test.id]; + var testValOut = VALOUTPUT[testResult.valresult]; + var testSelOut = SELOUTPUT[testResult.selresult]; + + var suiteChecksSelOnly = !suiteChecksHTMLOrText(suite); + var testUsesHTML = !!getTestParameter(suite, group, test, PARAM_EXECCOMMAND) || + !!getTestParameter(suite, group, test, PARAM_FUNCTION); + + // Set background color for test ID + var td = document.getElementById(trID + IDOUT_TESTID); + if (td) { + td.className = (suiteChecksSelOnly && testResult.selresult != SELRESULT_NA) ? testSelOut.css : testValOut.css; + } + + // Fill in "Command" and "Value" cells + var cmd; + var cmdOutput = ' '; + var valOutput = ' '; + + if (cmd = getTestParameter(suite, group, test, PARAM_EXECCOMMAND)) { + cmdOutput = escapeOutput(cmd); + var val = getTestParameter(suite, group, test, PARAM_VALUE); + if (val !== undefined) { + valOutput = formatValueOrString(val); + } + } else if (cmd = getTestParameter(suite, group, test, PARAM_FUNCTION)) { + cmdOutput = '<i>' + escapeOutput(cmd) + '</i>'; + } else if (cmd = getTestParameter(suite, group, test, PARAM_QUERYCOMMANDSUPPORTED)) { + cmdOutput = '<i>queryCommandSupported</i>'; + valOutput = escapeOutput(cmd); + } else if (cmd = getTestParameter(suite, group, test, PARAM_QUERYCOMMANDENABLED)) { + cmdOutput = '<i>queryCommandEnabled</i>'; + valOutput = escapeOutput(cmd); + } else if (cmd = getTestParameter(suite, group, test, PARAM_QUERYCOMMANDINDETERM)) { + cmdOutput = '<i>queryCommandIndeterm</i>'; + valOutput = escapeOutput(cmd); + } else if (cmd = getTestParameter(suite, group, test, PARAM_QUERYCOMMANDSTATE)) { + cmdOutput = '<i>queryCommandState</i>'; + valOutput = escapeOutput(cmd); + } else if (cmd = getTestParameter(suite, group, test, PARAM_QUERYCOMMANDVALUE)) { + cmdOutput = '<i>queryCommandValue</i>'; + valOutput = escapeOutput(cmd); + } else { + cmdOutput = '<i>(none)</i>'; + } + setTD(trID + IDOUT_COMMAND, cmdOutput); + setTD(trID + IDOUT_VALUE, valOutput); + + // Fill in "Attribute checked?" and "Style checked?" cells + if (testUsesHTML) { + var checkAttrs = getTestParameter(suite, group, test, PARAM_CHECK_ATTRIBUTES); + var checkStyle = getTestParameter(suite, group, test, PARAM_CHECK_STYLE); + + setTD(trID + IDOUT_CHECKATTRS, + checkAttrs ? OUTSTR_YES : OUTSTR_NO, + checkAttrs ? 'attributes must match' : 'attributes are ignored'); + + if (checkAttrs && checkStyle) { + setTD(trID + IDOUT_CHECKSTYLE, OUTSTR_YES, 'style attribute contents must match'); + } else if (checkAttrs) { + setTD(trID + IDOUT_CHECKSTYLE, OUTSTR_NO, 'style attribute contents is ignored'); + } else { + setTD(trID + IDOUT_CHECKSTYLE, OUTSTR_NO, 'all attributes (incl. style) are ignored'); + } + } else { + setTD(trID + IDOUT_CHECKATTRS, OUTSTR_NA, 'attributes not applicable'); + setTD(trID + IDOUT_CHECKSTYLE, OUTSTR_NA, 'style not applicable'); + } + + // Fill in test pad specification cell (initial HTML + selection markers) + setTD(trID + IDOUT_PAD, highlightSelectionMarkers(escapeOutput(getTestParameter(suite, group, test, PARAM_PAD)))); + + // Fill in expected result(s) cell + var expectedOutput = ''; + var expectedArr = getExpectationArray(getTestParameter(suite, group, test, PARAM_EXPECTED)); + for (var idx = 0; idx < expectedArr.length; ++idx) { + if (expectedOutput) { + expectedOutput += '\xA0\xA0\xA0<i>or</i><br>'; + } + expectedOutput += testUsesHTML ? highlightSelectionMarkers(escapeOutput(expectedArr[idx])) + : formatValueOrString(expectedArr[idx]); + } + var acceptedArr = getExpectationArray(getTestParameter(suite, group, test, PARAM_ACCEPT)); + for (var idx = 0; idx < acceptedArr.length; ++idx) { + expectedOutput += '<span class="accexp">\xA0\xA0\xA0<i>or</i></span><br><span class="accexp">'; + expectedOutput += testUsesHTML ? highlightSelectionMarkers(escapeOutput(acceptedArr[idx])) + : formatValueOrString(acceptedArr[idx]); + expectedOutput += '</span>'; + } + // TODO(rolandsteiner): THIS IS UGLY, relying on 'div' container being index 2, + // AND not allowing other containers to have 'outer' results - change!!! + var outerOutput = ''; + expectedArr = getExpectationArray(getContainerParameter(suite, group, test, containers[2], PARAM_EXPECTED_OUTER)); + for (var idx = 0; idx < expectedArr.length; ++idx) { + if (outerOutput) { + outerOutput += '\xA0\xA0\xA0<i>or</i><br>'; + } + outerOutput += testUsesHTML ? highlightSelectionMarkers(escapeOutput(expectedArr[idx])) + : formatValueOrString(expectedArr[idx]); + } + acceptedArr = getExpectationArray(getContainerParameter(suite, group, test, containers[2], PARAM_ACCEPT_OUTER)); + for (var idx = 0; idx < acceptedArr.length; ++idx) { + if (outerOutput) { + outerOutput += '<span class="accexp">\xA0\xA0\xA0<i>or</i></span><br>'; + } + outerOutput += '<span class="accexp">'; + outerOutput += testUsesHTML ? highlightSelectionMarkers(escapeOutput(acceptedArr[idx])) + : formatValueOrString(acceptedArr[idx]); + outerOutput += '</span>'; + } + if (outerOutput) { + expectedOutput += '<hr>' + outerOutput; + } + setTD(trID + IDOUT_EXPECTED, expectedOutput); + + // Iterate over the individual container results + for (var cntIdx = 0; cntIdx < containers.length; ++cntIdx) { + var cntID = containers[cntIdx].id; + var cntTD = document.getElementById(trID + IDOUT_CONTAINER + cntID); + var cntResult = testResult[cntID]; + var cntValOut = VALOUTPUT[cntResult.valresult]; + var cntSelOut = SELOUTPUT[cntResult.selresult]; + var cssVal = cntValOut.css; + var cssSel = (!suiteChecksSelOnly || cntResult.selresult != SELRESULT_NA) ? cntSelOut.css : cssVal; + var cssCnt = cssVal; + + // Fill in result status cell ("PASS", "ACC.", "FAIL", "EXC.", etc.) + setTD(trID + IDOUT_STATUSVAL + cntID, cntValOut.output, cntValOut.title, cssVal); + + // Fill in selection status cell ("PASS", "ACC.", "FAIL", "N/A") + setTD(trID + IDOUT_STATUSSEL + cntID, cntSelOut.output, cntSelOut.title, cssSel); + + // Fill in actual result + switch (cntResult.valresult) { + case VALRESULT_SETUP_EXCEPTION: + setTD(trID + IDOUT_ACTUAL + cntID, + SETUP_EXCEPTION + '(mouseover)', + escapeOutput(cntResult.output), + cssVal); + break; + + case VALRESULT_EXECUTION_EXCEPTION: + setTD(trID + IDOUT_ACTUAL + cntID, + EXECUTION_EXCEPTION + '(mouseover)', + escapeOutput(cntResult.output.toString()), + cssVal); + break; + + case VALRESULT_VERIFICATION_EXCEPTION: + setTD(trID + IDOUT_ACTUAL + cntID, + VERIFICATION_EXCEPTION + '(mouseover)', + escapeOutput(cntResult.output.toString()), + cssVal); + break; + + case VALRESULT_UNSUPPORTED: + setTD(trID + IDOUT_ACTUAL + cntID, + escapeOutput(cntResult.output), + '', + cssVal); + break; + + case VALRESULT_CANARY: + setTD(trID + IDOUT_ACTUAL + cntID, + highlightSelectionMarkersAndTextNodes(escapeOutput(cntResult.output)), + '', + cssVal); + break; + + case VALRESULT_DIFF: + case VALRESULT_ACCEPT: + case VALRESULT_EQUAL: + if (!testUsesHTML) { + setTD(trID + IDOUT_ACTUAL + cntID, + formatValueOrString(cntResult.output), + '', + cssVal); + } else if (cntResult.selresult == SELRESULT_CANARY) { + cssCnt = suiteChecksSelOnly ? cssSel : cssVal; + setTD(trID + IDOUT_ACTUAL + cntID, + highlightSelectionMarkersAndTextNodes(escapeOutput(cntResult.output)), + '', + cssCnt); + } else { + cssCnt = suiteChecksSelOnly ? cssSel : cssVal; + setTD(trID + IDOUT_ACTUAL + cntID, + formatActualResult(suite, group, test, cntResult.output), + '', + cssCnt); + } + break; + + default: + cssCnt = 'exception'; + setTD(trID + IDOUT_ACTUAL + cntID, + INTERNAL_ERR + 'UNKNOWN RESULT VALUE', + '', + cssCnt); + } + + if (cntTD) { + cntTD.className = cssCnt; + } + } +} + +/** + * Outputs the results of a single test suite + * + * @param {Object} suite as object reference + */ +function outputTestSuiteResults(suite) { + var suiteID = suite.id; + var span; + + span = document.getElementById(suiteID + '-score'); + if (span) { + span.innerHTML = results[suiteID].valscore + '/' + results[suiteID].count; + } + span = document.getElementById(suiteID + '-selscore'); + if (span) { + span.innerHTML = results[suiteID].selscore + '/' + results[suiteID].count; + } + span = document.getElementById(suiteID + '-time'); + if (span) { + span.innerHTML = results[suiteID].time; + } + span = document.getElementById(suiteID + '-progress'); + if (span) { + span.style.color = 'green'; + } + + for (var clsIdx = 0; clsIdx < testClassCount; ++clsIdx) { + var clsID = testClassIDs[clsIdx]; + var cls = suite[clsID]; + if (!cls) + continue; + + span = document.getElementById(suiteID + '-' + clsID + '-score'); + if (span) { + span.innerHTML = results[suiteID][clsID].valscore + '/' + results[suiteID][clsID].count; + } + span = document.getElementById(suiteID + '-' + clsID + '-selscore'); + if (span) { + span.innerHTML = results[suiteID][clsID].selscore + '/' + results[suiteID][clsID].count; + } + + var groupCount = cls.length; + + for (var groupIdx = 0; groupIdx < groupCount; ++groupIdx) { + var group = cls[groupIdx]; + var testCount = group.tests.length; + + for (var testIdx = 0; testIdx < testCount; ++testIdx) { + var test = group.tests[testIdx]; + + outputTestResults(suite, clsID, group, test); + } + } + } +} diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/pad.js b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/pad.js new file mode 100644 index 0000000000..282f0d907d --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/pad.js @@ -0,0 +1,269 @@ +/** + * @fileoverview + * Functions used to handle test and expectation strings. + * + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the 'License') + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @version 0.1 + * @author rolandsteiner@google.com + */ + +/** + * Normalize text selection indicators and convert inter-element selection + * indicators to comments. + * + * Note that this function relies on the spaces of the input string already + * having been normalized by canonicalizeSpaces! + * + * @param pad {String} HTML string that includes selection marker characters + * @return {String} the HTML string with the selection markers converted + */ +function convertSelectionIndicators(pad) { + // Sanity check: Markers { } | only directly before or after an element, + // or just before a closing > (i.e., not within a text node). + // Note that intra-tag selection markers have already been converted to the + // special selection attribute(s) above. + if (/[^>][{}\|][^<>]/.test(pad) || + /^[{}\|][^<]/.test(pad) || + /[^>][{}\|]$/.test(pad) || + /^[{}\|]*$/.test(pad)) { + throw SETUP_BAD_SELECTION_SPEC; + } + + // Convert intra-tag selection markers to special attributes. + pad = pad.replace(/\{\>/g, ATTRNAME_SEL_START + '="1">'); + pad = pad.replace(/\}\>/g, ATTRNAME_SEL_END + '="1">'); + pad = pad.replace(/\|\>/g, ATTRNAME_SEL_START + '="1" ' + + ATTRNAME_SEL_END + '="1">'); + + // Convert remaining {, }, | to comments with '[' and ']' data. + pad = pad.replace('{', '<!--[-->'); + pad = pad.replace('}', '<!--]-->'); + pad = pad.replace('|', '<!--[--><!--]-->'); + + // Convert caret indicator ^ to empty selection indicator [] + // (this simplifies further processing). + pad = pad.replace(/\^/, '[]'); + + return pad; +} + +/** + * Derives one point of the selection from the indicators with the HTML tree: + * '[' or ']' within a text or comment node, or the special selection + * attributes within an element node. + * + * @param root {DOMNode} root node of the recursive search + * @param marker {String} which marker to look for: '[' or ']' + * @return {Object} a pair object: {node: {DOMNode}/null, offset: {Integer}} + */ +function deriveSelectionPoint(root, marker) { + switch (root.nodeType) { + case DOM_NODE_TYPE_ELEMENT: + if (root.attributes) { + // Note: getAttribute() is necessary for this to work on all browsers! + if (marker == '[' && root.getAttribute(ATTRNAME_SEL_START)) { + root.removeAttribute(ATTRNAME_SEL_START); + return {node: root, offs: 0}; + } + if (marker == ']' && root.getAttribute(ATTRNAME_SEL_END)) { + root.removeAttribute(ATTRNAME_SEL_END); + return {node: root, offs: 0}; + } + } + for (var i = 0; i < root.childNodes.length; ++i) { + var pair = deriveSelectionPoint(root.childNodes[i], marker); + if (pair.node) { + return pair; + } + } + break; + + case DOM_NODE_TYPE_TEXT: + var pos = root.data.indexOf(marker); + if (pos != -1) { + // Remove selection marker from text. + var nodeText = root.data; + root.data = nodeText.substr(0, pos) + nodeText.substr(pos + 1); + return {node: root, offs: pos }; + } + break; + + case DOM_NODE_TYPE_COMMENT: + var pos = root.data.indexOf(marker); + if (pos != -1) { + // Remove comment node from parent. + var helper = root.previousSibling; + + for (pos = 0; helper; ++pos ) { + helper = helper.previousSibling; + } + helper = root; + root = root.parentNode; + root.removeChild(helper); + return {node: root, offs: pos }; + } + break; + } + + return {node: null, offs: 0 }; +} + +/** + * Initialize the test HTML with the starting state specified in the test. + * + * The selection is specified "inline", using special characters: + * ^ a collapsed text caret selection (same as []) + * [ the selection start within a text node + * ] the selection end within a text node + * | collapsed selection between elements (same as {}) + * { selection starting with the following element + * } selection ending with the preceding element + * {, } and | can also be used within an element tag, just before the closing + * angle bracket > to specify a selection [element, 0] where the element + * doesn't otherwise have any children. Ex.: <hr {>foobarbaz<hr }> + * + * Explicit and implicit specification can also be mixed between the 2 points. + * + * A pad string must only contain at most ONE of the above that is suitable for + * that start or end point, respectively, and must contain either both or none. + * + * @param suite {Object} suite that test originates in as object reference + * @param group {Object} group of tests within the suite the test belongs to + * @param test {Object} test to be run as object reference + * @param container {Object} container descriptor as object reference + */ +function initContainer(suite, group, test, container) { + var pad = getTestParameter(suite, group, test, PARAM_PAD); + pad = canonicalizeSpaces(pad); + pad = convertSelectionIndicators(pad); + + if (container.editorID) { + container.body.innerHTML = container.canary + container.tagOpen + pad + container.tagClose + container.canary; + container.editor = container.doc.getElementById(container.editorID); + } else { + container.body.innerHTML = pad; + container.editor = container.body; + } + + win = container.win; + doc = container.doc; + body = container.body; + editor = container.editor; + sel = null; + + if (!editor) { + throw SETUP_CONTAINER; + } + + if (getTestParameter(suite, group, test, PARAM_STYLE_WITH_CSS)) { + try { + container.doc.execCommand('styleWithCSS', false, true); + } catch (ex) { + // ignore exception if unsupported + } + } + + var selAnchor = deriveSelectionPoint(editor, '['); + var selFocus = deriveSelectionPoint(editor, ']'); + + // sanity check + if (!selAnchor || !selFocus) { + throw SETUP_SELECTION; + } + + if (!selAnchor.node || !selFocus.node) { + if (selAnchor.node || selFocus.node) { + // Broken test: only one selection point was specified + throw SETUP_BAD_SELECTION_SPEC; + } + sel = null; + return; + } + + if (selAnchor.node === selFocus.node) { + if (selAnchor.offs > selFocus.offs) { + // Both selection points are within the same node, the selection was + // specified inline (thus the end indicator element or character was + // removed), and the end point is before the start (reversed selection). + // Start offset that was derived is now off by 1 and needs adjustment. + --selAnchor.offs; + } + + if (selAnchor.offs === selFocus.offs) { + createCaret(selAnchor.node, selAnchor.offs).select(); + try { + sel = win.getSelection(); + } catch (ex) { + sel = undefined; + } + return; + } + } + + createFromNodes(selAnchor.node, selAnchor.offs, selFocus.node, selFocus.offs).select(); + + try { + sel = win.getSelection(); + } catch (ex) { + sel = undefined; + } +} + +/** + * Reset the editor element after a test is run. + * + * @param container {Object} container descriptor as object reference + */ +function resetContainer(container) { + // Remove errant styles and attributes that may have been set on the <body>. + container.body.removeAttribute('style'); + container.body.removeAttribute('color'); + container.body.removeAttribute('bgcolor'); + + try { + container.doc.execCommand('styleWithCSS', false, false); + } catch (ex) { + // Ignore exception if unsupported. + } +} + +/** + * Initialize the editor document. + */ +function initEditorDocs() { + for (var c = 0; c < containers.length; ++c) { + var container = containers[c]; + + container.iframe = document.getElementById('iframe-' + container.id); + container.win = container.iframe.contentWindow; + container.doc = container.win.document; + container.body = container.doc.body; + // container.editor is set per test (changes on embedded editor elements). + + // Some browsers require a selection to go with their 'styleWithCSS'. + try { + container.win.getSelection().selectAllChildren(editor); + } catch (ex) { + // ignore exception if unsupported + } + // Default styleWithCSS to false. + try { + container.doc.execCommand('styleWithCSS', false, false); + } catch (ex) { + // ignore exception if unsupported + } + } +} diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/range-bootstrap.js b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/range-bootstrap.js new file mode 100644 index 0000000000..24aef7ae9c --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/range-bootstrap.js @@ -0,0 +1,5 @@ +goog.require('goog.dom.Range'); + +window.createFromWindow = goog.dom.Range.createFromWindow; +window.createFromNodes = goog.dom.Range.createFromNodes; +window.createCaret = goog.dom.Range.createCaret; diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/range.js b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/range.js new file mode 100644 index 0000000000..3266761115 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/range.js @@ -0,0 +1,6184 @@ +var COMPILED = false; +var goog = goog || {}; +goog.global = this; +goog.DEBUG = true; +goog.LOCALE = "en"; +goog.evalWorksForGlobals_ = null; +goog.provide = function(name) { + if(!COMPILED) { + if(goog.getObjectByName(name) && !goog.implicitNamespaces_[name]) { + throw Error('Namespace "' + name + '" already declared.'); + } + var namespace = name; + while(namespace = namespace.substring(0, namespace.lastIndexOf("."))) { + goog.implicitNamespaces_[namespace] = true + } + } + goog.exportPath_(name) +}; +if(!COMPILED) { + goog.implicitNamespaces_ = {} +} +goog.exportPath_ = function(name, opt_object, opt_objectToExportTo) { + var parts = name.split("."); + var cur = opt_objectToExportTo || goog.global; + if(!(parts[0] in cur) && cur.execScript) { + cur.execScript("var " + parts[0]) + } + for(var part;parts.length && (part = parts.shift());) { + if(!parts.length && goog.isDef(opt_object)) { + cur[part] = opt_object + }else { + if(cur[part]) { + cur = cur[part] + }else { + cur = cur[part] = {} + } + } + } +}; +goog.getObjectByName = function(name, opt_obj) { + var parts = name.split("."); + var cur = opt_obj || goog.global; + for(var part;part = parts.shift();) { + if(cur[part]) { + cur = cur[part] + }else { + return null + } + } + return cur +}; +goog.globalize = function(obj, opt_global) { + var global = opt_global || goog.global; + for(var x in obj) { + global[x] = obj[x] + } +}; +goog.addDependency = function(relPath, provides, requires) { + if(!COMPILED) { + var provide, require; + var path = relPath.replace(/\\/g, "/"); + var deps = goog.dependencies_; + for(var i = 0;provide = provides[i];i++) { + deps.nameToPath[provide] = path; + if(!(path in deps.pathToNames)) { + deps.pathToNames[path] = {} + } + deps.pathToNames[path][provide] = true + } + for(var j = 0;require = requires[j];j++) { + if(!(path in deps.requires)) { + deps.requires[path] = {} + } + deps.requires[path][require] = true + } + } +}; +goog.require = function(rule) { + if(!COMPILED) { + if(goog.getObjectByName(rule)) { + return + } + var path = goog.getPathFromDeps_(rule); + if(path) { + goog.included_[path] = true; + goog.writeScripts_() + }else { + var errorMessage = "goog.require could not find: " + rule; + if(goog.global.console) { + goog.global.console["error"](errorMessage) + } + throw Error(errorMessage); + } + } +}; +goog.basePath = ""; +goog.global.CLOSURE_BASE_PATH; +goog.nullFunction = function() { +}; +goog.identityFunction = function(var_args) { + return arguments[0] +}; +goog.abstractMethod = function() { + throw Error("unimplemented abstract method"); +}; +goog.addSingletonGetter = function(ctor) { + ctor.getInstance = function() { + return ctor.instance_ || (ctor.instance_ = new ctor) + } +}; +if(!COMPILED) { + goog.included_ = {}; + goog.dependencies_ = {pathToNames:{}, nameToPath:{}, requires:{}, visited:{}, written:{}}; + goog.inHtmlDocument_ = function() { + var doc = goog.global.document; + return typeof doc != "undefined" && "write" in doc + }; + goog.findBasePath_ = function() { + if(!goog.inHtmlDocument_()) { + return + } + var doc = goog.global.document; + if(goog.global.CLOSURE_BASE_PATH) { + goog.basePath = goog.global.CLOSURE_BASE_PATH; + return + } + var scripts = doc.getElementsByTagName("script"); + for(var i = scripts.length - 1;i >= 0;--i) { + var src = scripts[i].src; + var l = src.length; + if(src.substr(l - 7) == "base.js") { + goog.basePath = src.substr(0, l - 7); + return + } + } + }; + goog.writeScriptTag_ = function(src) { + if(goog.inHtmlDocument_() && !goog.dependencies_.written[src]) { + goog.dependencies_.written[src] = true; + var doc = goog.global.document; + doc.write('<script type="text/javascript" src="' + src + '"></' + "script>") + } + }; + goog.writeScripts_ = function() { + var scripts = []; + var seenScript = {}; + var deps = goog.dependencies_; + function visitNode(path) { + if(path in deps.written) { + return + } + if(path in deps.visited) { + if(!(path in seenScript)) { + seenScript[path] = true; + scripts.push(path) + } + return + } + deps.visited[path] = true; + if(path in deps.requires) { + for(var requireName in deps.requires[path]) { + if(requireName in deps.nameToPath) { + visitNode(deps.nameToPath[requireName]) + }else { + if(!goog.getObjectByName(requireName)) { + throw Error("Undefined nameToPath for " + requireName); + } + } + } + } + if(!(path in seenScript)) { + seenScript[path] = true; + scripts.push(path) + } + } + for(var path in goog.included_) { + if(!deps.written[path]) { + visitNode(path) + } + } + for(var i = 0;i < scripts.length;i++) { + if(scripts[i]) { + goog.writeScriptTag_(goog.basePath + scripts[i]) + }else { + throw Error("Undefined script input"); + } + } + }; + goog.getPathFromDeps_ = function(rule) { + if(rule in goog.dependencies_.nameToPath) { + return goog.dependencies_.nameToPath[rule] + }else { + return null + } + }; + goog.findBasePath_(); +} +goog.typeOf = function(value) { + var s = typeof value; + if(s == "object") { + if(value) { + if(value instanceof Array || !(value instanceof Object) && Object.prototype.toString.call(value) == "[object Array]" || typeof value.length == "number" && typeof value.splice != "undefined" && typeof value.propertyIsEnumerable != "undefined" && !value.propertyIsEnumerable("splice")) { + return"array" + } + if(!(value instanceof Object) && (Object.prototype.toString.call(value) == "[object Function]" || typeof value.call != "undefined" && typeof value.propertyIsEnumerable != "undefined" && !value.propertyIsEnumerable("call"))) { + return"function" + } + }else { + return"null" + } + }else { + if(s == "function" && typeof value.call == "undefined") { + return"object" + } + } + return s +}; +goog.propertyIsEnumerableCustom_ = function(object, propName) { + if(propName in object) { + for(var key in object) { + if(key == propName && Object.prototype.hasOwnProperty.call(object, propName)) { + return true + } + } + } + return false +}; +goog.propertyIsEnumerable_ = function(object, propName) { + if(object instanceof Object) { + return Object.prototype.propertyIsEnumerable.call(object, propName) + }else { + return goog.propertyIsEnumerableCustom_(object, propName) + } +}; +goog.isDef = function(val) { + return val !== undefined +}; +goog.isNull = function(val) { + return val === null +}; +goog.isDefAndNotNull = function(val) { + return val != null +}; +goog.isArray = function(val) { + return goog.typeOf(val) == "array" +}; +goog.isArrayLike = function(val) { + var type = goog.typeOf(val); + return type == "array" || type == "object" && typeof val.length == "number" +}; +goog.isDateLike = function(val) { + return goog.isObject(val) && typeof val.getFullYear == "function" +}; +goog.isString = function(val) { + return typeof val == "string" +}; +goog.isBoolean = function(val) { + return typeof val == "boolean" +}; +goog.isNumber = function(val) { + return typeof val == "number" +}; +goog.isFunction = function(val) { + return goog.typeOf(val) == "function" +}; +goog.isObject = function(val) { + var type = goog.typeOf(val); + return type == "object" || type == "array" || type == "function" +}; +goog.getUid = function(obj) { + return obj[goog.UID_PROPERTY_] || (obj[goog.UID_PROPERTY_] = ++goog.uidCounter_) +}; +goog.removeUid = function(obj) { + if("removeAttribute" in obj) { + obj.removeAttribute(goog.UID_PROPERTY_) + } + try { + delete obj[goog.UID_PROPERTY_] + }catch(ex) { + } +}; +goog.UID_PROPERTY_ = "closure_uid_" + Math.floor(Math.random() * 2147483648).toString(36); +goog.uidCounter_ = 0; +goog.getHashCode = goog.getUid; +goog.removeHashCode = goog.removeUid; +goog.cloneObject = function(obj) { + var type = goog.typeOf(obj); + if(type == "object" || type == "array") { + if(obj.clone) { + return obj.clone() + } + var clone = type == "array" ? [] : {}; + for(var key in obj) { + clone[key] = goog.cloneObject(obj[key]) + } + return clone + } + return obj +}; +Object.prototype.clone; +goog.bind = function(fn, selfObj, var_args) { + var context = selfObj || goog.global; + if(arguments.length > 2) { + var boundArgs = Array.prototype.slice.call(arguments, 2); + return function() { + var newArgs = Array.prototype.slice.call(arguments); + Array.prototype.unshift.apply(newArgs, boundArgs); + return fn.apply(context, newArgs) + } + }else { + return function() { + return fn.apply(context, arguments) + } + } +}; +goog.partial = function(fn, var_args) { + var args = Array.prototype.slice.call(arguments, 1); + return function() { + var newArgs = Array.prototype.slice.call(arguments); + newArgs.unshift.apply(newArgs, args); + return fn.apply(this, newArgs) + } +}; +goog.mixin = function(target, source) { + for(var x in source) { + target[x] = source[x] + } +}; +goog.now = Date.now || function() { + return+new Date +}; +goog.globalEval = function(script) { + if(goog.global.execScript) { + goog.global.execScript(script, "JavaScript") + }else { + if(goog.global.eval) { + if(goog.evalWorksForGlobals_ == null) { + goog.global.eval("var _et_ = 1;"); + if(typeof goog.global["_et_"] != "undefined") { + delete goog.global["_et_"]; + goog.evalWorksForGlobals_ = true + }else { + goog.evalWorksForGlobals_ = false + } + } + if(goog.evalWorksForGlobals_) { + goog.global.eval(script) + }else { + var doc = goog.global.document; + var scriptElt = doc.createElement("script"); + scriptElt.type = "text/javascript"; + scriptElt.defer = false; + scriptElt.appendChild(doc.createTextNode(script)); + doc.body.appendChild(scriptElt); + doc.body.removeChild(scriptElt) + } + }else { + throw Error("goog.globalEval not available"); + } + } +}; +goog.typedef = true; +goog.cssNameMapping_; +goog.getCssName = function(className, opt_modifier) { + var cssName = className + (opt_modifier ? "-" + opt_modifier : ""); + return goog.cssNameMapping_ && cssName in goog.cssNameMapping_ ? goog.cssNameMapping_[cssName] : cssName +}; +goog.setCssNameMapping = function(mapping) { + goog.cssNameMapping_ = mapping +}; +goog.getMsg = function(str, opt_values) { + var values = opt_values || {}; + for(var key in values) { + var value = ("" + values[key]).replace(/\$/g, "$$$$"); + str = str.replace(new RegExp("\\{\\$" + key + "\\}", "gi"), value) + } + return str +}; +goog.exportSymbol = function(publicPath, object, opt_objectToExportTo) { + goog.exportPath_(publicPath, object, opt_objectToExportTo) +}; +goog.exportProperty = function(object, publicName, symbol) { + object[publicName] = symbol +}; +goog.inherits = function(childCtor, parentCtor) { + function tempCtor() { + } + tempCtor.prototype = parentCtor.prototype; + childCtor.superClass_ = parentCtor.prototype; + childCtor.prototype = new tempCtor; + childCtor.prototype.constructor = childCtor +}; +goog.base = function(me, opt_methodName, var_args) { + var caller = arguments.callee.caller; + if(caller.superClass_) { + return caller.superClass_.constructor.apply(me, Array.prototype.slice.call(arguments, 1)) + } + var args = Array.prototype.slice.call(arguments, 2); + var foundCaller = false; + for(var ctor = me.constructor;ctor;ctor = ctor.superClass_ && ctor.superClass_.constructor) { + if(ctor.prototype[opt_methodName] === caller) { + foundCaller = true + }else { + if(foundCaller) { + return ctor.prototype[opt_methodName].apply(me, args) + } + } + } + if(me[opt_methodName] === caller) { + return me.constructor.prototype[opt_methodName].apply(me, args) + }else { + throw Error("goog.base called from a method of one name " + "to a method of a different name"); + } +}; +goog.scope = function(fn) { + fn.call(goog.global) +}; +goog.provide("goog.debug.Error"); +goog.debug.Error = function(opt_msg) { + this.stack = (new Error).stack || ""; + if(opt_msg) { + this.message = String(opt_msg) + } +}; +goog.inherits(goog.debug.Error, Error); +goog.debug.Error.prototype.name = "CustomError"; +goog.provide("goog.string"); +goog.provide("goog.string.Unicode"); +goog.string.Unicode = {NBSP:"\u00a0"}; +goog.string.startsWith = function(str, prefix) { + return str.lastIndexOf(prefix, 0) == 0 +}; +goog.string.endsWith = function(str, suffix) { + var l = str.length - suffix.length; + return l >= 0 && str.indexOf(suffix, l) == l +}; +goog.string.caseInsensitiveStartsWith = function(str, prefix) { + return goog.string.caseInsensitiveCompare(prefix, str.substr(0, prefix.length)) == 0 +}; +goog.string.caseInsensitiveEndsWith = function(str, suffix) { + return goog.string.caseInsensitiveCompare(suffix, str.substr(str.length - suffix.length, suffix.length)) == 0 +}; +goog.string.subs = function(str, var_args) { + for(var i = 1;i < arguments.length;i++) { + var replacement = String(arguments[i]).replace(/\$/g, "$$$$"); + str = str.replace(/\%s/, replacement) + } + return str +}; +goog.string.collapseWhitespace = function(str) { + return str.replace(/[\s\xa0]+/g, " ").replace(/^\s+|\s+$/g, "") +}; +goog.string.isEmpty = function(str) { + return/^[\s\xa0]*$/.test(str) +}; +goog.string.isEmptySafe = function(str) { + return goog.string.isEmpty(goog.string.makeSafe(str)) +}; +goog.string.isBreakingWhitespace = function(str) { + return!/[^\t\n\r ]/.test(str) +}; +goog.string.isAlpha = function(str) { + return!/[^a-zA-Z]/.test(str) +}; +goog.string.isNumeric = function(str) { + return!/[^0-9]/.test(str) +}; +goog.string.isAlphaNumeric = function(str) { + return!/[^a-zA-Z0-9]/.test(str) +}; +goog.string.isSpace = function(ch) { + return ch == " " +}; +goog.string.isUnicodeChar = function(ch) { + return ch.length == 1 && ch >= " " && ch <= "~" || ch >= "\u0080" && ch <= "\ufffd" +}; +goog.string.stripNewlines = function(str) { + return str.replace(/(\r\n|\r|\n)+/g, " ") +}; +goog.string.canonicalizeNewlines = function(str) { + return str.replace(/(\r\n|\r|\n)/g, "\n") +}; +goog.string.normalizeWhitespace = function(str) { + return str.replace(/\xa0|\s/g, " ") +}; +goog.string.normalizeSpaces = function(str) { + return str.replace(/\xa0|[ \t]+/g, " ") +}; +goog.string.trim = function(str) { + return str.replace(/^[\s\xa0]+|[\s\xa0]+$/g, "") +}; +goog.string.trimLeft = function(str) { + return str.replace(/^[\s\xa0]+/, "") +}; +goog.string.trimRight = function(str) { + return str.replace(/[\s\xa0]+$/, "") +}; +goog.string.caseInsensitiveCompare = function(str1, str2) { + var test1 = String(str1).toLowerCase(); + var test2 = String(str2).toLowerCase(); + if(test1 < test2) { + return-1 + }else { + if(test1 == test2) { + return 0 + }else { + return 1 + } + } +}; +goog.string.numerateCompareRegExp_ = /(\.\d+)|(\d+)|(\D+)/g; +goog.string.numerateCompare = function(str1, str2) { + if(str1 == str2) { + return 0 + } + if(!str1) { + return-1 + } + if(!str2) { + return 1 + } + var tokens1 = str1.toLowerCase().match(goog.string.numerateCompareRegExp_); + var tokens2 = str2.toLowerCase().match(goog.string.numerateCompareRegExp_); + var count = Math.min(tokens1.length, tokens2.length); + for(var i = 0;i < count;i++) { + var a = tokens1[i]; + var b = tokens2[i]; + if(a != b) { + var num1 = parseInt(a, 10); + if(!isNaN(num1)) { + var num2 = parseInt(b, 10); + if(!isNaN(num2) && num1 - num2) { + return num1 - num2 + } + } + return a < b ? -1 : 1 + } + } + if(tokens1.length != tokens2.length) { + return tokens1.length - tokens2.length + } + return str1 < str2 ? -1 : 1 +}; +goog.string.encodeUriRegExp_ = /^[a-zA-Z0-9\-_.!~*'()]*$/; +goog.string.urlEncode = function(str) { + str = String(str); + if(!goog.string.encodeUriRegExp_.test(str)) { + return encodeURIComponent(str) + } + return str +}; +goog.string.urlDecode = function(str) { + return decodeURIComponent(str.replace(/\+/g, " ")) +}; +goog.string.newLineToBr = function(str, opt_xml) { + return str.replace(/(\r\n|\r|\n)/g, opt_xml ? "<br />" : "<br>") +}; +goog.string.htmlEscape = function(str, opt_isLikelyToContainHtmlChars) { + if(opt_isLikelyToContainHtmlChars) { + return str.replace(goog.string.amperRe_, "&").replace(goog.string.ltRe_, "<").replace(goog.string.gtRe_, ">").replace(goog.string.quotRe_, """) + }else { + if(!goog.string.allRe_.test(str)) { + return str + } + if(str.includes("&")) { + str = str.replace(goog.string.amperRe_, "&") + } + if(str.includes("<")) { + str = str.replace(goog.string.ltRe_, "<") + } + if(str.includes(">")) { + str = str.replace(goog.string.gtRe_, ">") + } + if(str.includes('"')) { + str = str.replace(goog.string.quotRe_, """) + } + return str + } +}; +goog.string.amperRe_ = /&/g; +goog.string.ltRe_ = /</g; +goog.string.gtRe_ = />/g; +goog.string.quotRe_ = /\"/g; +goog.string.allRe_ = /[&<>\"]/; +goog.string.unescapeEntities = function(str) { + if(goog.string.contains(str, "&")) { + if("document" in goog.global && !goog.string.contains(str, "<")) { + return goog.string.unescapeEntitiesUsingDom_(str) + }else { + return goog.string.unescapePureXmlEntities_(str) + } + } + return str +}; +goog.string.unescapeEntitiesUsingDom_ = function(str) { + var el = goog.global["document"]["createElement"]("a"); + el["innerHTML"] = str; + if(el[goog.string.NORMALIZE_FN_]) { + el[goog.string.NORMALIZE_FN_]() + } + str = el["firstChild"]["nodeValue"]; + el["innerHTML"] = ""; + return str +}; +goog.string.unescapePureXmlEntities_ = function(str) { + return str.replace(/&([^;]+);/g, function(s, entity) { + switch(entity) { + case "amp": + return"&"; + case "lt": + return"<"; + case "gt": + return">"; + case "quot": + return'"'; + default: + if(entity.charAt(0) == "#") { + var n = Number("0" + entity.substr(1)); + if(!isNaN(n)) { + return String.fromCharCode(n) + } + } + return s + } + }) +}; +goog.string.NORMALIZE_FN_ = "normalize"; +goog.string.whitespaceEscape = function(str, opt_xml) { + return goog.string.newLineToBr(str.replace(/ /g, "  "), opt_xml) +}; +goog.string.stripQuotes = function(str, quoteChars) { + var length = quoteChars.length; + for(var i = 0;i < length;i++) { + var quoteChar = length == 1 ? quoteChars : quoteChars.charAt(i); + if(str.charAt(0) == quoteChar && str.charAt(str.length - 1) == quoteChar) { + return str.substring(1, str.length - 1) + } + } + return str +}; +goog.string.truncate = function(str, chars, opt_protectEscapedCharacters) { + if(opt_protectEscapedCharacters) { + str = goog.string.unescapeEntities(str) + } + if(str.length > chars) { + str = str.substring(0, chars - 3) + "..." + } + if(opt_protectEscapedCharacters) { + str = goog.string.htmlEscape(str) + } + return str +}; +goog.string.truncateMiddle = function(str, chars, opt_protectEscapedCharacters) { + if(opt_protectEscapedCharacters) { + str = goog.string.unescapeEntities(str) + } + if(str.length > chars) { + var half = Math.floor(chars / 2); + var endPos = str.length - half; + half += chars % 2; + str = str.substring(0, half) + "..." + str.substring(endPos) + } + if(opt_protectEscapedCharacters) { + str = goog.string.htmlEscape(str) + } + return str +}; +goog.string.specialEscapeChars_ = {"\u0000":"\\0", "\u0008":"\\b", "\u000c":"\\f", "\n":"\\n", "\r":"\\r", "\t":"\\t", "\u000b":"\\x0B", '"':'\\"', "\\":"\\\\"}; +goog.string.jsEscapeCache_ = {"'":"\\'"}; +goog.string.quote = function(s) { + s = String(s); + if(s.quote) { + return s.quote() + }else { + var sb = ['"']; + for(var i = 0;i < s.length;i++) { + var ch = s.charAt(i); + var cc = ch.charCodeAt(0); + sb[i + 1] = goog.string.specialEscapeChars_[ch] || (cc > 31 && cc < 127 ? ch : goog.string.escapeChar(ch)) + } + sb.push('"'); + return sb.join("") + } +}; +goog.string.escapeString = function(str) { + var sb = []; + for(var i = 0;i < str.length;i++) { + sb[i] = goog.string.escapeChar(str.charAt(i)) + } + return sb.join("") +}; +goog.string.escapeChar = function(c) { + if(c in goog.string.jsEscapeCache_) { + return goog.string.jsEscapeCache_[c] + } + if(c in goog.string.specialEscapeChars_) { + return goog.string.jsEscapeCache_[c] = goog.string.specialEscapeChars_[c] + } + var rv = c; + var cc = c.charCodeAt(0); + if(cc > 31 && cc < 127) { + rv = c + }else { + if(cc < 256) { + rv = "\\x"; + if(cc < 16 || cc > 256) { + rv += "0" + } + }else { + rv = "\\u"; + if(cc < 4096) { + rv += "0" + } + } + rv += cc.toString(16).toUpperCase() + } + return goog.string.jsEscapeCache_[c] = rv +}; +goog.string.toMap = function(s) { + var rv = {}; + for(var i = 0;i < s.length;i++) { + rv[s.charAt(i)] = true + } + return rv +}; +goog.string.contains = function(s, ss) { + return s.includes(ss) +}; +goog.string.removeAt = function(s, index, stringLength) { + var resultStr = s; + if(index >= 0 && index < s.length && stringLength > 0) { + resultStr = s.substr(0, index) + s.substr(index + stringLength, s.length - index - stringLength) + } + return resultStr +}; +goog.string.remove = function(s, ss) { + var re = new RegExp(goog.string.regExpEscape(ss), ""); + return s.replace(re, "") +}; +goog.string.removeAll = function(s, ss) { + var re = new RegExp(goog.string.regExpEscape(ss), "g"); + return s.replace(re, "") +}; +goog.string.regExpEscape = function(s) { + return String(s).replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g, "\\$1").replace(/\x08/g, "\\x08") +}; +goog.string.repeat = function(string, length) { + return(new Array(length + 1)).join(string) +}; +goog.string.padNumber = function(num, length, opt_precision) { + var s = goog.isDef(opt_precision) ? num.toFixed(opt_precision) : String(num); + var index = s.indexOf("."); + if(index == -1) { + index = s.length + } + return goog.string.repeat("0", Math.max(0, length - index)) + s +}; +goog.string.makeSafe = function(obj) { + return obj == null ? "" : String(obj) +}; +goog.string.buildString = function(var_args) { + return Array.prototype.join.call(arguments, "") +}; +goog.string.getRandomString = function() { + return Math.floor(Math.random() * 2147483648).toString(36) + (Math.floor(Math.random() * 2147483648) ^ goog.now()).toString(36) +}; +goog.string.compareVersions = function(version1, version2) { + var order = 0; + var v1Subs = goog.string.trim(String(version1)).split("."); + var v2Subs = goog.string.trim(String(version2)).split("."); + var subCount = Math.max(v1Subs.length, v2Subs.length); + for(var subIdx = 0;order == 0 && subIdx < subCount;subIdx++) { + var v1Sub = v1Subs[subIdx] || ""; + var v2Sub = v2Subs[subIdx] || ""; + var v1CompParser = new RegExp("(\\d*)(\\D*)", "g"); + var v2CompParser = new RegExp("(\\d*)(\\D*)", "g"); + do { + var v1Comp = v1CompParser.exec(v1Sub) || ["", "", ""]; + var v2Comp = v2CompParser.exec(v2Sub) || ["", "", ""]; + if(v1Comp[0].length == 0 && v2Comp[0].length == 0) { + break + } + var v1CompNum = v1Comp[1].length == 0 ? 0 : parseInt(v1Comp[1], 10); + var v2CompNum = v2Comp[1].length == 0 ? 0 : parseInt(v2Comp[1], 10); + order = goog.string.compareElements_(v1CompNum, v2CompNum) || goog.string.compareElements_(v1Comp[2].length == 0, v2Comp[2].length == 0) || goog.string.compareElements_(v1Comp[2], v2Comp[2]) + }while(order == 0) + } + return order +}; +goog.string.compareElements_ = function(left, right) { + if(left < right) { + return-1 + }else { + if(left > right) { + return 1 + } + } + return 0 +}; +goog.string.HASHCODE_MAX_ = 4294967296; +goog.string.hashCode = function(str) { + var result = 0; + for(var i = 0;i < str.length;++i) { + result = 31 * result + str.charCodeAt(i); + result %= goog.string.HASHCODE_MAX_ + } + return result +}; +goog.string.uniqueStringCounter_ = Math.random() * 2147483648 | 0; +goog.string.createUniqueString = function() { + return"goog_" + goog.string.uniqueStringCounter_++ +}; +goog.string.toNumber = function(str) { + var num = Number(str); + if(num == 0 && goog.string.isEmpty(str)) { + return NaN + } + return num +}; +goog.provide("goog.asserts"); +goog.provide("goog.asserts.AssertionError"); +goog.require("goog.debug.Error"); +goog.require("goog.string"); +goog.asserts.ENABLE_ASSERTS = goog.DEBUG; +goog.asserts.AssertionError = function(messagePattern, messageArgs) { + messageArgs.unshift(messagePattern); + goog.debug.Error.call(this, goog.string.subs.apply(null, messageArgs)); + messageArgs.shift(); + this.messagePattern = messagePattern +}; +goog.inherits(goog.asserts.AssertionError, goog.debug.Error); +goog.asserts.AssertionError.prototype.name = "AssertionError"; +goog.asserts.doAssertFailure_ = function(defaultMessage, defaultArgs, givenMessage, givenArgs) { + var message = "Assertion failed"; + if(givenMessage) { + message += ": " + givenMessage; + var args = givenArgs + }else { + if(defaultMessage) { + message += ": " + defaultMessage; + args = defaultArgs + } + } + throw new goog.asserts.AssertionError("" + message, args || []); +}; +goog.asserts.assert = function(condition, opt_message, var_args) { + if(goog.asserts.ENABLE_ASSERTS && !condition) { + goog.asserts.doAssertFailure_("", null, opt_message, Array.prototype.slice.call(arguments, 2)) + } + return condition +}; +goog.asserts.fail = function(opt_message, var_args) { + if(goog.asserts.ENABLE_ASSERTS) { + throw new goog.asserts.AssertionError("Failure" + (opt_message ? ": " + opt_message : ""), Array.prototype.slice.call(arguments, 1)); + } +}; +goog.asserts.assertNumber = function(value, opt_message, var_args) { + if(goog.asserts.ENABLE_ASSERTS && !goog.isNumber(value)) { + goog.asserts.doAssertFailure_("Expected number but got %s: %s.", [goog.typeOf(value), value], opt_message, Array.prototype.slice.call(arguments, 2)) + } + return value +}; +goog.asserts.assertString = function(value, opt_message, var_args) { + if(goog.asserts.ENABLE_ASSERTS && !goog.isString(value)) { + goog.asserts.doAssertFailure_("Expected string but got %s: %s.", [goog.typeOf(value), value], opt_message, Array.prototype.slice.call(arguments, 2)) + } + return value +}; +goog.asserts.assertFunction = function(value, opt_message, var_args) { + if(goog.asserts.ENABLE_ASSERTS && !goog.isFunction(value)) { + goog.asserts.doAssertFailure_("Expected function but got %s: %s.", [goog.typeOf(value), value], opt_message, Array.prototype.slice.call(arguments, 2)) + } + return value +}; +goog.asserts.assertObject = function(value, opt_message, var_args) { + if(goog.asserts.ENABLE_ASSERTS && !goog.isObject(value)) { + goog.asserts.doAssertFailure_("Expected object but got %s: %s.", [goog.typeOf(value), value], opt_message, Array.prototype.slice.call(arguments, 2)) + } + return value +}; +goog.asserts.assertArray = function(value, opt_message, var_args) { + if(goog.asserts.ENABLE_ASSERTS && !goog.isArray(value)) { + goog.asserts.doAssertFailure_("Expected array but got %s: %s.", [goog.typeOf(value), value], opt_message, Array.prototype.slice.call(arguments, 2)) + } + return value +}; +goog.asserts.assertBoolean = function(value, opt_message, var_args) { + if(goog.asserts.ENABLE_ASSERTS && !goog.isBoolean(value)) { + goog.asserts.doAssertFailure_("Expected boolean but got %s: %s.", [goog.typeOf(value), value], opt_message, Array.prototype.slice.call(arguments, 2)) + } + return value +}; +goog.asserts.assertInstanceof = function(value, type, opt_message, var_args) { + if(goog.asserts.ENABLE_ASSERTS && !(value instanceof type)) { + goog.asserts.doAssertFailure_("instanceof check failed.", null, opt_message, Array.prototype.slice.call(arguments, 3)) + } +}; +goog.provide("goog.array"); +goog.require("goog.asserts"); +goog.array.ArrayLike; +goog.array.peek = function(array) { + return array[array.length - 1] +}; +goog.array.ARRAY_PROTOTYPE_ = Array.prototype; +goog.array.indexOf = goog.array.ARRAY_PROTOTYPE_.indexOf ? function(arr, obj, opt_fromIndex) { + goog.asserts.assert(arr.length != null); + return goog.array.ARRAY_PROTOTYPE_.indexOf.call(arr, obj, opt_fromIndex) +} : function(arr, obj, opt_fromIndex) { + var fromIndex = opt_fromIndex == null ? 0 : opt_fromIndex < 0 ? Math.max(0, arr.length + opt_fromIndex) : opt_fromIndex; + if(goog.isString(arr)) { + if(!goog.isString(obj) || obj.length != 1) { + return-1 + } + return arr.indexOf(obj, fromIndex) + } + for(var i = fromIndex;i < arr.length;i++) { + if(i in arr && arr[i] === obj) { + return i + } + } + return-1 +}; +goog.array.lastIndexOf = goog.array.ARRAY_PROTOTYPE_.lastIndexOf ? function(arr, obj, opt_fromIndex) { + goog.asserts.assert(arr.length != null); + var fromIndex = opt_fromIndex == null ? arr.length - 1 : opt_fromIndex; + return goog.array.ARRAY_PROTOTYPE_.lastIndexOf.call(arr, obj, fromIndex) +} : function(arr, obj, opt_fromIndex) { + var fromIndex = opt_fromIndex == null ? arr.length - 1 : opt_fromIndex; + if(fromIndex < 0) { + fromIndex = Math.max(0, arr.length + fromIndex) + } + if(goog.isString(arr)) { + if(!goog.isString(obj) || obj.length != 1) { + return-1 + } + return arr.lastIndexOf(obj, fromIndex) + } + for(var i = fromIndex;i >= 0;i--) { + if(i in arr && arr[i] === obj) { + return i + } + } + return-1 +}; +goog.array.forEach = goog.array.ARRAY_PROTOTYPE_.forEach ? function(arr, f, opt_obj) { + goog.asserts.assert(arr.length != null); + goog.array.ARRAY_PROTOTYPE_.forEach.call(arr, f, opt_obj) +} : function(arr, f, opt_obj) { + var l = arr.length; + var arr2 = goog.isString(arr) ? arr.split("") : arr; + for(var i = 0;i < l;i++) { + if(i in arr2) { + f.call(opt_obj, arr2[i], i, arr) + } + } +}; +goog.array.forEachRight = function(arr, f, opt_obj) { + var l = arr.length; + var arr2 = goog.isString(arr) ? arr.split("") : arr; + for(var i = l - 1;i >= 0;--i) { + if(i in arr2) { + f.call(opt_obj, arr2[i], i, arr) + } + } +}; +goog.array.filter = goog.array.ARRAY_PROTOTYPE_.filter ? function(arr, f, opt_obj) { + goog.asserts.assert(arr.length != null); + return goog.array.ARRAY_PROTOTYPE_.filter.call(arr, f, opt_obj) +} : function(arr, f, opt_obj) { + var l = arr.length; + var res = []; + var resLength = 0; + var arr2 = goog.isString(arr) ? arr.split("") : arr; + for(var i = 0;i < l;i++) { + if(i in arr2) { + var val = arr2[i]; + if(f.call(opt_obj, val, i, arr)) { + res[resLength++] = val + } + } + } + return res +}; +goog.array.map = goog.array.ARRAY_PROTOTYPE_.map ? function(arr, f, opt_obj) { + goog.asserts.assert(arr.length != null); + return goog.array.ARRAY_PROTOTYPE_.map.call(arr, f, opt_obj) +} : function(arr, f, opt_obj) { + var l = arr.length; + var res = new Array(l); + var arr2 = goog.isString(arr) ? arr.split("") : arr; + for(var i = 0;i < l;i++) { + if(i in arr2) { + res[i] = f.call(opt_obj, arr2[i], i, arr) + } + } + return res +}; +goog.array.reduce = function(arr, f, val, opt_obj) { + if(arr.reduce) { + if(opt_obj) { + return arr.reduce(goog.bind(f, opt_obj), val) + }else { + return arr.reduce(f, val) + } + } + var rval = val; + goog.array.forEach(arr, function(val, index) { + rval = f.call(opt_obj, rval, val, index, arr) + }); + return rval +}; +goog.array.reduceRight = function(arr, f, val, opt_obj) { + if(arr.reduceRight) { + if(opt_obj) { + return arr.reduceRight(goog.bind(f, opt_obj), val) + }else { + return arr.reduceRight(f, val) + } + } + var rval = val; + goog.array.forEachRight(arr, function(val, index) { + rval = f.call(opt_obj, rval, val, index, arr) + }); + return rval +}; +goog.array.some = goog.array.ARRAY_PROTOTYPE_.some ? function(arr, f, opt_obj) { + goog.asserts.assert(arr.length != null); + return goog.array.ARRAY_PROTOTYPE_.some.call(arr, f, opt_obj) +} : function(arr, f, opt_obj) { + var l = arr.length; + var arr2 = goog.isString(arr) ? arr.split("") : arr; + for(var i = 0;i < l;i++) { + if(i in arr2 && f.call(opt_obj, arr2[i], i, arr)) { + return true + } + } + return false +}; +goog.array.every = goog.array.ARRAY_PROTOTYPE_.every ? function(arr, f, opt_obj) { + goog.asserts.assert(arr.length != null); + return goog.array.ARRAY_PROTOTYPE_.every.call(arr, f, opt_obj) +} : function(arr, f, opt_obj) { + var l = arr.length; + var arr2 = goog.isString(arr) ? arr.split("") : arr; + for(var i = 0;i < l;i++) { + if(i in arr2 && !f.call(opt_obj, arr2[i], i, arr)) { + return false + } + } + return true +}; +goog.array.find = function(arr, f, opt_obj) { + var i = goog.array.findIndex(arr, f, opt_obj); + return i < 0 ? null : goog.isString(arr) ? arr.charAt(i) : arr[i] +}; +goog.array.findIndex = function(arr, f, opt_obj) { + var l = arr.length; + var arr2 = goog.isString(arr) ? arr.split("") : arr; + for(var i = 0;i < l;i++) { + if(i in arr2 && f.call(opt_obj, arr2[i], i, arr)) { + return i + } + } + return-1 +}; +goog.array.findRight = function(arr, f, opt_obj) { + var i = goog.array.findIndexRight(arr, f, opt_obj); + return i < 0 ? null : goog.isString(arr) ? arr.charAt(i) : arr[i] +}; +goog.array.findIndexRight = function(arr, f, opt_obj) { + var l = arr.length; + var arr2 = goog.isString(arr) ? arr.split("") : arr; + for(var i = l - 1;i >= 0;i--) { + if(i in arr2 && f.call(opt_obj, arr2[i], i, arr)) { + return i + } + } + return-1 +}; +goog.array.contains = function(arr, obj) { + return goog.array.includes(arr, obj) +}; +goog.array.isEmpty = function(arr) { + return arr.length == 0 +}; +goog.array.clear = function(arr) { + if(!goog.isArray(arr)) { + for(var i = arr.length - 1;i >= 0;i--) { + delete arr[i] + } + } + arr.length = 0 +}; +goog.array.insert = function(arr, obj) { + if(!goog.array.contains(arr, obj)) { + arr.push(obj) + } +}; +goog.array.insertAt = function(arr, obj, opt_i) { + goog.array.splice(arr, opt_i, 0, obj) +}; +goog.array.insertArrayAt = function(arr, elementsToAdd, opt_i) { + goog.partial(goog.array.splice, arr, opt_i, 0).apply(null, elementsToAdd) +}; +goog.array.insertBefore = function(arr, obj, opt_obj2) { + var i; + if(arguments.length == 2 || (i = goog.array.indexOf(arr, opt_obj2)) < 0) { + arr.push(obj) + }else { + goog.array.insertAt(arr, obj, i) + } +}; +goog.array.remove = function(arr, obj) { + var i = goog.array.indexOf(arr, obj); + var rv; + if(rv = i >= 0) { + goog.array.removeAt(arr, i) + } + return rv +}; +goog.array.removeAt = function(arr, i) { + goog.asserts.assert(arr.length != null); + return goog.array.ARRAY_PROTOTYPE_.splice.call(arr, i, 1).length == 1 +}; +goog.array.removeIf = function(arr, f, opt_obj) { + var i = goog.array.findIndex(arr, f, opt_obj); + if(i >= 0) { + goog.array.removeAt(arr, i); + return true + } + return false +}; +goog.array.concat = function(var_args) { + return goog.array.ARRAY_PROTOTYPE_.concat.apply(goog.array.ARRAY_PROTOTYPE_, arguments) +}; +goog.array.clone = function(arr) { + if(goog.isArray(arr)) { + return goog.array.concat(arr) + }else { + var rv = []; + for(var i = 0, len = arr.length;i < len;i++) { + rv[i] = arr[i] + } + return rv + } +}; +goog.array.toArray = function(object) { + if(goog.isArray(object)) { + return goog.array.concat(object) + } + return goog.array.clone(object) +}; +goog.array.extend = function(arr1, var_args) { + for(var i = 1;i < arguments.length;i++) { + var arr2 = arguments[i]; + var isArrayLike; + if(goog.isArray(arr2) || (isArrayLike = goog.isArrayLike(arr2)) && arr2.hasOwnProperty("callee")) { + arr1.push.apply(arr1, arr2) + }else { + if(isArrayLike) { + var len1 = arr1.length; + var len2 = arr2.length; + for(var j = 0;j < len2;j++) { + arr1[len1 + j] = arr2[j] + } + }else { + arr1.push(arr2) + } + } + } +}; +goog.array.splice = function(arr, index, howMany, var_args) { + goog.asserts.assert(arr.length != null); + return goog.array.ARRAY_PROTOTYPE_.splice.apply(arr, goog.array.slice(arguments, 1)) +}; +goog.array.slice = function(arr, start, opt_end) { + goog.asserts.assert(arr.length != null); + if(arguments.length <= 2) { + return goog.array.ARRAY_PROTOTYPE_.slice.call(arr, start) + }else { + return goog.array.ARRAY_PROTOTYPE_.slice.call(arr, start, opt_end) + } +}; +goog.array.removeDuplicates = function(arr, opt_rv) { + var rv = opt_rv || arr; + var seen = {}, cursorInsert = 0, cursorRead = 0; + while(cursorRead < arr.length) { + var current = arr[cursorRead++]; + var uid = goog.isObject(current) ? goog.getUid(current) : current; + if(!Object.prototype.hasOwnProperty.call(seen, uid)) { + seen[uid] = true; + rv[cursorInsert++] = current + } + } + rv.length = cursorInsert +}; +goog.array.binarySearch = function(arr, target, opt_compareFn) { + return goog.array.binarySearch_(arr, opt_compareFn || goog.array.defaultCompare, false, target) +}; +goog.array.binarySelect = function(arr, evaluator, opt_obj) { + return goog.array.binarySearch_(arr, evaluator, true, undefined, opt_obj) +}; +goog.array.binarySearch_ = function(arr, compareFn, isEvaluator, opt_target, opt_selfObj) { + var left = 0; + var right = arr.length; + var found; + while(left < right) { + var middle = left + right >> 1; + var compareResult; + if(isEvaluator) { + compareResult = compareFn.call(opt_selfObj, arr[middle], middle, arr) + }else { + compareResult = compareFn(opt_target, arr[middle]) + } + if(compareResult > 0) { + left = middle + 1 + }else { + right = middle; + found = !compareResult + } + } + return found ? left : ~left +}; +goog.array.sort = function(arr, opt_compareFn) { + goog.asserts.assert(arr.length != null); + goog.array.ARRAY_PROTOTYPE_.sort.call(arr, opt_compareFn || goog.array.defaultCompare) +}; +goog.array.stableSort = function(arr, opt_compareFn) { + for(var i = 0;i < arr.length;i++) { + arr[i] = {index:i, value:arr[i]} + } + var valueCompareFn = opt_compareFn || goog.array.defaultCompare; + function stableCompareFn(obj1, obj2) { + return valueCompareFn(obj1.value, obj2.value) || obj1.index - obj2.index + } + goog.array.sort(arr, stableCompareFn); + for(var i = 0;i < arr.length;i++) { + arr[i] = arr[i].value + } +}; +goog.array.sortObjectsByKey = function(arr, key, opt_compareFn) { + var compare = opt_compareFn || goog.array.defaultCompare; + goog.array.sort(arr, function(a, b) { + return compare(a[key], b[key]) + }) +}; +goog.array.equals = function(arr1, arr2, opt_equalsFn) { + if(!goog.isArrayLike(arr1) || !goog.isArrayLike(arr2) || arr1.length != arr2.length) { + return false + } + var l = arr1.length; + var equalsFn = opt_equalsFn || goog.array.defaultCompareEquality; + for(var i = 0;i < l;i++) { + if(!equalsFn(arr1[i], arr2[i])) { + return false + } + } + return true +}; +goog.array.compare = function(arr1, arr2, opt_equalsFn) { + return goog.array.equals(arr1, arr2, opt_equalsFn) +}; +goog.array.defaultCompare = function(a, b) { + return a > b ? 1 : a < b ? -1 : 0 +}; +goog.array.defaultCompareEquality = function(a, b) { + return a === b +}; +goog.array.binaryInsert = function(array, value, opt_compareFn) { + var index = goog.array.binarySearch(array, value, opt_compareFn); + if(index < 0) { + goog.array.insertAt(array, value, -(index + 1)); + return true + } + return false +}; +goog.array.binaryRemove = function(array, value, opt_compareFn) { + var index = goog.array.binarySearch(array, value, opt_compareFn); + return index >= 0 ? goog.array.removeAt(array, index) : false +}; +goog.array.bucket = function(array, sorter) { + var buckets = {}; + for(var i = 0;i < array.length;i++) { + var value = array[i]; + var key = sorter(value, i, array); + if(goog.isDef(key)) { + var bucket = buckets[key] || (buckets[key] = []); + bucket.push(value) + } + } + return buckets +}; +goog.array.repeat = function(value, n) { + var array = []; + for(var i = 0;i < n;i++) { + array[i] = value + } + return array +}; +goog.array.flatten = function(var_args) { + var result = []; + for(var i = 0;i < arguments.length;i++) { + var element = arguments[i]; + if(goog.isArray(element)) { + result.push.apply(result, goog.array.flatten.apply(null, element)) + }else { + result.push(element) + } + } + return result +}; +goog.array.rotate = function(array, n) { + goog.asserts.assert(array.length != null); + if(array.length) { + n %= array.length; + if(n > 0) { + goog.array.ARRAY_PROTOTYPE_.unshift.apply(array, array.splice(-n, n)) + }else { + if(n < 0) { + goog.array.ARRAY_PROTOTYPE_.push.apply(array, array.splice(0, -n)) + } + } + } + return array +}; +goog.array.zip = function(var_args) { + if(!arguments.length) { + return[] + } + var result = []; + for(var i = 0;true;i++) { + var value = []; + for(var j = 0;j < arguments.length;j++) { + var arr = arguments[j]; + if(i >= arr.length) { + return result + } + value.push(arr[i]) + } + result.push(value) + } +}; +goog.provide("goog.userAgent"); +goog.require("goog.string"); +goog.userAgent.ASSUME_IE = false; +goog.userAgent.ASSUME_GECKO = false; +goog.userAgent.ASSUME_WEBKIT = false; +goog.userAgent.ASSUME_MOBILE_WEBKIT = false; +goog.userAgent.ASSUME_OPERA = false; +goog.userAgent.BROWSER_KNOWN_ = goog.userAgent.ASSUME_IE || goog.userAgent.ASSUME_GECKO || goog.userAgent.ASSUME_MOBILE_WEBKIT || goog.userAgent.ASSUME_WEBKIT || goog.userAgent.ASSUME_OPERA; +goog.userAgent.getUserAgentString = function() { + return goog.global["navigator"] ? goog.global["navigator"].userAgent : null +}; +goog.userAgent.getNavigator = function() { + return goog.global["navigator"] +}; +goog.userAgent.init_ = function() { + goog.userAgent.detectedOpera_ = false; + goog.userAgent.detectedIe_ = false; + goog.userAgent.detectedWebkit_ = false; + goog.userAgent.detectedMobile_ = false; + goog.userAgent.detectedGecko_ = false; + var ua; + if(!goog.userAgent.BROWSER_KNOWN_ && (ua = goog.userAgent.getUserAgentString())) { + var navigator = goog.userAgent.getNavigator(); + goog.userAgent.detectedOpera_ = ua.indexOf("Opera") == 0; + goog.userAgent.detectedIe_ = !goog.userAgent.detectedOpera_ && ua.includes("MSIE"); + goog.userAgent.detectedWebkit_ = !goog.userAgent.detectedOpera_ && ua.includes("WebKit"); + goog.userAgent.detectedMobile_ = goog.userAgent.detectedWebkit_ && ua.includes("Mobile"); + goog.userAgent.detectedGecko_ = !goog.userAgent.detectedOpera_ && !goog.userAgent.detectedWebkit_ && navigator.product == "Gecko" + } +}; +if(!goog.userAgent.BROWSER_KNOWN_) { + goog.userAgent.init_() +} +goog.userAgent.OPERA = goog.userAgent.BROWSER_KNOWN_ ? goog.userAgent.ASSUME_OPERA : goog.userAgent.detectedOpera_; +goog.userAgent.IE = goog.userAgent.BROWSER_KNOWN_ ? goog.userAgent.ASSUME_IE : goog.userAgent.detectedIe_; +goog.userAgent.GECKO = goog.userAgent.BROWSER_KNOWN_ ? goog.userAgent.ASSUME_GECKO : goog.userAgent.detectedGecko_; +goog.userAgent.WEBKIT = goog.userAgent.BROWSER_KNOWN_ ? goog.userAgent.ASSUME_WEBKIT || goog.userAgent.ASSUME_MOBILE_WEBKIT : goog.userAgent.detectedWebkit_; +goog.userAgent.MOBILE = goog.userAgent.ASSUME_MOBILE_WEBKIT || goog.userAgent.detectedMobile_; +goog.userAgent.SAFARI = goog.userAgent.WEBKIT; +goog.userAgent.determinePlatform_ = function() { + var navigator = goog.userAgent.getNavigator(); + return navigator && navigator.platform || "" +}; +goog.userAgent.PLATFORM = goog.userAgent.determinePlatform_(); +goog.userAgent.ASSUME_MAC = false; +goog.userAgent.ASSUME_WINDOWS = false; +goog.userAgent.ASSUME_LINUX = false; +goog.userAgent.ASSUME_X11 = false; +goog.userAgent.PLATFORM_KNOWN_ = goog.userAgent.ASSUME_MAC || goog.userAgent.ASSUME_WINDOWS || goog.userAgent.ASSUME_LINUX || goog.userAgent.ASSUME_X11; +goog.userAgent.initPlatform_ = function() { + goog.userAgent.detectedMac_ = goog.string.contains(goog.userAgent.PLATFORM, "Mac"); + goog.userAgent.detectedWindows_ = goog.string.contains(goog.userAgent.PLATFORM, "Win"); + goog.userAgent.detectedLinux_ = goog.string.contains(goog.userAgent.PLATFORM, "Linux"); + goog.userAgent.detectedX11_ = !!goog.userAgent.getNavigator() && goog.string.contains(goog.userAgent.getNavigator()["appVersion"] || "", "X11") +}; +if(!goog.userAgent.PLATFORM_KNOWN_) { + goog.userAgent.initPlatform_() +} +goog.userAgent.MAC = goog.userAgent.PLATFORM_KNOWN_ ? goog.userAgent.ASSUME_MAC : goog.userAgent.detectedMac_; +goog.userAgent.WINDOWS = goog.userAgent.PLATFORM_KNOWN_ ? goog.userAgent.ASSUME_WINDOWS : goog.userAgent.detectedWindows_; +goog.userAgent.LINUX = goog.userAgent.PLATFORM_KNOWN_ ? goog.userAgent.ASSUME_LINUX : goog.userAgent.detectedLinux_; +goog.userAgent.X11 = goog.userAgent.PLATFORM_KNOWN_ ? goog.userAgent.ASSUME_X11 : goog.userAgent.detectedX11_; +goog.userAgent.determineVersion_ = function() { + var version = "", re; + if(goog.userAgent.OPERA && goog.global["opera"]) { + var operaVersion = goog.global["opera"].version; + version = typeof operaVersion == "function" ? operaVersion() : operaVersion + }else { + if(goog.userAgent.GECKO) { + re = /rv\:([^\);]+)(\)|;)/ + }else { + if(goog.userAgent.IE) { + re = /MSIE\s+([^\);]+)(\)|;)/ + }else { + if(goog.userAgent.WEBKIT) { + re = /WebKit\/(\S+)/ + } + } + } + if(re) { + var arr = re.exec(goog.userAgent.getUserAgentString()); + version = arr ? arr[1] : "" + } + } + if(goog.userAgent.IE) { + var docMode = goog.userAgent.getDocumentMode_(); + if(docMode > parseFloat(version)) { + return String(docMode) + } + } + return version +}; +goog.userAgent.getDocumentMode_ = function() { + var doc = goog.global["document"]; + return doc ? doc["documentMode"] : undefined +}; +goog.userAgent.VERSION = goog.userAgent.determineVersion_(); +goog.userAgent.compare = function(v1, v2) { + return goog.string.compareVersions(v1, v2) +}; +goog.userAgent.isVersionCache_ = {}; +goog.userAgent.isVersion = function(version) { + return goog.userAgent.isVersionCache_[version] || (goog.userAgent.isVersionCache_[version] = goog.string.compareVersions(goog.userAgent.VERSION, version) >= 0) +}; +goog.provide("goog.dom.BrowserFeature"); +goog.require("goog.userAgent"); +goog.dom.BrowserFeature = { + CAN_ADD_NAME_OR_TYPE_ATTRIBUTES: !goog.userAgent.IE || goog.userAgent.isVersion("9"), + CAN_USE_INNER_TEXT: goog.userAgent.IE && !goog.userAgent.isVersion("9"), + INNER_HTML_NEEDS_SCOPED_ELEMENT: goog.userAgent.IE +}; +goog.provide("goog.dom.TagName"); +goog.dom.TagName = {A:"A", ABBR:"ABBR", ACRONYM:"ACRONYM", ADDRESS:"ADDRESS", APPLET:"APPLET", AREA:"AREA", B:"B", BASE:"BASE", BASEFONT:"BASEFONT", BDO:"BDO", BIG:"BIG", BLOCKQUOTE:"BLOCKQUOTE", BODY:"BODY", BR:"BR", BUTTON:"BUTTON", CANVAS:"CANVAS", CAPTION:"CAPTION", CENTER:"CENTER", CITE:"CITE", CODE:"CODE", COL:"COL", COLGROUP:"COLGROUP", DD:"DD", DEL:"DEL", DFN:"DFN", DIR:"DIR", DIV:"DIV", DL:"DL", DT:"DT", EM:"EM", FIELDSET:"FIELDSET", FONT:"FONT", FORM:"FORM", FRAME:"FRAME", FRAMESET:"FRAMESET", +H1:"H1", H2:"H2", H3:"H3", H4:"H4", H5:"H5", H6:"H6", HEAD:"HEAD", HR:"HR", HTML:"HTML", I:"I", IFRAME:"IFRAME", IMG:"IMG", INPUT:"INPUT", INS:"INS", ISINDEX:"ISINDEX", KBD:"KBD", LABEL:"LABEL", LEGEND:"LEGEND", LI:"LI", LINK:"LINK", MAP:"MAP", MENU:"MENU", META:"META", NOFRAMES:"NOFRAMES", NOSCRIPT:"NOSCRIPT", OBJECT:"OBJECT", OL:"OL", OPTGROUP:"OPTGROUP", OPTION:"OPTION", P:"P", PARAM:"PARAM", PRE:"PRE", Q:"Q", S:"S", SAMP:"SAMP", SCRIPT:"SCRIPT", SELECT:"SELECT", SMALL:"SMALL", SPAN:"SPAN", STRIKE:"STRIKE", +STRONG:"STRONG", STYLE:"STYLE", SUB:"SUB", SUP:"SUP", TABLE:"TABLE", TBODY:"TBODY", TD:"TD", TEXTAREA:"TEXTAREA", TFOOT:"TFOOT", TH:"TH", THEAD:"THEAD", TITLE:"TITLE", TR:"TR", TT:"TT", U:"U", UL:"UL", VAR:"VAR"}; +goog.provide("goog.dom.classes"); +goog.require("goog.array"); +goog.dom.classes.set = function(element, className) { + element.className = className +}; +goog.dom.classes.get = function(element) { + var className = element.className; + return className && typeof className.split == "function" ? className.split(/\s+/) : [] +}; +goog.dom.classes.add = function(element, var_args) { + var classes = goog.dom.classes.get(element); + var args = goog.array.slice(arguments, 1); + var b = goog.dom.classes.add_(classes, args); + element.className = classes.join(" "); + return b +}; +goog.dom.classes.remove = function(element, var_args) { + var classes = goog.dom.classes.get(element); + var args = goog.array.slice(arguments, 1); + var b = goog.dom.classes.remove_(classes, args); + element.className = classes.join(" "); + return b +}; +goog.dom.classes.add_ = function(classes, args) { + var rv = 0; + for(var i = 0;i < args.length;i++) { + if(!goog.array.contains(classes, args[i])) { + classes.push(args[i]); + rv++ + } + } + return rv == args.length +}; +goog.dom.classes.remove_ = function(classes, args) { + var rv = 0; + for(var i = 0;i < classes.length;i++) { + if(goog.array.contains(args, classes[i])) { + goog.array.splice(classes, i--, 1); + rv++ + } + } + return rv == args.length +}; +goog.dom.classes.swap = function(element, fromClass, toClass) { + var classes = goog.dom.classes.get(element); + var removed = false; + for(var i = 0;i < classes.length;i++) { + if(classes[i] == fromClass) { + goog.array.splice(classes, i--, 1); + removed = true + } + } + if(removed) { + classes.push(toClass); + element.className = classes.join(" ") + } + return removed +}; +goog.dom.classes.addRemove = function(element, classesToRemove, classesToAdd) { + var classes = goog.dom.classes.get(element); + if(goog.isString(classesToRemove)) { + goog.array.remove(classes, classesToRemove) + }else { + if(goog.isArray(classesToRemove)) { + goog.dom.classes.remove_(classes, classesToRemove) + } + } + if(goog.isString(classesToAdd) && !goog.array.contains(classes, classesToAdd)) { + classes.push(classesToAdd) + }else { + if(goog.isArray(classesToAdd)) { + goog.dom.classes.add_(classes, classesToAdd) + } + } + element.className = classes.join(" ") +}; +goog.dom.classes.has = function(element, className) { + return goog.array.contains(goog.dom.classes.get(element), className) +}; +goog.dom.classes.enable = function(element, className, enabled) { + if(enabled) { + goog.dom.classes.add(element, className) + }else { + goog.dom.classes.remove(element, className) + } +}; +goog.dom.classes.toggle = function(element, className) { + var add = !goog.dom.classes.has(element, className); + goog.dom.classes.enable(element, className, add); + return add +}; +goog.provide("goog.math.Coordinate"); +goog.math.Coordinate = function(opt_x, opt_y) { + this.x = goog.isDef(opt_x) ? opt_x : 0; + this.y = goog.isDef(opt_y) ? opt_y : 0 +}; +goog.math.Coordinate.prototype.clone = function() { + return new goog.math.Coordinate(this.x, this.y) +}; +if(goog.DEBUG) { + goog.math.Coordinate.prototype.toString = function() { + return"(" + this.x + ", " + this.y + ")" + } +} +goog.math.Coordinate.equals = function(a, b) { + if(a == b) { + return true + } + if(!a || !b) { + return false + } + return a.x == b.x && a.y == b.y +}; +goog.math.Coordinate.distance = function(a, b) { + var dx = a.x - b.x; + var dy = a.y - b.y; + return Math.sqrt(dx * dx + dy * dy) +}; +goog.math.Coordinate.squaredDistance = function(a, b) { + var dx = a.x - b.x; + var dy = a.y - b.y; + return dx * dx + dy * dy +}; +goog.math.Coordinate.difference = function(a, b) { + return new goog.math.Coordinate(a.x - b.x, a.y - b.y) +}; +goog.math.Coordinate.sum = function(a, b) { + return new goog.math.Coordinate(a.x + b.x, a.y + b.y) +}; +goog.provide("goog.math.Size"); +goog.math.Size = function(width, height) { + this.width = width; + this.height = height +}; +goog.math.Size.equals = function(a, b) { + if(a == b) { + return true + } + if(!a || !b) { + return false + } + return a.width == b.width && a.height == b.height +}; +goog.math.Size.prototype.clone = function() { + return new goog.math.Size(this.width, this.height) +}; +if(goog.DEBUG) { + goog.math.Size.prototype.toString = function() { + return"(" + this.width + " x " + this.height + ")" + } +} +goog.math.Size.prototype.getLongest = function() { + return Math.max(this.width, this.height) +}; +goog.math.Size.prototype.getShortest = function() { + return Math.min(this.width, this.height) +}; +goog.math.Size.prototype.area = function() { + return this.width * this.height +}; +goog.math.Size.prototype.perimeter = function() { + return(this.width + this.height) * 2 +}; +goog.math.Size.prototype.aspectRatio = function() { + return this.width / this.height +}; +goog.math.Size.prototype.isEmpty = function() { + return!this.area() +}; +goog.math.Size.prototype.ceil = function() { + this.width = Math.ceil(this.width); + this.height = Math.ceil(this.height); + return this +}; +goog.math.Size.prototype.fitsInside = function(target) { + return this.width <= target.width && this.height <= target.height +}; +goog.math.Size.prototype.floor = function() { + this.width = Math.floor(this.width); + this.height = Math.floor(this.height); + return this +}; +goog.math.Size.prototype.round = function() { + this.width = Math.round(this.width); + this.height = Math.round(this.height); + return this +}; +goog.math.Size.prototype.scale = function(s) { + this.width *= s; + this.height *= s; + return this +}; +goog.math.Size.prototype.scaleToFit = function(target) { + var s = this.aspectRatio() > target.aspectRatio() ? target.width / this.width : target.height / this.height; + return this.scale(s) +}; +goog.provide("goog.object"); +goog.object.forEach = function(obj, f, opt_obj) { + for(var key in obj) { + f.call(opt_obj, obj[key], key, obj) + } +}; +goog.object.filter = function(obj, f, opt_obj) { + var res = {}; + for(var key in obj) { + if(f.call(opt_obj, obj[key], key, obj)) { + res[key] = obj[key] + } + } + return res +}; +goog.object.map = function(obj, f, opt_obj) { + var res = {}; + for(var key in obj) { + res[key] = f.call(opt_obj, obj[key], key, obj) + } + return res +}; +goog.object.some = function(obj, f, opt_obj) { + for(var key in obj) { + if(f.call(opt_obj, obj[key], key, obj)) { + return true + } + } + return false +}; +goog.object.every = function(obj, f, opt_obj) { + for(var key in obj) { + if(!f.call(opt_obj, obj[key], key, obj)) { + return false + } + } + return true +}; +goog.object.getCount = function(obj) { + var rv = 0; + for(var key in obj) { + rv++ + } + return rv +}; +goog.object.getAnyKey = function(obj) { + for(var key in obj) { + return key + } +}; +goog.object.getAnyValue = function(obj) { + for(var key in obj) { + return obj[key] + } +}; +goog.object.contains = function(obj, val) { + return goog.object.containsValue(obj, val) +}; +goog.object.getValues = function(obj) { + var res = []; + var i = 0; + for(var key in obj) { + res[i++] = obj[key] + } + return res +}; +goog.object.getKeys = function(obj) { + var res = []; + var i = 0; + for(var key in obj) { + res[i++] = key + } + return res +}; +goog.object.containsKey = function(obj, key) { + return key in obj +}; +goog.object.containsValue = function(obj, val) { + for(var key in obj) { + if(obj[key] == val) { + return true + } + } + return false +}; +goog.object.findKey = function(obj, f, opt_this) { + for(var key in obj) { + if(f.call(opt_this, obj[key], key, obj)) { + return key + } + } + return undefined +}; +goog.object.findValue = function(obj, f, opt_this) { + var key = goog.object.findKey(obj, f, opt_this); + return key && obj[key] +}; +goog.object.isEmpty = function(obj) { + for(var key in obj) { + return false + } + return true +}; +goog.object.clear = function(obj) { + var keys = goog.object.getKeys(obj); + for(var i = keys.length - 1;i >= 0;i--) { + goog.object.remove(obj, keys[i]) + } +}; +goog.object.remove = function(obj, key) { + var rv; + if(rv = key in obj) { + delete obj[key] + } + return rv +}; +goog.object.add = function(obj, key, val) { + if(key in obj) { + throw Error('The object already contains the key "' + key + '"'); + } + goog.object.set(obj, key, val) +}; +goog.object.get = function(obj, key, opt_val) { + if(key in obj) { + return obj[key] + } + return opt_val +}; +goog.object.set = function(obj, key, value) { + obj[key] = value +}; +goog.object.setIfUndefined = function(obj, key, value) { + return key in obj ? obj[key] : obj[key] = value +}; +goog.object.clone = function(obj) { + var res = {}; + for(var key in obj) { + res[key] = obj[key] + } + return res +}; +goog.object.transpose = function(obj) { + var transposed = {}; + for(var key in obj) { + transposed[obj[key]] = key + } + return transposed +}; +goog.object.PROTOTYPE_FIELDS_ = ["constructor", "hasOwnProperty", "isPrototypeOf", "propertyIsEnumerable", "toLocaleString", "toString", "valueOf"]; +goog.object.extend = function(target, var_args) { + var key, source; + for(var i = 1;i < arguments.length;i++) { + source = arguments[i]; + for(key in source) { + target[key] = source[key] + } + for(var j = 0;j < goog.object.PROTOTYPE_FIELDS_.length;j++) { + key = goog.object.PROTOTYPE_FIELDS_[j]; + if(Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key] + } + } + } +}; +goog.object.create = function(var_args) { + var argLength = arguments.length; + if(argLength == 1 && goog.isArray(arguments[0])) { + return goog.object.create.apply(null, arguments[0]) + } + if(argLength % 2) { + throw Error("Uneven number of arguments"); + } + var rv = {}; + for(var i = 0;i < argLength;i += 2) { + rv[arguments[i]] = arguments[i + 1] + } + return rv +}; +goog.object.createSet = function(var_args) { + var argLength = arguments.length; + if(argLength == 1 && goog.isArray(arguments[0])) { + return goog.object.createSet.apply(null, arguments[0]) + } + var rv = {}; + for(var i = 0;i < argLength;i++) { + rv[arguments[i]] = true + } + return rv +}; +goog.provide("goog.dom"); +goog.provide("goog.dom.DomHelper"); +goog.provide("goog.dom.NodeType"); +goog.require("goog.array"); +goog.require("goog.dom.BrowserFeature"); +goog.require("goog.dom.TagName"); +goog.require("goog.dom.classes"); +goog.require("goog.math.Coordinate"); +goog.require("goog.math.Size"); +goog.require("goog.object"); +goog.require("goog.string"); +goog.require("goog.userAgent"); +goog.dom.ASSUME_QUIRKS_MODE = false; +goog.dom.ASSUME_STANDARDS_MODE = false; +goog.dom.COMPAT_MODE_KNOWN_ = goog.dom.ASSUME_QUIRKS_MODE || goog.dom.ASSUME_STANDARDS_MODE; +goog.dom.NodeType = {ELEMENT:1, ATTRIBUTE:2, TEXT:3, CDATA_SECTION:4, ENTITY_REFERENCE:5, ENTITY:6, PROCESSING_INSTRUCTION:7, COMMENT:8, DOCUMENT:9, DOCUMENT_TYPE:10, DOCUMENT_FRAGMENT:11, NOTATION:12}; +goog.dom.getDomHelper = function(opt_element) { + return opt_element ? new goog.dom.DomHelper(goog.dom.getOwnerDocument(opt_element)) : goog.dom.defaultDomHelper_ || (goog.dom.defaultDomHelper_ = new goog.dom.DomHelper) +}; +goog.dom.defaultDomHelper_; +goog.dom.getDocument = function() { + return document +}; +goog.dom.getElement = function(element) { + return goog.isString(element) ? document.getElementById(element) : element +}; +goog.dom.$ = goog.dom.getElement; +goog.dom.getElementsByTagNameAndClass = function(opt_tag, opt_class, opt_el) { + return goog.dom.getElementsByTagNameAndClass_(document, opt_tag, opt_class, opt_el) +}; +goog.dom.getElementsByClass = function(className, opt_el) { + var parent = opt_el || document; + if(goog.dom.canUseQuerySelector_(parent)) { + return parent.querySelectorAll("." + className) + }else { + if(parent.getElementsByClassName) { + return parent.getElementsByClassName(className) + } + } + return goog.dom.getElementsByTagNameAndClass_(document, "*", className, opt_el) +}; +goog.dom.getElementByClass = function(className, opt_el) { + var parent = opt_el || document; + var retVal = null; + if(goog.dom.canUseQuerySelector_(parent)) { + retVal = parent.querySelector("." + className) + }else { + retVal = goog.dom.getElementsByClass(className, opt_el)[0] + } + return retVal || null +}; +goog.dom.canUseQuerySelector_ = function(parent) { + return parent.querySelectorAll && parent.querySelector && (!goog.userAgent.WEBKIT || goog.dom.isCss1CompatMode_(document) || goog.userAgent.isVersion("528")) +}; +goog.dom.getElementsByTagNameAndClass_ = function(doc, opt_tag, opt_class, opt_el) { + var parent = opt_el || doc; + var tagName = opt_tag && opt_tag != "*" ? opt_tag.toUpperCase() : ""; + if(goog.dom.canUseQuerySelector_(parent) && (tagName || opt_class)) { + var query = tagName + (opt_class ? "." + opt_class : ""); + return parent.querySelectorAll(query) + } + if(opt_class && parent.getElementsByClassName) { + var els = parent.getElementsByClassName(opt_class); + if(tagName) { + var arrayLike = {}; + var len = 0; + for(var i = 0, el;el = els[i];i++) { + if(tagName == el.nodeName) { + arrayLike[len++] = el + } + } + arrayLike.length = len; + return arrayLike + }else { + return els + } + } + var els = parent.getElementsByTagName(tagName || "*"); + if(opt_class) { + var arrayLike = {}; + var len = 0; + for(var i = 0, el;el = els[i];i++) { + var className = el.className; + if(typeof className.split == "function" && goog.array.contains(className.split(/\s+/), opt_class)) { + arrayLike[len++] = el + } + } + arrayLike.length = len; + return arrayLike + }else { + return els + } +}; +goog.dom.$$ = goog.dom.getElementsByTagNameAndClass; +goog.dom.setProperties = function(element, properties) { + goog.object.forEach(properties, function(val, key) { + if(key == "style") { + element.style.cssText = val + }else { + if(key == "class") { + element.className = val + }else { + if(key == "for") { + element.htmlFor = val + }else { + if(key in goog.dom.DIRECT_ATTRIBUTE_MAP_) { + element.setAttribute(goog.dom.DIRECT_ATTRIBUTE_MAP_[key], val) + }else { + element[key] = val + } + } + } + } + }) +}; +goog.dom.DIRECT_ATTRIBUTE_MAP_ = {cellpadding:"cellPadding", cellspacing:"cellSpacing", colspan:"colSpan", rowspan:"rowSpan", valign:"vAlign", height:"height", width:"width", usemap:"useMap", frameborder:"frameBorder", type:"type"}; +goog.dom.getViewportSize = function(opt_window) { + return goog.dom.getViewportSize_(opt_window || window) +}; +goog.dom.getViewportSize_ = function(win) { + var doc = win.document; + if(goog.userAgent.WEBKIT && !goog.userAgent.isVersion("500") && !goog.userAgent.MOBILE) { + if(typeof win.innerHeight == "undefined") { + win = window + } + var innerHeight = win.innerHeight; + var scrollHeight = win.document.documentElement.scrollHeight; + if(win == win.top) { + if(scrollHeight < innerHeight) { + innerHeight -= 15 + } + } + return new goog.math.Size(win.innerWidth, innerHeight) + } + var readsFromDocumentElement = goog.dom.isCss1CompatMode_(doc); + if(goog.userAgent.OPERA && !goog.userAgent.isVersion("9.50")) { + readsFromDocumentElement = false + } + var el = readsFromDocumentElement ? doc.documentElement : doc.body; + return new goog.math.Size(el.clientWidth, el.clientHeight) +}; +goog.dom.getDocumentHeight = function() { + return goog.dom.getDocumentHeight_(window) +}; +goog.dom.getDocumentHeight_ = function(win) { + var doc = win.document; + var height = 0; + if(doc) { + var vh = goog.dom.getViewportSize_(win).height; + var body = doc.body; + var docEl = doc.documentElement; + if(goog.dom.isCss1CompatMode_(doc) && docEl.scrollHeight) { + height = docEl.scrollHeight != vh ? docEl.scrollHeight : docEl.offsetHeight + }else { + var sh = docEl.scrollHeight; + var oh = docEl.offsetHeight; + if(docEl.clientHeight != oh) { + sh = body.scrollHeight; + oh = body.offsetHeight + } + if(sh > vh) { + height = sh > oh ? sh : oh + }else { + height = sh < oh ? sh : oh + } + } + } + return height +}; +goog.dom.getPageScroll = function(opt_window) { + var win = opt_window || goog.global || window; + return goog.dom.getDomHelper(win.document).getDocumentScroll() +}; +goog.dom.getDocumentScroll = function() { + return goog.dom.getDocumentScroll_(document) +}; +goog.dom.getDocumentScroll_ = function(doc) { + var el = goog.dom.getDocumentScrollElement_(doc); + return new goog.math.Coordinate(el.scrollLeft, el.scrollTop) +}; +goog.dom.getDocumentScrollElement = function() { + return goog.dom.getDocumentScrollElement_(document) +}; +goog.dom.getDocumentScrollElement_ = function(doc) { + return!goog.userAgent.WEBKIT && goog.dom.isCss1CompatMode_(doc) ? doc.documentElement : doc.body +}; +goog.dom.getWindow = function(opt_doc) { + return opt_doc ? goog.dom.getWindow_(opt_doc) : window +}; +goog.dom.getWindow_ = function(doc) { + return doc.parentWindow || doc.defaultView +}; +goog.dom.createDom = function(tagName, opt_attributes, var_args) { + return goog.dom.createDom_(document, arguments) +}; +goog.dom.createDom_ = function(doc, args) { + var tagName = args[0]; + var attributes = args[1]; + if(!goog.dom.BrowserFeature.CAN_ADD_NAME_OR_TYPE_ATTRIBUTES && attributes && (attributes.name || attributes.type)) { + var tagNameArr = ["<", tagName]; + if(attributes.name) { + tagNameArr.push(' name="', goog.string.htmlEscape(attributes.name), '"') + } + if(attributes.type) { + tagNameArr.push(' type="', goog.string.htmlEscape(attributes.type), '"'); + var clone = {}; + goog.object.extend(clone, attributes); + attributes = clone; + delete attributes.type + } + tagNameArr.push(">"); + tagName = tagNameArr.join("") + } + var element = doc.createElement(tagName); + if(attributes) { + if(goog.isString(attributes)) { + element.className = attributes + }else { + if(goog.isArray(attributes)) { + goog.dom.classes.add.apply(null, [element].concat(attributes)) + }else { + goog.dom.setProperties(element, attributes) + } + } + } + if(args.length > 2) { + goog.dom.append_(doc, element, args, 2) + } + return element +}; +goog.dom.append_ = function(doc, parent, args, startIndex) { + function childHandler(child) { + if(child) { + parent.appendChild(goog.isString(child) ? doc.createTextNode(child) : child) + } + } + for(var i = startIndex;i < args.length;i++) { + var arg = args[i]; + if(goog.isArrayLike(arg) && !goog.dom.isNodeLike(arg)) { + goog.array.forEach(goog.dom.isNodeList(arg) ? goog.array.clone(arg) : arg, childHandler) + }else { + childHandler(arg) + } + } +}; +goog.dom.$dom = goog.dom.createDom; +goog.dom.createElement = function(name) { + return document.createElement(name) +}; +goog.dom.createTextNode = function(content) { + return document.createTextNode(content) +}; +goog.dom.createTable = function(rows, columns, opt_fillWithNbsp) { + return goog.dom.createTable_(document, rows, columns, !!opt_fillWithNbsp) +}; +goog.dom.createTable_ = function(doc, rows, columns, fillWithNbsp) { + var rowHtml = ["<tr>"]; + for(var i = 0;i < columns;i++) { + rowHtml.push(fillWithNbsp ? "<td> </td>" : "<td></td>") + } + rowHtml.push("</tr>"); + rowHtml = rowHtml.join(""); + var totalHtml = ["<table>"]; + for(i = 0;i < rows;i++) { + totalHtml.push(rowHtml) + } + totalHtml.push("</table>"); + var elem = doc.createElement(goog.dom.TagName.DIV); + elem.innerHTML = totalHtml.join(""); + return elem.firstChild.remove() +}; +goog.dom.htmlToDocumentFragment = function(htmlString) { + return goog.dom.htmlToDocumentFragment_(document, htmlString) +}; +goog.dom.htmlToDocumentFragment_ = function(doc, htmlString) { + var tempDiv = doc.createElement("div"); + if(goog.dom.BrowserFeature.INNER_HTML_NEEDS_SCOPED_ELEMENT) { + tempDiv.innerHTML = "<br>" + htmlString; + tempDiv.firstChild.remove() + }else { + tempDiv.innerHTML = htmlString + } + if(tempDiv.childNodes.length == 1) { + return tempDiv.firstChild.remove() + }else { + var fragment = doc.createDocumentFragment(); + while(tempDiv.firstChild) { + fragment.appendChild(tempDiv.firstChild) + } + return fragment + } +}; +goog.dom.getCompatMode = function() { + return goog.dom.isCss1CompatMode() ? "CSS1Compat" : "BackCompat" +}; +goog.dom.isCss1CompatMode = function() { + return goog.dom.isCss1CompatMode_(document) +}; +goog.dom.isCss1CompatMode_ = function(doc) { + if(goog.dom.COMPAT_MODE_KNOWN_) { + return goog.dom.ASSUME_STANDARDS_MODE + } + return doc.compatMode == "CSS1Compat" +}; +goog.dom.canHaveChildren = function(node) { + if(node.nodeType != goog.dom.NodeType.ELEMENT) { + return false + } + switch(node.tagName) { + case goog.dom.TagName.APPLET: + ; + case goog.dom.TagName.AREA: + ; + case goog.dom.TagName.BASE: + ; + case goog.dom.TagName.BR: + ; + case goog.dom.TagName.COL: + ; + case goog.dom.TagName.FRAME: + ; + case goog.dom.TagName.HR: + ; + case goog.dom.TagName.IMG: + ; + case goog.dom.TagName.INPUT: + ; + case goog.dom.TagName.IFRAME: + ; + case goog.dom.TagName.ISINDEX: + ; + case goog.dom.TagName.LINK: + ; + case goog.dom.TagName.NOFRAMES: + ; + case goog.dom.TagName.NOSCRIPT: + ; + case goog.dom.TagName.META: + ; + case goog.dom.TagName.OBJECT: + ; + case goog.dom.TagName.PARAM: + ; + case goog.dom.TagName.SCRIPT: + ; + case goog.dom.TagName.STYLE: + return false + } + return true +}; +goog.dom.appendChild = function(parent, child) { + parent.appendChild(child) +}; +goog.dom.append = function(parent, var_args) { + goog.dom.append_(goog.dom.getOwnerDocument(parent), parent, arguments, 1) +}; +goog.dom.removeChildren = function(node) { + var child; + while(child = node.firstChild) { + node.removeChild(child) + } +}; +goog.dom.insertSiblingBefore = function(newNode, refNode) { + if(refNode.parentNode) { + refNode.parentNode.insertBefore(newNode, refNode) + } +}; +goog.dom.insertSiblingAfter = function(newNode, refNode) { + if(refNode.parentNode) { + refNode.parentNode.insertBefore(newNode, refNode.nextSibling) + } +}; +goog.dom.removeNode = function(node) { + return node && node.parentNode ? node.remove() : null +}; +goog.dom.replaceNode = function(newNode, oldNode) { + var parent = oldNode.parentNode; + if(parent) { + parent.replaceChild(newNode, oldNode) + } +}; +goog.dom.flattenElement = function(element) { + var child, parent = element.parentNode; + if(parent && parent.nodeType != goog.dom.NodeType.DOCUMENT_FRAGMENT) { + if(element.removeNode) { + return element.removeNode(false) + }else { + while(child = element.firstChild) { + parent.insertBefore(child, element) + } + return goog.dom.removeNode(element) + } + } +}; +goog.dom.getFirstElementChild = function(node) { + return goog.dom.getNextElementNode_(node.firstChild, true) +}; +goog.dom.getLastElementChild = function(node) { + return goog.dom.getNextElementNode_(node.lastChild, false) +}; +goog.dom.getNextElementSibling = function(node) { + return goog.dom.getNextElementNode_(node.nextSibling, true) +}; +goog.dom.getPreviousElementSibling = function(node) { + return goog.dom.getNextElementNode_(node.previousSibling, false) +}; +goog.dom.getNextElementNode_ = function(node, forward) { + while(node && node.nodeType != goog.dom.NodeType.ELEMENT) { + node = forward ? node.nextSibling : node.previousSibling + } + return node +}; +goog.dom.getNextNode = function(node) { + if(!node) { + return null + } + if(node.firstChild) { + return node.firstChild + } + while(node && !node.nextSibling) { + node = node.parentNode + } + return node ? node.nextSibling : null +}; +goog.dom.getPreviousNode = function(node) { + if(!node) { + return null + } + if(!node.previousSibling) { + return node.parentNode + } + node = node.previousSibling; + while(node && node.lastChild) { + node = node.lastChild + } + return node +}; +goog.dom.isNodeLike = function(obj) { + return goog.isObject(obj) && obj.nodeType > 0 +}; +goog.dom.contains = function(parent, descendant) { + if(parent.contains && descendant.nodeType == goog.dom.NodeType.ELEMENT) { + return parent == descendant || parent.contains(descendant) + } + if(typeof parent.compareDocumentPosition != "undefined") { + return parent == descendant || Boolean(parent.compareDocumentPosition(descendant) & 16) + } + while(descendant && parent != descendant) { + descendant = descendant.parentNode + } + return descendant == parent +}; +goog.dom.compareNodeOrder = function(node1, node2) { + if(node1 == node2) { + return 0 + } + if(node1.compareDocumentPosition) { + return node1.compareDocumentPosition(node2) & 2 ? 1 : -1 + } + if("sourceIndex" in node1 || node1.parentNode && "sourceIndex" in node1.parentNode) { + var isElement1 = node1.nodeType == goog.dom.NodeType.ELEMENT; + var isElement2 = node2.nodeType == goog.dom.NodeType.ELEMENT; + if(isElement1 && isElement2) { + return node1.sourceIndex - node2.sourceIndex + }else { + var parent1 = node1.parentNode; + var parent2 = node2.parentNode; + if(parent1 == parent2) { + return goog.dom.compareSiblingOrder_(node1, node2) + } + if(!isElement1 && goog.dom.contains(parent1, node2)) { + return-1 * goog.dom.compareParentsDescendantNodeIe_(node1, node2) + } + if(!isElement2 && goog.dom.contains(parent2, node1)) { + return goog.dom.compareParentsDescendantNodeIe_(node2, node1) + } + return(isElement1 ? node1.sourceIndex : parent1.sourceIndex) - (isElement2 ? node2.sourceIndex : parent2.sourceIndex) + } + } + var doc = goog.dom.getOwnerDocument(node1); + var range1, range2; + range1 = doc.createRange(); + range1.selectNode(node1); + range1.collapse(true); + range2 = doc.createRange(); + range2.selectNode(node2); + range2.collapse(true); + return range1.compareBoundaryPoints(goog.global["Range"].START_TO_END, range2) +}; +goog.dom.compareParentsDescendantNodeIe_ = function(textNode, node) { + var parent = textNode.parentNode; + if(parent == node) { + return-1 + } + var sibling = node; + while(sibling.parentNode != parent) { + sibling = sibling.parentNode + } + return goog.dom.compareSiblingOrder_(sibling, textNode) +}; +goog.dom.compareSiblingOrder_ = function(node1, node2) { + var s = node2; + while(s = s.previousSibling) { + if(s == node1) { + return-1 + } + } + return 1 +}; +goog.dom.findCommonAncestor = function(var_args) { + var i, count = arguments.length; + if(!count) { + return null + }else { + if(count == 1) { + return arguments[0] + } + } + var paths = []; + var minLength = Infinity; + for(i = 0;i < count;i++) { + var ancestors = []; + var node = arguments[i]; + while(node) { + ancestors.unshift(node); + node = node.parentNode + } + paths.push(ancestors); + minLength = Math.min(minLength, ancestors.length) + } + var output = null; + for(i = 0;i < minLength;i++) { + var first = paths[0][i]; + for(var j = 1;j < count;j++) { + if(first != paths[j][i]) { + return output + } + } + output = first + } + return output +}; +goog.dom.getOwnerDocument = function(node) { + return node.nodeType == goog.dom.NodeType.DOCUMENT ? node : node.ownerDocument || node.document +}; +goog.dom.getFrameContentDocument = function(frame) { + var doc; + if(goog.userAgent.WEBKIT) { + doc = frame.document || frame.contentWindow.document + }else { + doc = frame.contentDocument || frame.contentWindow.document + } + return doc +}; +goog.dom.getFrameContentWindow = function(frame) { + return frame.contentWindow || goog.dom.getWindow_(goog.dom.getFrameContentDocument(frame)) +}; +goog.dom.setTextContent = function(element, text) { + if("textContent" in element) { + element.textContent = text + }else { + if(element.firstChild && element.firstChild.nodeType == goog.dom.NodeType.TEXT) { + while(element.lastChild != element.firstChild) { + element.removeChild(element.lastChild) + } + element.firstChild.data = text + }else { + goog.dom.removeChildren(element); + var doc = goog.dom.getOwnerDocument(element); + element.appendChild(doc.createTextNode(text)) + } + } +}; +goog.dom.getOuterHtml = function(element) { + if("outerHTML" in element) { + return element.outerHTML + }else { + var doc = goog.dom.getOwnerDocument(element); + var div = doc.createElement("div"); + div.appendChild(element.cloneNode(true)); + return div.innerHTML + } +}; +goog.dom.findNode = function(root, p) { + var rv = []; + var found = goog.dom.findNodes_(root, p, rv, true); + return found ? rv[0] : undefined +}; +goog.dom.findNodes = function(root, p) { + var rv = []; + goog.dom.findNodes_(root, p, rv, false); + return rv +}; +goog.dom.findNodes_ = function(root, p, rv, findOne) { + if(root != null) { + for(var i = 0, child;child = root.childNodes[i];i++) { + if(p(child)) { + rv.push(child); + if(findOne) { + return true + } + } + if(goog.dom.findNodes_(child, p, rv, findOne)) { + return true + } + } + } + return false +}; +goog.dom.TAGS_TO_IGNORE_ = {SCRIPT:1, STYLE:1, HEAD:1, IFRAME:1, OBJECT:1}; +goog.dom.PREDEFINED_TAG_VALUES_ = {IMG:" ", BR:"\n"}; +goog.dom.isFocusableTabIndex = function(element) { + var attrNode = element.getAttributeNode("tabindex"); + if(attrNode && attrNode.specified) { + var index = element.tabIndex; + return goog.isNumber(index) && index >= 0 + } + return false +}; +goog.dom.setFocusableTabIndex = function(element, enable) { + if(enable) { + element.tabIndex = 0 + }else { + element.removeAttribute("tabIndex") + } +}; +goog.dom.getTextContent = function(node) { + var textContent; + if(goog.dom.BrowserFeature.CAN_USE_INNER_TEXT && "innerText" in node) { + textContent = goog.string.canonicalizeNewlines(node.innerText) + }else { + var buf = []; + goog.dom.getTextContent_(node, buf, true); + textContent = buf.join("") + } + textContent = textContent.replace(/ \xAD /g, " ").replace(/\xAD/g, ""); + if(!goog.userAgent.IE) { + textContent = textContent.replace(/ +/g, " ") + } + if(textContent != " ") { + textContent = textContent.replace(/^\s*/, "") + } + return textContent +}; +goog.dom.getRawTextContent = function(node) { + var buf = []; + goog.dom.getTextContent_(node, buf, false); + return buf.join("") +}; +goog.dom.getTextContent_ = function(node, buf, normalizeWhitespace) { + if(node.nodeName in goog.dom.TAGS_TO_IGNORE_) { + }else { + if(node.nodeType == goog.dom.NodeType.TEXT) { + if(normalizeWhitespace) { + buf.push(String(node.nodeValue).replace(/(\r\n|\r|\n)/g, "")) + }else { + buf.push(node.nodeValue) + } + }else { + if(node.nodeName in goog.dom.PREDEFINED_TAG_VALUES_) { + buf.push(goog.dom.PREDEFINED_TAG_VALUES_[node.nodeName]) + }else { + var child = node.firstChild; + while(child) { + goog.dom.getTextContent_(child, buf, normalizeWhitespace); + child = child.nextSibling + } + } + } + } +}; +goog.dom.getNodeTextLength = function(node) { + return goog.dom.getTextContent(node).length +}; +goog.dom.getNodeTextOffset = function(node, opt_offsetParent) { + var root = opt_offsetParent || goog.dom.getOwnerDocument(node).body; + var buf = []; + while(node && node != root) { + var cur = node; + while(cur = cur.previousSibling) { + buf.unshift(goog.dom.getTextContent(cur)) + } + node = node.parentNode + } + return goog.string.trimLeft(buf.join("")).replace(/ +/g, " ").length +}; +goog.dom.getNodeAtOffset = function(parent, offset, opt_result) { + var stack = [parent], pos = 0, cur; + while(stack.length > 0 && pos < offset) { + cur = stack.pop(); + if(cur.nodeName in goog.dom.TAGS_TO_IGNORE_) { + }else { + if(cur.nodeType == goog.dom.NodeType.TEXT) { + var text = cur.nodeValue.replace(/(\r\n|\r|\n)/g, "").replace(/ +/g, " "); + pos += text.length + }else { + if(cur.nodeName in goog.dom.PREDEFINED_TAG_VALUES_) { + pos += goog.dom.PREDEFINED_TAG_VALUES_[cur.nodeName].length + }else { + for(var i = cur.childNodes.length - 1;i >= 0;i--) { + stack.push(cur.childNodes[i]) + } + } + } + } + } + if(goog.isObject(opt_result)) { + opt_result.remainder = cur ? cur.nodeValue.length + offset - pos - 1 : 0; + opt_result.node = cur + } + return cur +}; +goog.dom.isNodeList = function(val) { + if(val && typeof val.length == "number") { + if(goog.isObject(val)) { + return typeof val.item == "function" || typeof val.item == "string" + }else { + if(goog.isFunction(val)) { + return typeof val.item == "function" + } + } + } + return false +}; +goog.dom.getAncestorByTagNameAndClass = function(element, opt_tag, opt_class) { + var tagName = opt_tag ? opt_tag.toUpperCase() : null; + return goog.dom.getAncestor(element, function(node) { + return(!tagName || node.nodeName == tagName) && (!opt_class || goog.dom.classes.has(node, opt_class)) + }, true) +}; +goog.dom.getAncestor = function(element, matcher, opt_includeNode, opt_maxSearchSteps) { + if(!opt_includeNode) { + element = element.parentNode + } + var ignoreSearchSteps = opt_maxSearchSteps == null; + var steps = 0; + while(element && (ignoreSearchSteps || steps <= opt_maxSearchSteps)) { + if(matcher(element)) { + return element + } + element = element.parentNode; + steps++ + } + return null +}; +goog.dom.DomHelper = function(opt_document) { + this.document_ = opt_document || goog.global.document || document +}; +goog.dom.DomHelper.prototype.getDomHelper = goog.dom.getDomHelper; +goog.dom.DomHelper.prototype.setDocument = function(document) { + this.document_ = document +}; +goog.dom.DomHelper.prototype.getDocument = function() { + return this.document_ +}; +goog.dom.DomHelper.prototype.getElement = function(element) { + if(goog.isString(element)) { + return this.document_.getElementById(element) + }else { + return element + } +}; +goog.dom.DomHelper.prototype.$ = goog.dom.DomHelper.prototype.getElement; +goog.dom.DomHelper.prototype.getElementsByTagNameAndClass = function(opt_tag, opt_class, opt_el) { + return goog.dom.getElementsByTagNameAndClass_(this.document_, opt_tag, opt_class, opt_el) +}; +goog.dom.DomHelper.prototype.getElementsByClass = function(className, opt_el) { + var doc = opt_el || this.document_; + return goog.dom.getElementsByClass(className, doc) +}; +goog.dom.DomHelper.prototype.getElementByClass = function(className, opt_el) { + var doc = opt_el || this.document_; + return goog.dom.getElementByClass(className, doc) +}; +goog.dom.DomHelper.prototype.$$ = goog.dom.DomHelper.prototype.getElementsByTagNameAndClass; +goog.dom.DomHelper.prototype.setProperties = goog.dom.setProperties; +goog.dom.DomHelper.prototype.getViewportSize = function(opt_window) { + return goog.dom.getViewportSize(opt_window || this.getWindow()) +}; +goog.dom.DomHelper.prototype.getDocumentHeight = function() { + return goog.dom.getDocumentHeight_(this.getWindow()) +}; +goog.dom.Appendable; +goog.dom.DomHelper.prototype.createDom = function(tagName, opt_attributes, var_args) { + return goog.dom.createDom_(this.document_, arguments) +}; +goog.dom.DomHelper.prototype.$dom = goog.dom.DomHelper.prototype.createDom; +goog.dom.DomHelper.prototype.createElement = function(name) { + return this.document_.createElement(name) +}; +goog.dom.DomHelper.prototype.createTextNode = function(content) { + return this.document_.createTextNode(content) +}; +goog.dom.DomHelper.prototype.createTable = function(rows, columns, opt_fillWithNbsp) { + return goog.dom.createTable_(this.document_, rows, columns, !!opt_fillWithNbsp) +}; +goog.dom.DomHelper.prototype.htmlToDocumentFragment = function(htmlString) { + return goog.dom.htmlToDocumentFragment_(this.document_, htmlString) +}; +goog.dom.DomHelper.prototype.getCompatMode = function() { + return this.isCss1CompatMode() ? "CSS1Compat" : "BackCompat" +}; +goog.dom.DomHelper.prototype.isCss1CompatMode = function() { + return goog.dom.isCss1CompatMode_(this.document_) +}; +goog.dom.DomHelper.prototype.getWindow = function() { + return goog.dom.getWindow_(this.document_) +}; +goog.dom.DomHelper.prototype.getDocumentScrollElement = function() { + return goog.dom.getDocumentScrollElement_(this.document_) +}; +goog.dom.DomHelper.prototype.getDocumentScroll = function() { + return goog.dom.getDocumentScroll_(this.document_) +}; +goog.dom.DomHelper.prototype.appendChild = goog.dom.appendChild; +goog.dom.DomHelper.prototype.append = goog.dom.append; +goog.dom.DomHelper.prototype.removeChildren = goog.dom.removeChildren; +goog.dom.DomHelper.prototype.insertSiblingBefore = goog.dom.insertSiblingBefore; +goog.dom.DomHelper.prototype.insertSiblingAfter = goog.dom.insertSiblingAfter; +goog.dom.DomHelper.prototype.removeNode = goog.dom.removeNode; +goog.dom.DomHelper.prototype.replaceNode = goog.dom.replaceNode; +goog.dom.DomHelper.prototype.flattenElement = goog.dom.flattenElement; +goog.dom.DomHelper.prototype.getFirstElementChild = goog.dom.getFirstElementChild; +goog.dom.DomHelper.prototype.getLastElementChild = goog.dom.getLastElementChild; +goog.dom.DomHelper.prototype.getNextElementSibling = goog.dom.getNextElementSibling; +goog.dom.DomHelper.prototype.getPreviousElementSibling = goog.dom.getPreviousElementSibling; +goog.dom.DomHelper.prototype.getNextNode = goog.dom.getNextNode; +goog.dom.DomHelper.prototype.getPreviousNode = goog.dom.getPreviousNode; +goog.dom.DomHelper.prototype.isNodeLike = goog.dom.isNodeLike; +goog.dom.DomHelper.prototype.contains = goog.dom.contains; +goog.dom.DomHelper.prototype.getOwnerDocument = goog.dom.getOwnerDocument; +goog.dom.DomHelper.prototype.getFrameContentDocument = goog.dom.getFrameContentDocument; +goog.dom.DomHelper.prototype.getFrameContentWindow = goog.dom.getFrameContentWindow; +goog.dom.DomHelper.prototype.setTextContent = goog.dom.setTextContent; +goog.dom.DomHelper.prototype.findNode = goog.dom.findNode; +goog.dom.DomHelper.prototype.findNodes = goog.dom.findNodes; +goog.dom.DomHelper.prototype.getTextContent = goog.dom.getTextContent; +goog.dom.DomHelper.prototype.getNodeTextLength = goog.dom.getNodeTextLength; +goog.dom.DomHelper.prototype.getNodeTextOffset = goog.dom.getNodeTextOffset; +goog.dom.DomHelper.prototype.getAncestorByTagNameAndClass = goog.dom.getAncestorByTagNameAndClass; +goog.dom.DomHelper.prototype.getAncestor = goog.dom.getAncestor; +goog.provide("goog.Disposable"); +goog.provide("goog.dispose"); +goog.Disposable = function() { +}; +goog.Disposable.prototype.disposed_ = false; +goog.Disposable.prototype.isDisposed = function() { + return this.disposed_ +}; +goog.Disposable.prototype.getDisposed = goog.Disposable.prototype.isDisposed; +goog.Disposable.prototype.dispose = function() { + if(!this.disposed_) { + this.disposed_ = true; + this.disposeInternal() + } +}; +goog.Disposable.prototype.disposeInternal = function() { +}; +goog.dispose = function(obj) { + if(obj && typeof obj.dispose == "function") { + obj.dispose() + } +}; +goog.provide("goog.structs"); +goog.require("goog.array"); +goog.require("goog.object"); +goog.structs.getCount = function(col) { + if(typeof col.getCount == "function") { + return col.getCount() + } + if(goog.isArrayLike(col) || goog.isString(col)) { + return col.length + } + return goog.object.getCount(col) +}; +goog.structs.getValues = function(col) { + if(typeof col.getValues == "function") { + return col.getValues() + } + if(goog.isString(col)) { + return col.split("") + } + if(goog.isArrayLike(col)) { + var rv = []; + var l = col.length; + for(var i = 0;i < l;i++) { + rv.push(col[i]) + } + return rv + } + return goog.object.getValues(col) +}; +goog.structs.getKeys = function(col) { + if(typeof col.getKeys == "function") { + return col.getKeys() + } + if(typeof col.getValues == "function") { + return undefined + } + if(goog.isArrayLike(col) || goog.isString(col)) { + var rv = []; + var l = col.length; + for(var i = 0;i < l;i++) { + rv.push(i) + } + return rv + } + return goog.object.getKeys(col) +}; +goog.structs.contains = function(col, val) { + if(typeof col.contains == "function") { + return col.contains(val) + } + if(typeof col.containsValue == "function") { + return col.containsValue(val) + } + if(goog.isArrayLike(col) || goog.isString(col)) { + return goog.array.contains(col, val) + } + return goog.object.containsValue(col, val) +}; +goog.structs.isEmpty = function(col) { + if(typeof col.isEmpty == "function") { + return col.isEmpty() + } + if(goog.isArrayLike(col) || goog.isString(col)) { + return goog.array.isEmpty(col) + } + return goog.object.isEmpty(col) +}; +goog.structs.clear = function(col) { + if(typeof col.clear == "function") { + col.clear() + }else { + if(goog.isArrayLike(col)) { + goog.array.clear(col) + }else { + goog.object.clear(col) + } + } +}; +goog.structs.forEach = function(col, f, opt_obj) { + if(typeof col.forEach == "function") { + col.forEach(f, opt_obj) + }else { + if(goog.isArrayLike(col) || goog.isString(col)) { + goog.array.forEach(col, f, opt_obj) + }else { + var keys = goog.structs.getKeys(col); + var values = goog.structs.getValues(col); + var l = values.length; + for(var i = 0;i < l;i++) { + f.call(opt_obj, values[i], keys && keys[i], col) + } + } + } +}; +goog.structs.filter = function(col, f, opt_obj) { + if(typeof col.filter == "function") { + return col.filter(f, opt_obj) + } + if(goog.isArrayLike(col) || goog.isString(col)) { + return goog.array.filter(col, f, opt_obj) + } + var rv; + var keys = goog.structs.getKeys(col); + var values = goog.structs.getValues(col); + var l = values.length; + if(keys) { + rv = {}; + for(var i = 0;i < l;i++) { + if(f.call(opt_obj, values[i], keys[i], col)) { + rv[keys[i]] = values[i] + } + } + }else { + rv = []; + for(var i = 0;i < l;i++) { + if(f.call(opt_obj, values[i], undefined, col)) { + rv.push(values[i]) + } + } + } + return rv +}; +goog.structs.map = function(col, f, opt_obj) { + if(typeof col.map == "function") { + return col.map(f, opt_obj) + } + if(goog.isArrayLike(col) || goog.isString(col)) { + return goog.array.map(col, f, opt_obj) + } + var rv; + var keys = goog.structs.getKeys(col); + var values = goog.structs.getValues(col); + var l = values.length; + if(keys) { + rv = {}; + for(var i = 0;i < l;i++) { + rv[keys[i]] = f.call(opt_obj, values[i], keys[i], col) + } + }else { + rv = []; + for(var i = 0;i < l;i++) { + rv[i] = f.call(opt_obj, values[i], undefined, col) + } + } + return rv +}; +goog.structs.some = function(col, f, opt_obj) { + if(typeof col.some == "function") { + return col.some(f, opt_obj) + } + if(goog.isArrayLike(col) || goog.isString(col)) { + return goog.array.some(col, f, opt_obj) + } + var keys = goog.structs.getKeys(col); + var values = goog.structs.getValues(col); + var l = values.length; + for(var i = 0;i < l;i++) { + if(f.call(opt_obj, values[i], keys && keys[i], col)) { + return true + } + } + return false +}; +goog.structs.every = function(col, f, opt_obj) { + if(typeof col.every == "function") { + return col.every(f, opt_obj) + } + if(goog.isArrayLike(col) || goog.isString(col)) { + return goog.array.every(col, f, opt_obj) + } + var keys = goog.structs.getKeys(col); + var values = goog.structs.getValues(col); + var l = values.length; + for(var i = 0;i < l;i++) { + if(!f.call(opt_obj, values[i], keys && keys[i], col)) { + return false + } + } + return true +}; +goog.provide("goog.iter"); +goog.provide("goog.iter.Iterator"); +goog.provide("goog.iter.StopIteration"); +goog.require("goog.array"); +goog.iter.Iterable; +if("StopIteration" in goog.global) { + goog.iter.StopIteration = goog.global["StopIteration"] +}else { + goog.iter.StopIteration = Error("StopIteration") +} +goog.iter.Iterator = function() { +}; +goog.iter.Iterator.prototype.next = function() { + throw goog.iter.StopIteration; +}; +goog.iter.Iterator.prototype.__iterator__ = function(opt_keys) { + return this +}; +goog.iter.toIterator = function(iterable) { + if(iterable instanceof goog.iter.Iterator) { + return iterable + } + if(typeof iterable.__iterator__ == "function") { + return iterable.__iterator__(false) + } + if(goog.isArrayLike(iterable)) { + var i = 0; + var newIter = new goog.iter.Iterator; + newIter.next = function() { + while(true) { + if(i >= iterable.length) { + throw goog.iter.StopIteration; + } + if(!(i in iterable)) { + i++; + continue + } + return iterable[i++] + } + }; + return newIter + } + throw Error("Not implemented"); +}; +goog.iter.forEach = function(iterable, f, opt_obj) { + if(goog.isArrayLike(iterable)) { + try { + goog.array.forEach(iterable, f, opt_obj) + }catch(ex) { + if(ex !== goog.iter.StopIteration) { + throw ex; + } + } + }else { + iterable = goog.iter.toIterator(iterable); + try { + while(true) { + f.call(opt_obj, iterable.next(), undefined, iterable) + } + }catch(ex) { + if(ex !== goog.iter.StopIteration) { + throw ex; + } + } + } +}; +goog.iter.filter = function(iterable, f, opt_obj) { + iterable = goog.iter.toIterator(iterable); + var newIter = new goog.iter.Iterator; + newIter.next = function() { + while(true) { + var val = iterable.next(); + if(f.call(opt_obj, val, undefined, iterable)) { + return val + } + } + }; + return newIter +}; +goog.iter.range = function(startOrStop, opt_stop, opt_step) { + var start = 0; + var stop = startOrStop; + var step = opt_step || 1; + if(arguments.length > 1) { + start = startOrStop; + stop = opt_stop + } + if(step == 0) { + throw Error("Range step argument must not be zero"); + } + var newIter = new goog.iter.Iterator; + newIter.next = function() { + if(step > 0 && start >= stop || step < 0 && start <= stop) { + throw goog.iter.StopIteration; + } + var rv = start; + start += step; + return rv + }; + return newIter +}; +goog.iter.join = function(iterable, deliminator) { + return goog.iter.toArray(iterable).join(deliminator) +}; +goog.iter.map = function(iterable, f, opt_obj) { + iterable = goog.iter.toIterator(iterable); + var newIter = new goog.iter.Iterator; + newIter.next = function() { + while(true) { + var val = iterable.next(); + return f.call(opt_obj, val, undefined, iterable) + } + }; + return newIter +}; +goog.iter.reduce = function(iterable, f, val, opt_obj) { + var rval = val; + goog.iter.forEach(iterable, function(val) { + rval = f.call(opt_obj, rval, val) + }); + return rval +}; +goog.iter.some = function(iterable, f, opt_obj) { + iterable = goog.iter.toIterator(iterable); + try { + while(true) { + if(f.call(opt_obj, iterable.next(), undefined, iterable)) { + return true + } + } + }catch(ex) { + if(ex !== goog.iter.StopIteration) { + throw ex; + } + } + return false +}; +goog.iter.every = function(iterable, f, opt_obj) { + iterable = goog.iter.toIterator(iterable); + try { + while(true) { + if(!f.call(opt_obj, iterable.next(), undefined, iterable)) { + return false + } + } + }catch(ex) { + if(ex !== goog.iter.StopIteration) { + throw ex; + } + } + return true +}; +goog.iter.chain = function(var_args) { + var args = arguments; + var length = args.length; + var i = 0; + var newIter = new goog.iter.Iterator; + newIter.next = function() { + try { + if(i >= length) { + throw goog.iter.StopIteration; + } + var current = goog.iter.toIterator(args[i]); + return current.next() + }catch(ex) { + if(ex !== goog.iter.StopIteration || i >= length) { + throw ex; + }else { + i++; + return this.next() + } + } + }; + return newIter +}; +goog.iter.dropWhile = function(iterable, f, opt_obj) { + iterable = goog.iter.toIterator(iterable); + var newIter = new goog.iter.Iterator; + var dropping = true; + newIter.next = function() { + while(true) { + var val = iterable.next(); + if(dropping && f.call(opt_obj, val, undefined, iterable)) { + continue + }else { + dropping = false + } + return val + } + }; + return newIter +}; +goog.iter.takeWhile = function(iterable, f, opt_obj) { + iterable = goog.iter.toIterator(iterable); + var newIter = new goog.iter.Iterator; + var taking = true; + newIter.next = function() { + while(true) { + if(taking) { + var val = iterable.next(); + if(f.call(opt_obj, val, undefined, iterable)) { + return val + }else { + taking = false + } + }else { + throw goog.iter.StopIteration; + } + } + }; + return newIter +}; +goog.iter.toArray = function(iterable) { + if(goog.isArrayLike(iterable)) { + return goog.array.toArray(iterable) + } + iterable = goog.iter.toIterator(iterable); + var array = []; + goog.iter.forEach(iterable, function(val) { + array.push(val) + }); + return array +}; +goog.iter.equals = function(iterable1, iterable2) { + iterable1 = goog.iter.toIterator(iterable1); + iterable2 = goog.iter.toIterator(iterable2); + var b1, b2; + try { + while(true) { + b1 = b2 = false; + var val1 = iterable1.next(); + b1 = true; + var val2 = iterable2.next(); + b2 = true; + if(val1 != val2) { + return false + } + } + }catch(ex) { + if(ex !== goog.iter.StopIteration) { + throw ex; + }else { + if(b1 && !b2) { + return false + } + if(!b2) { + try { + val2 = iterable2.next(); + return false + }catch(ex1) { + if(ex1 !== goog.iter.StopIteration) { + throw ex1; + } + return true + } + } + } + } + return false +}; +goog.iter.nextOrValue = function(iterable, defaultValue) { + try { + return goog.iter.toIterator(iterable).next() + }catch(e) { + if(e != goog.iter.StopIteration) { + throw e; + } + return defaultValue + } +}; +goog.provide("goog.structs.Map"); +goog.require("goog.iter.Iterator"); +goog.require("goog.iter.StopIteration"); +goog.require("goog.object"); +goog.require("goog.structs"); +goog.structs.Map = function(opt_map, var_args) { + this.map_ = {}; + this.keys_ = []; + var argLength = arguments.length; + if(argLength > 1) { + if(argLength % 2) { + throw Error("Uneven number of arguments"); + } + for(var i = 0;i < argLength;i += 2) { + this.set(arguments[i], arguments[i + 1]) + } + }else { + if(opt_map) { + this.addAll(opt_map) + } + } +}; +goog.structs.Map.prototype.count_ = 0; +goog.structs.Map.prototype.version_ = 0; +goog.structs.Map.prototype.getCount = function() { + return this.count_ +}; +goog.structs.Map.prototype.getValues = function() { + this.cleanupKeysArray_(); + var rv = []; + for(var i = 0;i < this.keys_.length;i++) { + var key = this.keys_[i]; + rv.push(this.map_[key]) + } + return rv +}; +goog.structs.Map.prototype.getKeys = function() { + this.cleanupKeysArray_(); + return this.keys_.concat() +}; +goog.structs.Map.prototype.containsKey = function(key) { + return goog.structs.Map.hasKey_(this.map_, key) +}; +goog.structs.Map.prototype.containsValue = function(val) { + for(var i = 0;i < this.keys_.length;i++) { + var key = this.keys_[i]; + if(goog.structs.Map.hasKey_(this.map_, key) && this.map_[key] == val) { + return true + } + } + return false +}; +goog.structs.Map.prototype.equals = function(otherMap, opt_equalityFn) { + if(this === otherMap) { + return true + } + if(this.count_ != otherMap.getCount()) { + return false + } + var equalityFn = opt_equalityFn || goog.structs.Map.defaultEquals; + this.cleanupKeysArray_(); + for(var key, i = 0;key = this.keys_[i];i++) { + if(!equalityFn(this.get(key), otherMap.get(key))) { + return false + } + } + return true +}; +goog.structs.Map.defaultEquals = function(a, b) { + return a === b +}; +goog.structs.Map.prototype.isEmpty = function() { + return this.count_ == 0 +}; +goog.structs.Map.prototype.clear = function() { + this.map_ = {}; + this.keys_.length = 0; + this.count_ = 0; + this.version_ = 0 +}; +goog.structs.Map.prototype.remove = function(key) { + if(goog.structs.Map.hasKey_(this.map_, key)) { + delete this.map_[key]; + this.count_--; + this.version_++; + if(this.keys_.length > 2 * this.count_) { + this.cleanupKeysArray_() + } + return true + } + return false +}; +goog.structs.Map.prototype.cleanupKeysArray_ = function() { + if(this.count_ != this.keys_.length) { + var srcIndex = 0; + var destIndex = 0; + while(srcIndex < this.keys_.length) { + var key = this.keys_[srcIndex]; + if(goog.structs.Map.hasKey_(this.map_, key)) { + this.keys_[destIndex++] = key + } + srcIndex++ + } + this.keys_.length = destIndex + } + if(this.count_ != this.keys_.length) { + var seen = {}; + var srcIndex = 0; + var destIndex = 0; + while(srcIndex < this.keys_.length) { + var key = this.keys_[srcIndex]; + if(!goog.structs.Map.hasKey_(seen, key)) { + this.keys_[destIndex++] = key; + seen[key] = 1 + } + srcIndex++ + } + this.keys_.length = destIndex + } +}; +goog.structs.Map.prototype.get = function(key, opt_val) { + if(goog.structs.Map.hasKey_(this.map_, key)) { + return this.map_[key] + } + return opt_val +}; +goog.structs.Map.prototype.set = function(key, value) { + if(!goog.structs.Map.hasKey_(this.map_, key)) { + this.count_++; + this.keys_.push(key); + this.version_++ + } + this.map_[key] = value +}; +goog.structs.Map.prototype.addAll = function(map) { + var keys, values; + if(map instanceof goog.structs.Map) { + keys = map.getKeys(); + values = map.getValues() + }else { + keys = goog.object.getKeys(map); + values = goog.object.getValues(map) + } + for(var i = 0;i < keys.length;i++) { + this.set(keys[i], values[i]) + } +}; +goog.structs.Map.prototype.clone = function() { + return new goog.structs.Map(this) +}; +goog.structs.Map.prototype.transpose = function() { + var transposed = new goog.structs.Map; + for(var i = 0;i < this.keys_.length;i++) { + var key = this.keys_[i]; + var value = this.map_[key]; + transposed.set(value, key) + } + return transposed +}; +goog.structs.Map.prototype.toObject = function() { + this.cleanupKeysArray_(); + var obj = {}; + for(var i = 0;i < this.keys_.length;i++) { + var key = this.keys_[i]; + obj[key] = this.map_[key] + } + return obj +}; +goog.structs.Map.prototype.getKeyIterator = function() { + return this.__iterator__(true) +}; +goog.structs.Map.prototype.getValueIterator = function() { + return this.__iterator__(false) +}; +goog.structs.Map.prototype.__iterator__ = function(opt_keys) { + this.cleanupKeysArray_(); + var i = 0; + var keys = this.keys_; + var map = this.map_; + var version = this.version_; + var selfObj = this; + var newIter = new goog.iter.Iterator; + newIter.next = function() { + while(true) { + if(version != selfObj.version_) { + throw Error("The map has changed since the iterator was created"); + } + if(i >= keys.length) { + throw goog.iter.StopIteration; + } + var key = keys[i++]; + return opt_keys ? key : map[key] + } + }; + return newIter +}; +goog.structs.Map.hasKey_ = function(obj, key) { + return Object.prototype.hasOwnProperty.call(obj, key) +}; +goog.provide("goog.structs.Set"); +goog.require("goog.structs"); +goog.require("goog.structs.Map"); +goog.structs.Set = function(opt_values) { + this.map_ = new goog.structs.Map; + if(opt_values) { + this.addAll(opt_values) + } +}; +goog.structs.Set.getKey_ = function(val) { + var type = typeof val; + if(type == "object" && val || type == "function") { + return"o" + goog.getUid(val) + }else { + return type.substr(0, 1) + val + } +}; +goog.structs.Set.prototype.getCount = function() { + return this.map_.getCount() +}; +goog.structs.Set.prototype.add = function(element) { + this.map_.set(goog.structs.Set.getKey_(element), element) +}; +goog.structs.Set.prototype.addAll = function(col) { + var values = goog.structs.getValues(col); + var l = values.length; + for(var i = 0;i < l;i++) { + this.add(values[i]) + } +}; +goog.structs.Set.prototype.removeAll = function(col) { + var values = goog.structs.getValues(col); + var l = values.length; + for(var i = 0;i < l;i++) { + this.remove(values[i]) + } +}; +goog.structs.Set.prototype.remove = function(element) { + return this.map_.remove(goog.structs.Set.getKey_(element)) +}; +goog.structs.Set.prototype.clear = function() { + this.map_.clear() +}; +goog.structs.Set.prototype.isEmpty = function() { + return this.map_.isEmpty() +}; +goog.structs.Set.prototype.contains = function(element) { + return this.map_.containsKey(goog.structs.Set.getKey_(element)) +}; +goog.structs.Set.prototype.containsAll = function(col) { + return goog.structs.every(col, this.contains, this) +}; +goog.structs.Set.prototype.intersection = function(col) { + var result = new goog.structs.Set; + var values = goog.structs.getValues(col); + for(var i = 0;i < values.length;i++) { + var value = values[i]; + if(this.contains(value)) { + result.add(value) + } + } + return result +}; +goog.structs.Set.prototype.getValues = function() { + return this.map_.getValues() +}; +goog.structs.Set.prototype.clone = function() { + return new goog.structs.Set(this) +}; +goog.structs.Set.prototype.equals = function(col) { + return this.getCount() == goog.structs.getCount(col) && this.isSubsetOf(col) +}; +goog.structs.Set.prototype.isSubsetOf = function(col) { + var colCount = goog.structs.getCount(col); + if(this.getCount() > colCount) { + return false + } + if(!(col instanceof goog.structs.Set) && colCount > 5) { + col = new goog.structs.Set(col) + } + return goog.structs.every(this, function(value) { + return goog.structs.contains(col, value) + }) +}; +goog.structs.Set.prototype.__iterator__ = function(opt_keys) { + return this.map_.__iterator__(false) +}; +goog.provide("goog.debug"); +goog.require("goog.array"); +goog.require("goog.string"); +goog.require("goog.structs.Set"); +goog.debug.catchErrors = function(logFunc, opt_cancel, opt_target) { + var target = opt_target || goog.global; + var oldErrorHandler = target.onerror; + target.onerror = function(message, url, line) { + if(oldErrorHandler) { + oldErrorHandler(message, url, line) + } + logFunc({message:message, fileName:url, line:line}); + return Boolean(opt_cancel) + } +}; +goog.debug.expose = function(obj, opt_showFn) { + if(typeof obj == "undefined") { + return"undefined" + } + if(obj == null) { + return"NULL" + } + var str = []; + for(var x in obj) { + if(!opt_showFn && goog.isFunction(obj[x])) { + continue + } + var s = x + " = "; + try { + s += obj[x] + }catch(e) { + s += "*** " + e + " ***" + } + str.push(s) + } + return str.join("\n") +}; +goog.debug.deepExpose = function(obj, opt_showFn) { + var previous = new goog.structs.Set; + var str = []; + var helper = function(obj, space) { + var nestspace = space + " "; + var indentMultiline = function(str) { + return str.replace(/\n/g, "\n" + space) + }; + try { + if(!goog.isDef(obj)) { + str.push("undefined") + }else { + if(goog.isNull(obj)) { + str.push("NULL") + }else { + if(goog.isString(obj)) { + str.push('"' + indentMultiline(obj) + '"') + }else { + if(goog.isFunction(obj)) { + str.push(indentMultiline(String(obj))) + }else { + if(goog.isObject(obj)) { + if(previous.contains(obj)) { + str.push("*** reference loop detected ***") + }else { + previous.add(obj); + str.push("{"); + for(var x in obj) { + if(!opt_showFn && goog.isFunction(obj[x])) { + continue + } + str.push("\n"); + str.push(nestspace); + str.push(x + " = "); + helper(obj[x], nestspace) + } + str.push("\n" + space + "}") + } + }else { + str.push(obj) + } + } + } + } + } + }catch(e) { + str.push("*** " + e + " ***") + } + }; + helper(obj, ""); + return str.join("") +}; +goog.debug.exposeArray = function(arr) { + var str = []; + for(var i = 0;i < arr.length;i++) { + if(goog.isArray(arr[i])) { + str.push(goog.debug.exposeArray(arr[i])) + }else { + str.push(arr[i]) + } + } + return"[ " + str.join(", ") + " ]" +}; +goog.debug.exposeException = function(err, opt_fn) { + try { + var e = goog.debug.normalizeErrorObject(err); + var error = "Message: " + goog.string.htmlEscape(e.message) + '\nUrl: <a href="view-source:' + e.fileName + '" target="_new">' + e.fileName + "</a>\nLine: " + e.lineNumber + "\n\nBrowser stack:\n" + goog.string.htmlEscape(e.stack + "-> ") + "[end]\n\nJS stack traversal:\n" + goog.string.htmlEscape(goog.debug.getStacktrace(opt_fn) + "-> "); + return error + }catch(e2) { + return"Exception trying to expose exception! You win, we lose. " + e2 + } +}; +goog.debug.normalizeErrorObject = function(err) { + var href = goog.getObjectByName("window.location.href"); + return typeof err == "string" ? {message:err, name:"Unknown error", lineNumber:"Not available", fileName:href, stack:"Not available"} : !err.lineNumber || !err.fileName || !err.stack ? {message:err.message, name:err.name, lineNumber:err.lineNumber || err.line || "Not available", fileName:err.fileName || err.filename || err.sourceURL || href, stack:err.stack || "Not available"} : err +}; +goog.debug.enhanceError = function(err, opt_message) { + var error = typeof err == "string" ? Error(err) : err; + if(!error.stack) { + error.stack = goog.debug.getStacktrace(arguments.callee.caller) + } + if(opt_message) { + var x = 0; + while(error["message" + x]) { + ++x + } + error["message" + x] = String(opt_message) + } + return error +}; +goog.debug.getStacktraceSimple = function(opt_depth) { + var sb = []; + var fn = arguments.callee.caller; + var depth = 0; + while(fn && (!opt_depth || depth < opt_depth)) { + sb.push(goog.debug.getFunctionName(fn)); + sb.push("()\n"); + try { + fn = fn.caller + }catch(e) { + sb.push("[exception trying to get caller]\n"); + break + } + depth++; + if(depth >= goog.debug.MAX_STACK_DEPTH) { + sb.push("[...long stack...]"); + break + } + } + if(opt_depth && depth >= opt_depth) { + sb.push("[...reached max depth limit...]") + }else { + sb.push("[end]") + } + return sb.join("") +}; +goog.debug.MAX_STACK_DEPTH = 50; +goog.debug.getStacktrace = function(opt_fn) { + return goog.debug.getStacktraceHelper_(opt_fn || arguments.callee.caller, []) +}; +goog.debug.getStacktraceHelper_ = function(fn, visited) { + var sb = []; + if(goog.array.contains(visited, fn)) { + sb.push("[...circular reference...]") + }else { + if(fn && visited.length < goog.debug.MAX_STACK_DEPTH) { + sb.push(goog.debug.getFunctionName(fn) + "("); + var args = fn.arguments; + for(var i = 0;i < args.length;i++) { + if(i > 0) { + sb.push(", ") + } + var argDesc; + var arg = args[i]; + switch(typeof arg) { + case "object": + argDesc = arg ? "object" : "null"; + break; + case "string": + argDesc = arg; + break; + case "number": + argDesc = String(arg); + break; + case "boolean": + argDesc = arg ? "true" : "false"; + break; + case "function": + argDesc = goog.debug.getFunctionName(arg); + argDesc = argDesc ? argDesc : "[fn]"; + break; + case "undefined": + ; + default: + argDesc = typeof arg; + break + } + if(argDesc.length > 40) { + argDesc = argDesc.substr(0, 40) + "..." + } + sb.push(argDesc) + } + visited.push(fn); + sb.push(")\n"); + try { + sb.push(goog.debug.getStacktraceHelper_(fn.caller, visited)) + }catch(e) { + sb.push("[exception trying to get caller]\n") + } + }else { + if(fn) { + sb.push("[...long stack...]") + }else { + sb.push("[end]") + } + } + } + return sb.join("") +}; +goog.debug.getFunctionName = function(fn) { + var functionSource = String(fn); + if(!goog.debug.fnNameCache_[functionSource]) { + var matches = /function ([^\(]+)/.exec(functionSource); + if(matches) { + var method = matches[1]; + goog.debug.fnNameCache_[functionSource] = method + }else { + goog.debug.fnNameCache_[functionSource] = "[Anonymous]" + } + } + return goog.debug.fnNameCache_[functionSource] +}; +goog.debug.makeWhitespaceVisible = function(string) { + return string.replace(/ /g, "[_]").replace(/\f/g, "[f]").replace(/\n/g, "[n]\n").replace(/\r/g, "[r]").replace(/\t/g, "[t]") +}; +goog.debug.fnNameCache_ = {}; +goog.provide("goog.debug.LogRecord"); +goog.debug.LogRecord = function(level, msg, loggerName, opt_time, opt_sequenceNumber) { + this.reset(level, msg, loggerName, opt_time, opt_sequenceNumber) +}; +goog.debug.LogRecord.prototype.time_; +goog.debug.LogRecord.prototype.level_; +goog.debug.LogRecord.prototype.msg_; +goog.debug.LogRecord.prototype.loggerName_; +goog.debug.LogRecord.prototype.sequenceNumber_ = 0; +goog.debug.LogRecord.prototype.exception_ = null; +goog.debug.LogRecord.prototype.exceptionText_ = null; +goog.debug.LogRecord.ENABLE_SEQUENCE_NUMBERS = true; +goog.debug.LogRecord.nextSequenceNumber_ = 0; +goog.debug.LogRecord.prototype.reset = function(level, msg, loggerName, opt_time, opt_sequenceNumber) { + if(goog.debug.LogRecord.ENABLE_SEQUENCE_NUMBERS) { + this.sequenceNumber_ = typeof opt_sequenceNumber == "number" ? opt_sequenceNumber : goog.debug.LogRecord.nextSequenceNumber_++ + } + this.time_ = opt_time || goog.now(); + this.level_ = level; + this.msg_ = msg; + this.loggerName_ = loggerName; + delete this.exception_; + delete this.exceptionText_ +}; +goog.debug.LogRecord.prototype.getLoggerName = function() { + return this.loggerName_ +}; +goog.debug.LogRecord.prototype.getException = function() { + return this.exception_ +}; +goog.debug.LogRecord.prototype.setException = function(exception) { + this.exception_ = exception +}; +goog.debug.LogRecord.prototype.getExceptionText = function() { + return this.exceptionText_ +}; +goog.debug.LogRecord.prototype.setExceptionText = function(text) { + this.exceptionText_ = text +}; +goog.debug.LogRecord.prototype.setLoggerName = function(loggerName) { + this.loggerName_ = loggerName +}; +goog.debug.LogRecord.prototype.getLevel = function() { + return this.level_ +}; +goog.debug.LogRecord.prototype.setLevel = function(level) { + this.level_ = level +}; +goog.debug.LogRecord.prototype.getMessage = function() { + return this.msg_ +}; +goog.debug.LogRecord.prototype.setMessage = function(msg) { + this.msg_ = msg +}; +goog.debug.LogRecord.prototype.getMillis = function() { + return this.time_ +}; +goog.debug.LogRecord.prototype.setMillis = function(time) { + this.time_ = time +}; +goog.debug.LogRecord.prototype.getSequenceNumber = function() { + return this.sequenceNumber_ +}; +goog.provide("goog.debug.LogBuffer"); +goog.require("goog.asserts"); +goog.require("goog.debug.LogRecord"); +goog.debug.LogBuffer = function() { + goog.asserts.assert(goog.debug.LogBuffer.isBufferingEnabled(), "Cannot use goog.debug.LogBuffer without defining " + "goog.debug.LogBuffer.CAPACITY."); + this.clear() +}; +goog.debug.LogBuffer.getInstance = function() { + if(!goog.debug.LogBuffer.instance_) { + goog.debug.LogBuffer.instance_ = new goog.debug.LogBuffer + } + return goog.debug.LogBuffer.instance_ +}; +goog.debug.LogBuffer.CAPACITY = 0; +goog.debug.LogBuffer.prototype.buffer_; +goog.debug.LogBuffer.prototype.curIndex_; +goog.debug.LogBuffer.prototype.isFull_; +goog.debug.LogBuffer.prototype.addRecord = function(level, msg, loggerName) { + var curIndex = (this.curIndex_ + 1) % goog.debug.LogBuffer.CAPACITY; + this.curIndex_ = curIndex; + if(this.isFull_) { + var ret = this.buffer_[curIndex]; + ret.reset(level, msg, loggerName); + return ret + } + this.isFull_ = curIndex == goog.debug.LogBuffer.CAPACITY - 1; + return this.buffer_[curIndex] = new goog.debug.LogRecord(level, msg, loggerName) +}; +goog.debug.LogBuffer.isBufferingEnabled = function() { + return goog.debug.LogBuffer.CAPACITY > 0 +}; +goog.debug.LogBuffer.prototype.clear = function() { + this.buffer_ = new Array(goog.debug.LogBuffer.CAPACITY); + this.curIndex_ = -1; + this.isFull_ = false +}; +goog.debug.LogBuffer.prototype.forEachRecord = function(func) { + var buffer = this.buffer_; + if(!buffer[0]) { + return + } + var curIndex = this.curIndex_; + var i = this.isFull_ ? curIndex : -1; + do { + i = (i + 1) % goog.debug.LogBuffer.CAPACITY; + func(buffer[i]) + }while(i != curIndex) +}; +goog.provide("goog.debug.LogManager"); +goog.provide("goog.debug.Logger"); +goog.provide("goog.debug.Logger.Level"); +goog.require("goog.array"); +goog.require("goog.asserts"); +goog.require("goog.debug"); +goog.require("goog.debug.LogBuffer"); +goog.require("goog.debug.LogRecord"); +goog.debug.Logger = function(name) { + this.name_ = name +}; +goog.debug.Logger.prototype.parent_ = null; +goog.debug.Logger.prototype.level_ = null; +goog.debug.Logger.prototype.children_ = null; +goog.debug.Logger.prototype.handlers_ = null; +goog.debug.Logger.ENABLE_HIERARCHY = true; +if(!goog.debug.Logger.ENABLE_HIERARCHY) { + goog.debug.Logger.rootHandlers_ = []; + goog.debug.Logger.rootLevel_ +} +goog.debug.Logger.Level = function(name, value) { + this.name = name; + this.value = value +}; +goog.debug.Logger.Level.prototype.toString = function() { + return this.name +}; +goog.debug.Logger.Level.OFF = new goog.debug.Logger.Level("OFF", Infinity); +goog.debug.Logger.Level.SHOUT = new goog.debug.Logger.Level("SHOUT", 1200); +goog.debug.Logger.Level.SEVERE = new goog.debug.Logger.Level("SEVERE", 1E3); +goog.debug.Logger.Level.WARNING = new goog.debug.Logger.Level("WARNING", 900); +goog.debug.Logger.Level.INFO = new goog.debug.Logger.Level("INFO", 800); +goog.debug.Logger.Level.CONFIG = new goog.debug.Logger.Level("CONFIG", 700); +goog.debug.Logger.Level.FINE = new goog.debug.Logger.Level("FINE", 500); +goog.debug.Logger.Level.FINER = new goog.debug.Logger.Level("FINER", 400); +goog.debug.Logger.Level.FINEST = new goog.debug.Logger.Level("FINEST", 300); +goog.debug.Logger.Level.ALL = new goog.debug.Logger.Level("ALL", 0); +goog.debug.Logger.Level.PREDEFINED_LEVELS = [goog.debug.Logger.Level.OFF, goog.debug.Logger.Level.SHOUT, goog.debug.Logger.Level.SEVERE, goog.debug.Logger.Level.WARNING, goog.debug.Logger.Level.INFO, goog.debug.Logger.Level.CONFIG, goog.debug.Logger.Level.FINE, goog.debug.Logger.Level.FINER, goog.debug.Logger.Level.FINEST, goog.debug.Logger.Level.ALL]; +goog.debug.Logger.Level.predefinedLevelsCache_ = null; +goog.debug.Logger.Level.createPredefinedLevelsCache_ = function() { + goog.debug.Logger.Level.predefinedLevelsCache_ = {}; + for(var i = 0, level;level = goog.debug.Logger.Level.PREDEFINED_LEVELS[i];i++) { + goog.debug.Logger.Level.predefinedLevelsCache_[level.value] = level; + goog.debug.Logger.Level.predefinedLevelsCache_[level.name] = level + } +}; +goog.debug.Logger.Level.getPredefinedLevel = function(name) { + if(!goog.debug.Logger.Level.predefinedLevelsCache_) { + goog.debug.Logger.Level.createPredefinedLevelsCache_() + } + return goog.debug.Logger.Level.predefinedLevelsCache_[name] || null +}; +goog.debug.Logger.Level.getPredefinedLevelByValue = function(value) { + if(!goog.debug.Logger.Level.predefinedLevelsCache_) { + goog.debug.Logger.Level.createPredefinedLevelsCache_() + } + if(value in goog.debug.Logger.Level.predefinedLevelsCache_) { + return goog.debug.Logger.Level.predefinedLevelsCache_[value] + } + for(var i = 0;i < goog.debug.Logger.Level.PREDEFINED_LEVELS.length;++i) { + var level = goog.debug.Logger.Level.PREDEFINED_LEVELS[i]; + if(level.value <= value) { + return level + } + } + return null +}; +goog.debug.Logger.getLogger = function(name) { + return goog.debug.LogManager.getLogger(name) +}; +goog.debug.Logger.prototype.getName = function() { + return this.name_ +}; +goog.debug.Logger.prototype.addHandler = function(handler) { + if(goog.debug.Logger.ENABLE_HIERARCHY) { + if(!this.handlers_) { + this.handlers_ = [] + } + this.handlers_.push(handler) + }else { + goog.asserts.assert(!this.name_, "Cannot call addHandler on a non-root logger when " + "goog.debug.Logger.ENABLE_HIERARCHY is false."); + goog.debug.Logger.rootHandlers_.push(handler) + } +}; +goog.debug.Logger.prototype.removeHandler = function(handler) { + var handlers = goog.debug.Logger.ENABLE_HIERARCHY ? this.handlers_ : goog.debug.Logger.rootHandlers_; + return!!handlers && goog.array.remove(handlers, handler) +}; +goog.debug.Logger.prototype.getParent = function() { + return this.parent_ +}; +goog.debug.Logger.prototype.getChildren = function() { + if(!this.children_) { + this.children_ = {} + } + return this.children_ +}; +goog.debug.Logger.prototype.setLevel = function(level) { + if(goog.debug.Logger.ENABLE_HIERARCHY) { + this.level_ = level + }else { + goog.asserts.assert(!this.name_, "Cannot call setLevel() on a non-root logger when " + "goog.debug.Logger.ENABLE_HIERARCHY is false."); + goog.debug.Logger.rootLevel_ = level + } +}; +goog.debug.Logger.prototype.getLevel = function() { + return this.level_ +}; +goog.debug.Logger.prototype.getEffectiveLevel = function() { + if(!goog.debug.Logger.ENABLE_HIERARCHY) { + return goog.debug.Logger.rootLevel_ + } + if(this.level_) { + return this.level_ + } + if(this.parent_) { + return this.parent_.getEffectiveLevel() + } + goog.asserts.fail("Root logger has no level set."); + return null +}; +goog.debug.Logger.prototype.isLoggable = function(level) { + return level.value >= this.getEffectiveLevel().value +}; +goog.debug.Logger.prototype.log = function(level, msg, opt_exception) { + if(this.isLoggable(level)) { + this.doLogRecord_(this.getLogRecord(level, msg, opt_exception)) + } +}; +goog.debug.Logger.prototype.getLogRecord = function(level, msg, opt_exception) { + if(goog.debug.LogBuffer.isBufferingEnabled()) { + var logRecord = goog.debug.LogBuffer.getInstance().addRecord(level, msg, this.name_) + }else { + logRecord = new goog.debug.LogRecord(level, String(msg), this.name_) + } + if(opt_exception) { + logRecord.setException(opt_exception); + logRecord.setExceptionText(goog.debug.exposeException(opt_exception, arguments.callee.caller)) + } + return logRecord +}; +goog.debug.Logger.prototype.shout = function(msg, opt_exception) { + this.log(goog.debug.Logger.Level.SHOUT, msg, opt_exception) +}; +goog.debug.Logger.prototype.severe = function(msg, opt_exception) { + this.log(goog.debug.Logger.Level.SEVERE, msg, opt_exception) +}; +goog.debug.Logger.prototype.warning = function(msg, opt_exception) { + this.log(goog.debug.Logger.Level.WARNING, msg, opt_exception) +}; +goog.debug.Logger.prototype.info = function(msg, opt_exception) { + this.log(goog.debug.Logger.Level.INFO, msg, opt_exception) +}; +goog.debug.Logger.prototype.config = function(msg, opt_exception) { + this.log(goog.debug.Logger.Level.CONFIG, msg, opt_exception) +}; +goog.debug.Logger.prototype.fine = function(msg, opt_exception) { + this.log(goog.debug.Logger.Level.FINE, msg, opt_exception) +}; +goog.debug.Logger.prototype.finer = function(msg, opt_exception) { + this.log(goog.debug.Logger.Level.FINER, msg, opt_exception) +}; +goog.debug.Logger.prototype.finest = function(msg, opt_exception) { + this.log(goog.debug.Logger.Level.FINEST, msg, opt_exception) +}; +goog.debug.Logger.prototype.logRecord = function(logRecord) { + if(this.isLoggable(logRecord.getLevel())) { + this.doLogRecord_(logRecord) + } +}; +goog.debug.Logger.prototype.doLogRecord_ = function(logRecord) { + if(goog.debug.Logger.ENABLE_HIERARCHY) { + var target = this; + while(target) { + target.callPublish_(logRecord); + target = target.getParent() + } + }else { + for(var i = 0, handler;handler = goog.debug.Logger.rootHandlers_[i++];) { + handler(logRecord) + } + } +}; +goog.debug.Logger.prototype.callPublish_ = function(logRecord) { + if(this.handlers_) { + for(var i = 0, handler;handler = this.handlers_[i];i++) { + handler(logRecord) + } + } +}; +goog.debug.Logger.prototype.setParent_ = function(parent) { + this.parent_ = parent +}; +goog.debug.Logger.prototype.addChild_ = function(name, logger) { + this.getChildren()[name] = logger +}; +goog.debug.LogManager = {}; +goog.debug.LogManager.loggers_ = {}; +goog.debug.LogManager.rootLogger_ = null; +goog.debug.LogManager.initialize = function() { + if(!goog.debug.LogManager.rootLogger_) { + goog.debug.LogManager.rootLogger_ = new goog.debug.Logger(""); + goog.debug.LogManager.loggers_[""] = goog.debug.LogManager.rootLogger_; + goog.debug.LogManager.rootLogger_.setLevel(goog.debug.Logger.Level.CONFIG) + } +}; +goog.debug.LogManager.getLoggers = function() { + return goog.debug.LogManager.loggers_ +}; +goog.debug.LogManager.getRoot = function() { + goog.debug.LogManager.initialize(); + return goog.debug.LogManager.rootLogger_ +}; +goog.debug.LogManager.getLogger = function(name) { + goog.debug.LogManager.initialize(); + var ret = goog.debug.LogManager.loggers_[name]; + return ret || goog.debug.LogManager.createLogger_(name) +}; +goog.debug.LogManager.createFunctionForCatchErrors = function(opt_logger) { + return function(info) { + var logger = opt_logger || goog.debug.LogManager.getRoot(); + logger.severe("Error: " + info.message + " (" + info.fileName + " @ Line: " + info.line + ")") + } +}; +goog.debug.LogManager.createLogger_ = function(name) { + var logger = new goog.debug.Logger(name); + if(goog.debug.Logger.ENABLE_HIERARCHY) { + var lastDotIndex = name.lastIndexOf("."); + var parentName = name.substr(0, lastDotIndex); + var leafName = name.substr(lastDotIndex + 1); + var parentLogger = goog.debug.LogManager.getLogger(parentName); + parentLogger.addChild_(leafName, logger); + logger.setParent_(parentLogger) + } + goog.debug.LogManager.loggers_[name] = logger; + return logger +}; +goog.provide("goog.dom.SavedRange"); +goog.require("goog.Disposable"); +goog.require("goog.debug.Logger"); +goog.dom.SavedRange = function() { + goog.Disposable.call(this) +}; +goog.inherits(goog.dom.SavedRange, goog.Disposable); +goog.dom.SavedRange.logger_ = goog.debug.Logger.getLogger("goog.dom.SavedRange"); +goog.dom.SavedRange.prototype.restore = function(opt_stayAlive) { + if(this.isDisposed()) { + goog.dom.SavedRange.logger_.severe("Disposed SavedRange objects cannot be restored.") + } + var range = this.restoreInternal(); + if(!opt_stayAlive) { + this.dispose() + } + return range +}; +goog.dom.SavedRange.prototype.restoreInternal = goog.abstractMethod; +goog.provide("goog.dom.SavedCaretRange"); +goog.require("goog.array"); +goog.require("goog.dom"); +goog.require("goog.dom.SavedRange"); +goog.require("goog.dom.TagName"); +goog.require("goog.string"); +goog.dom.SavedCaretRange = function(range) { + goog.dom.SavedRange.call(this); + this.startCaretId_ = goog.string.createUniqueString(); + this.endCaretId_ = goog.string.createUniqueString(); + this.dom_ = goog.dom.getDomHelper(range.getDocument()); + range.surroundWithNodes(this.createCaret_(true), this.createCaret_(false)) +}; +goog.inherits(goog.dom.SavedCaretRange, goog.dom.SavedRange); +goog.dom.SavedCaretRange.prototype.toAbstractRange = function() { + var range = null; + var startCaret = this.getCaret(true); + var endCaret = this.getCaret(false); + if(startCaret && endCaret) { + range = goog.dom.Range.createFromNodes(startCaret, 0, endCaret, 0) + } + return range +}; +goog.dom.SavedCaretRange.prototype.getCaret = function(start) { + return this.dom_.getElement(start ? this.startCaretId_ : this.endCaretId_) +}; +goog.dom.SavedCaretRange.prototype.removeCarets = function(opt_range) { + goog.dom.removeNode(this.getCaret(true)); + goog.dom.removeNode(this.getCaret(false)); + return opt_range +}; +goog.dom.SavedCaretRange.prototype.setRestorationDocument = function(doc) { + this.dom_.setDocument(doc) +}; +goog.dom.SavedCaretRange.prototype.restoreInternal = function() { + var range = null; + var startCaret = this.getCaret(true); + var endCaret = this.getCaret(false); + if(startCaret && endCaret) { + var startNode = startCaret.parentNode; + var startOffset = goog.array.indexOf(startNode.childNodes, startCaret); + var endNode = endCaret.parentNode; + var endOffset = goog.array.indexOf(endNode.childNodes, endCaret); + if(endNode == startNode) { + endOffset -= 1 + } + range = goog.dom.Range.createFromNodes(startNode, startOffset, endNode, endOffset); + range = this.removeCarets(range); + range.select() + }else { + this.removeCarets() + } + return range +}; +goog.dom.SavedCaretRange.prototype.disposeInternal = function() { + this.removeCarets(); + this.dom_ = null +}; +goog.dom.SavedCaretRange.prototype.createCaret_ = function(start) { + return this.dom_.createDom(goog.dom.TagName.SPAN, {id:start ? this.startCaretId_ : this.endCaretId_}) +}; +goog.dom.SavedCaretRange.CARET_REGEX = /<span\s+id="?goog_\d+"?><\/span>/ig; +goog.dom.SavedCaretRange.htmlEqual = function(str1, str2) { + return str1 == str2 || str1.replace(goog.dom.SavedCaretRange.CARET_REGEX, "") == str2.replace(goog.dom.SavedCaretRange.CARET_REGEX, "") +}; +goog.provide("goog.dom.TagIterator"); +goog.provide("goog.dom.TagWalkType"); +goog.require("goog.dom.NodeType"); +goog.require("goog.iter.Iterator"); +goog.require("goog.iter.StopIteration"); +goog.dom.TagWalkType = {START_TAG:1, OTHER:0, END_TAG:-1}; +goog.dom.TagIterator = function(opt_node, opt_reversed, opt_unconstrained, opt_tagType, opt_depth) { + this.reversed = !!opt_reversed; + if(opt_node) { + this.setPosition(opt_node, opt_tagType) + } + this.depth = opt_depth != undefined ? opt_depth : this.tagType || 0; + if(this.reversed) { + this.depth *= -1 + } + this.constrained = !opt_unconstrained +}; +goog.inherits(goog.dom.TagIterator, goog.iter.Iterator); +goog.dom.TagIterator.prototype.node = null; +goog.dom.TagIterator.prototype.tagType = goog.dom.TagWalkType.OTHER; +goog.dom.TagIterator.prototype.depth; +goog.dom.TagIterator.prototype.reversed; +goog.dom.TagIterator.prototype.constrained; +goog.dom.TagIterator.prototype.started_ = false; +goog.dom.TagIterator.prototype.setPosition = function(node, opt_tagType, opt_depth) { + this.node = node; + if(node) { + if(goog.isNumber(opt_tagType)) { + this.tagType = opt_tagType + }else { + this.tagType = this.node.nodeType != goog.dom.NodeType.ELEMENT ? goog.dom.TagWalkType.OTHER : this.reversed ? goog.dom.TagWalkType.END_TAG : goog.dom.TagWalkType.START_TAG + } + } + if(goog.isNumber(opt_depth)) { + this.depth = opt_depth + } +}; +goog.dom.TagIterator.prototype.copyFrom = function(other) { + this.node = other.node; + this.tagType = other.tagType; + this.depth = other.depth; + this.reversed = other.reversed; + this.constrained = other.constrained +}; +goog.dom.TagIterator.prototype.clone = function() { + return new goog.dom.TagIterator(this.node, this.reversed, !this.constrained, this.tagType, this.depth) +}; +goog.dom.TagIterator.prototype.skipTag = function() { + var check = this.reversed ? goog.dom.TagWalkType.END_TAG : goog.dom.TagWalkType.START_TAG; + if(this.tagType == check) { + this.tagType = check * -1; + this.depth += this.tagType * (this.reversed ? -1 : 1) + } +}; +goog.dom.TagIterator.prototype.restartTag = function() { + var check = this.reversed ? goog.dom.TagWalkType.START_TAG : goog.dom.TagWalkType.END_TAG; + if(this.tagType == check) { + this.tagType = check * -1; + this.depth += this.tagType * (this.reversed ? -1 : 1) + } +}; +goog.dom.TagIterator.prototype.next = function() { + var node; + if(this.started_) { + if(!this.node || this.constrained && this.depth == 0) { + throw goog.iter.StopIteration; + } + node = this.node; + var startType = this.reversed ? goog.dom.TagWalkType.END_TAG : goog.dom.TagWalkType.START_TAG; + if(this.tagType == startType) { + var child = this.reversed ? node.lastChild : node.firstChild; + if(child) { + this.setPosition(child) + }else { + this.setPosition(node, startType * -1) + } + }else { + var sibling = this.reversed ? node.previousSibling : node.nextSibling; + if(sibling) { + this.setPosition(sibling) + }else { + this.setPosition(node.parentNode, startType * -1) + } + } + this.depth += this.tagType * (this.reversed ? -1 : 1) + }else { + this.started_ = true + } + node = this.node; + if(!this.node) { + throw goog.iter.StopIteration; + } + return node +}; +goog.dom.TagIterator.prototype.isStarted = function() { + return this.started_ +}; +goog.dom.TagIterator.prototype.isStartTag = function() { + return this.tagType == goog.dom.TagWalkType.START_TAG +}; +goog.dom.TagIterator.prototype.isEndTag = function() { + return this.tagType == goog.dom.TagWalkType.END_TAG +}; +goog.dom.TagIterator.prototype.isNonElement = function() { + return this.tagType == goog.dom.TagWalkType.OTHER +}; +goog.dom.TagIterator.prototype.equals = function(other) { + return other.node == this.node && (!this.node || other.tagType == this.tagType) +}; +goog.dom.TagIterator.prototype.splice = function(var_args) { + var node = this.node; + this.restartTag(); + this.reversed = !this.reversed; + goog.dom.TagIterator.prototype.next.call(this); + this.reversed = !this.reversed; + var arr = goog.isArrayLike(arguments[0]) ? arguments[0] : arguments; + for(var i = arr.length - 1;i >= 0;i--) { + goog.dom.insertSiblingAfter(arr[i], node) + } + goog.dom.removeNode(node) +}; +goog.provide("goog.dom.AbstractRange"); +goog.provide("goog.dom.RangeIterator"); +goog.provide("goog.dom.RangeType"); +goog.require("goog.dom"); +goog.require("goog.dom.NodeType"); +goog.require("goog.dom.SavedCaretRange"); +goog.require("goog.dom.TagIterator"); +goog.require("goog.userAgent"); +goog.dom.RangeType = {TEXT:"text", CONTROL:"control", MULTI:"mutli"}; +goog.dom.AbstractRange = function() { +}; +goog.dom.AbstractRange.getBrowserSelectionForWindow = function(win) { + if(win.getSelection) { + return win.getSelection() + }else { + var doc = win.document; + var sel = doc.selection; + if(sel) { + try { + var range = sel.createRange(); + if(range.parentElement) { + if(range.parentElement().document != doc) { + return null + } + }else { + if(!range.length || range.item(0).document != doc) { + return null + } + } + }catch(e) { + return null + } + return sel + } + return null + } +}; +goog.dom.AbstractRange.isNativeControlRange = function(range) { + return!!range && !!range.addElement +}; +goog.dom.AbstractRange.prototype.clone = goog.abstractMethod; +goog.dom.AbstractRange.prototype.getType = goog.abstractMethod; +goog.dom.AbstractRange.prototype.getBrowserRangeObject = goog.abstractMethod; +goog.dom.AbstractRange.prototype.setBrowserRangeObject = function(nativeRange) { + return false +}; +goog.dom.AbstractRange.prototype.getTextRangeCount = goog.abstractMethod; +goog.dom.AbstractRange.prototype.getTextRange = goog.abstractMethod; +goog.dom.AbstractRange.prototype.getTextRanges = function() { + var output = []; + for(var i = 0, len = this.getTextRangeCount();i < len;i++) { + output.push(this.getTextRange(i)) + } + return output +}; +goog.dom.AbstractRange.prototype.getContainer = goog.abstractMethod; +goog.dom.AbstractRange.prototype.getContainerElement = function() { + var node = this.getContainer(); + return node.nodeType == goog.dom.NodeType.ELEMENT ? node : node.parentNode +}; +goog.dom.AbstractRange.prototype.getStartNode = goog.abstractMethod; +goog.dom.AbstractRange.prototype.getStartOffset = goog.abstractMethod; +goog.dom.AbstractRange.prototype.getEndNode = goog.abstractMethod; +goog.dom.AbstractRange.prototype.getEndOffset = goog.abstractMethod; +goog.dom.AbstractRange.prototype.getAnchorNode = function() { + return this.isReversed() ? this.getEndNode() : this.getStartNode() +}; +goog.dom.AbstractRange.prototype.getAnchorOffset = function() { + return this.isReversed() ? this.getEndOffset() : this.getStartOffset() +}; +goog.dom.AbstractRange.prototype.getFocusNode = function() { + return this.isReversed() ? this.getStartNode() : this.getEndNode() +}; +goog.dom.AbstractRange.prototype.getFocusOffset = function() { + return this.isReversed() ? this.getStartOffset() : this.getEndOffset() +}; +goog.dom.AbstractRange.prototype.isReversed = function() { + return false +}; +goog.dom.AbstractRange.prototype.getDocument = function() { + return goog.dom.getOwnerDocument(goog.userAgent.IE ? this.getContainer() : this.getStartNode()) +}; +goog.dom.AbstractRange.prototype.getWindow = function() { + return goog.dom.getWindow(this.getDocument()) +}; +goog.dom.AbstractRange.prototype.containsRange = goog.abstractMethod; +goog.dom.AbstractRange.prototype.containsNode = function(node, opt_allowPartial) { + return this.containsRange(goog.dom.Range.createFromNodeContents(node), opt_allowPartial) +}; +goog.dom.AbstractRange.prototype.isRangeInDocument = goog.abstractMethod; +goog.dom.AbstractRange.prototype.isCollapsed = goog.abstractMethod; +goog.dom.AbstractRange.prototype.getText = goog.abstractMethod; +goog.dom.AbstractRange.prototype.getHtmlFragment = goog.abstractMethod; +goog.dom.AbstractRange.prototype.getValidHtml = goog.abstractMethod; +goog.dom.AbstractRange.prototype.getPastableHtml = goog.abstractMethod; +goog.dom.AbstractRange.prototype.__iterator__ = goog.abstractMethod; +goog.dom.AbstractRange.prototype.select = goog.abstractMethod; +goog.dom.AbstractRange.prototype.removeContents = goog.abstractMethod; +goog.dom.AbstractRange.prototype.insertNode = goog.abstractMethod; +goog.dom.AbstractRange.prototype.replaceContentsWithNode = function(node) { + if(!this.isCollapsed()) { + this.removeContents() + } + return this.insertNode(node, true) +}; +goog.dom.AbstractRange.prototype.surroundWithNodes = goog.abstractMethod; +goog.dom.AbstractRange.prototype.saveUsingDom = goog.abstractMethod; +goog.dom.AbstractRange.prototype.saveUsingCarets = function() { + return this.getStartNode() && this.getEndNode() ? new goog.dom.SavedCaretRange(this) : null +}; +goog.dom.AbstractRange.prototype.collapse = goog.abstractMethod; +goog.dom.RangeIterator = function(node, opt_reverse) { + goog.dom.TagIterator.call(this, node, opt_reverse, true) +}; +goog.inherits(goog.dom.RangeIterator, goog.dom.TagIterator); +goog.dom.RangeIterator.prototype.getStartTextOffset = goog.abstractMethod; +goog.dom.RangeIterator.prototype.getEndTextOffset = goog.abstractMethod; +goog.dom.RangeIterator.prototype.getStartNode = goog.abstractMethod; +goog.dom.RangeIterator.prototype.getEndNode = goog.abstractMethod; +goog.dom.RangeIterator.prototype.isLast = goog.abstractMethod; +goog.provide("goog.dom.AbstractMultiRange"); +goog.require("goog.array"); +goog.require("goog.dom"); +goog.require("goog.dom.AbstractRange"); +goog.dom.AbstractMultiRange = function() { +}; +goog.inherits(goog.dom.AbstractMultiRange, goog.dom.AbstractRange); +goog.dom.AbstractMultiRange.prototype.containsRange = function(otherRange, opt_allowPartial) { + var ranges = this.getTextRanges(); + var otherRanges = otherRange.getTextRanges(); + var fn = opt_allowPartial ? goog.array.some : goog.array.every; + return fn(otherRanges, function(otherRange) { + return goog.array.some(ranges, function(range) { + return range.containsRange(otherRange, opt_allowPartial) + }) + }) +}; +goog.dom.AbstractMultiRange.prototype.insertNode = function(node, before) { + if(before) { + goog.dom.insertSiblingBefore(node, this.getStartNode()) + }else { + goog.dom.insertSiblingAfter(node, this.getEndNode()) + } + return node +}; +goog.dom.AbstractMultiRange.prototype.surroundWithNodes = function(startNode, endNode) { + this.insertNode(startNode, true); + this.insertNode(endNode, false) +}; +goog.provide("goog.dom.TextRangeIterator"); +goog.require("goog.array"); +goog.require("goog.dom.NodeType"); +goog.require("goog.dom.RangeIterator"); +goog.require("goog.dom.TagName"); +goog.require("goog.iter.StopIteration"); +goog.dom.TextRangeIterator = function(startNode, startOffset, endNode, endOffset, opt_reverse) { + var goNext; + if(startNode) { + this.startNode_ = startNode; + this.startOffset_ = startOffset; + this.endNode_ = endNode; + this.endOffset_ = endOffset; + if(startNode.nodeType == goog.dom.NodeType.ELEMENT && startNode.tagName != goog.dom.TagName.BR) { + var startChildren = startNode.childNodes; + var candidate = startChildren[startOffset]; + if(candidate) { + this.startNode_ = candidate; + this.startOffset_ = 0 + }else { + if(startChildren.length) { + this.startNode_ = goog.array.peek(startChildren) + } + goNext = true + } + } + if(endNode.nodeType == goog.dom.NodeType.ELEMENT) { + this.endNode_ = endNode.childNodes[endOffset]; + if(this.endNode_) { + this.endOffset_ = 0 + }else { + this.endNode_ = endNode + } + } + } + goog.dom.RangeIterator.call(this, opt_reverse ? this.endNode_ : this.startNode_, opt_reverse); + if(goNext) { + try { + this.next() + }catch(e) { + if(e != goog.iter.StopIteration) { + throw e; + } + } + } +}; +goog.inherits(goog.dom.TextRangeIterator, goog.dom.RangeIterator); +goog.dom.TextRangeIterator.prototype.startNode_ = null; +goog.dom.TextRangeIterator.prototype.endNode_ = null; +goog.dom.TextRangeIterator.prototype.startOffset_ = 0; +goog.dom.TextRangeIterator.prototype.endOffset_ = 0; +goog.dom.TextRangeIterator.prototype.getStartTextOffset = function() { + return this.node.nodeType != goog.dom.NodeType.TEXT ? -1 : this.node == this.startNode_ ? this.startOffset_ : 0 +}; +goog.dom.TextRangeIterator.prototype.getEndTextOffset = function() { + return this.node.nodeType != goog.dom.NodeType.TEXT ? -1 : this.node == this.endNode_ ? this.endOffset_ : this.node.nodeValue.length +}; +goog.dom.TextRangeIterator.prototype.getStartNode = function() { + return this.startNode_ +}; +goog.dom.TextRangeIterator.prototype.setStartNode = function(node) { + if(!this.isStarted()) { + this.setPosition(node) + } + this.startNode_ = node; + this.startOffset_ = 0 +}; +goog.dom.TextRangeIterator.prototype.getEndNode = function() { + return this.endNode_ +}; +goog.dom.TextRangeIterator.prototype.setEndNode = function(node) { + this.endNode_ = node; + this.endOffset_ = 0 +}; +goog.dom.TextRangeIterator.prototype.isLast = function() { + return this.isStarted() && this.node == this.endNode_ && (!this.endOffset_ || !this.isStartTag()) +}; +goog.dom.TextRangeIterator.prototype.next = function() { + if(this.isLast()) { + throw goog.iter.StopIteration; + } + return goog.dom.TextRangeIterator.superClass_.next.call(this) +}; +goog.dom.TextRangeIterator.prototype.skipTag = function() { + goog.dom.TextRangeIterator.superClass_.skipTag.apply(this); + if(goog.dom.contains(this.node, this.endNode_)) { + throw goog.iter.StopIteration; + } +}; +goog.dom.TextRangeIterator.prototype.copyFrom = function(other) { + this.startNode_ = other.startNode_; + this.endNode_ = other.endNode_; + this.startOffset_ = other.startOffset_; + this.endOffset_ = other.endOffset_; + this.isReversed_ = other.isReversed_; + goog.dom.TextRangeIterator.superClass_.copyFrom.call(this, other) +}; +goog.dom.TextRangeIterator.prototype.clone = function() { + var copy = new goog.dom.TextRangeIterator(this.startNode_, this.startOffset_, this.endNode_, this.endOffset_, this.isReversed_); + copy.copyFrom(this); + return copy +}; +goog.provide("goog.dom.RangeEndpoint"); +goog.dom.RangeEndpoint = {START:1, END:0}; +goog.provide("goog.userAgent.jscript"); +goog.require("goog.string"); +goog.userAgent.jscript.ASSUME_NO_JSCRIPT = false; +goog.userAgent.jscript.init_ = function() { + var hasScriptEngine = "ScriptEngine" in goog.global; + goog.userAgent.jscript.DETECTED_HAS_JSCRIPT_ = hasScriptEngine && goog.global["ScriptEngine"]() == "JScript"; + goog.userAgent.jscript.DETECTED_VERSION_ = goog.userAgent.jscript.DETECTED_HAS_JSCRIPT_ ? goog.global["ScriptEngineMajorVersion"]() + "." + goog.global["ScriptEngineMinorVersion"]() + "." + goog.global["ScriptEngineBuildVersion"]() : "0" +}; +if(!goog.userAgent.jscript.ASSUME_NO_JSCRIPT) { + goog.userAgent.jscript.init_() +} +goog.userAgent.jscript.HAS_JSCRIPT = goog.userAgent.jscript.ASSUME_NO_JSCRIPT ? false : goog.userAgent.jscript.DETECTED_HAS_JSCRIPT_; +goog.userAgent.jscript.VERSION = goog.userAgent.jscript.ASSUME_NO_JSCRIPT ? "0" : goog.userAgent.jscript.DETECTED_VERSION_; +goog.userAgent.jscript.isVersion = function(version) { + return goog.string.compareVersions(goog.userAgent.jscript.VERSION, version) >= 0 +}; +goog.provide("goog.string.StringBuffer"); +goog.require("goog.userAgent.jscript"); +goog.string.StringBuffer = function(opt_a1, var_args) { + this.buffer_ = goog.userAgent.jscript.HAS_JSCRIPT ? [] : ""; + if(opt_a1 != null) { + this.append.apply(this, arguments) + } +}; +goog.string.StringBuffer.prototype.set = function(s) { + this.clear(); + this.append(s) +}; +if(goog.userAgent.jscript.HAS_JSCRIPT) { + goog.string.StringBuffer.prototype.bufferLength_ = 0; + goog.string.StringBuffer.prototype.append = function(a1, opt_a2, var_args) { + if(opt_a2 == null) { + this.buffer_[this.bufferLength_++] = a1 + }else { + this.buffer_.push.apply(this.buffer_, arguments); + this.bufferLength_ = this.buffer_.length + } + return this + } +}else { + goog.string.StringBuffer.prototype.append = function(a1, opt_a2, var_args) { + this.buffer_ += a1; + if(opt_a2 != null) { + for(var i = 1;i < arguments.length;i++) { + this.buffer_ += arguments[i] + } + } + return this + } +} +goog.string.StringBuffer.prototype.clear = function() { + if(goog.userAgent.jscript.HAS_JSCRIPT) { + this.buffer_.length = 0; + this.bufferLength_ = 0 + }else { + this.buffer_ = "" + } +}; +goog.string.StringBuffer.prototype.getLength = function() { + return this.toString().length +}; +goog.string.StringBuffer.prototype.toString = function() { + if(goog.userAgent.jscript.HAS_JSCRIPT) { + var str = this.buffer_.join(""); + this.clear(); + if(str) { + this.append(str) + } + return str + }else { + return this.buffer_ + } +}; +goog.provide("goog.dom.browserrange.AbstractRange"); +goog.require("goog.dom"); +goog.require("goog.dom.NodeType"); +goog.require("goog.dom.RangeEndpoint"); +goog.require("goog.dom.TagName"); +goog.require("goog.dom.TextRangeIterator"); +goog.require("goog.iter"); +goog.require("goog.string"); +goog.require("goog.string.StringBuffer"); +goog.require("goog.userAgent"); +goog.dom.browserrange.AbstractRange = function() { +}; +goog.dom.browserrange.AbstractRange.prototype.clone = goog.abstractMethod; +goog.dom.browserrange.AbstractRange.prototype.getBrowserRange = goog.abstractMethod; +goog.dom.browserrange.AbstractRange.prototype.getContainer = goog.abstractMethod; +goog.dom.browserrange.AbstractRange.prototype.getStartNode = goog.abstractMethod; +goog.dom.browserrange.AbstractRange.prototype.getStartOffset = goog.abstractMethod; +goog.dom.browserrange.AbstractRange.prototype.getEndNode = goog.abstractMethod; +goog.dom.browserrange.AbstractRange.prototype.getEndOffset = goog.abstractMethod; +goog.dom.browserrange.AbstractRange.prototype.compareBrowserRangeEndpoints = goog.abstractMethod; +goog.dom.browserrange.AbstractRange.prototype.containsRange = function(abstractRange, opt_allowPartial) { + var checkPartial = opt_allowPartial && !abstractRange.isCollapsed(); + var range = abstractRange.getBrowserRange(); + var start = goog.dom.RangeEndpoint.START, end = goog.dom.RangeEndpoint.END; + try { + if(checkPartial) { + return this.compareBrowserRangeEndpoints(range, end, start) >= 0 && this.compareBrowserRangeEndpoints(range, start, end) <= 0 + }else { + return this.compareBrowserRangeEndpoints(range, end, end) >= 0 && this.compareBrowserRangeEndpoints(range, start, start) <= 0 + } + }catch(e) { + if(!goog.userAgent.IE) { + throw e; + } + return false + } +}; +goog.dom.browserrange.AbstractRange.prototype.containsNode = function(node, opt_allowPartial) { + return this.containsRange(goog.dom.browserrange.createRangeFromNodeContents(node), opt_allowPartial) +}; +goog.dom.browserrange.AbstractRange.prototype.isCollapsed = goog.abstractMethod; +goog.dom.browserrange.AbstractRange.prototype.getText = goog.abstractMethod; +goog.dom.browserrange.AbstractRange.prototype.getHtmlFragment = function() { + var output = new goog.string.StringBuffer; + goog.iter.forEach(this, function(node, ignore, it) { + if(node.nodeType == goog.dom.NodeType.TEXT) { + output.append(goog.string.htmlEscape(node.nodeValue.substring(it.getStartTextOffset(), it.getEndTextOffset()))) + }else { + if(node.nodeType == goog.dom.NodeType.ELEMENT) { + if(it.isEndTag()) { + if(goog.dom.canHaveChildren(node)) { + output.append("</" + node.tagName + ">") + } + }else { + var shallow = node.cloneNode(false); + var html = goog.dom.getOuterHtml(shallow); + if(goog.userAgent.IE && node.tagName == goog.dom.TagName.LI) { + output.append(html) + }else { + var index = html.lastIndexOf("<"); + output.append(index ? html.substr(0, index) : html) + } + } + } + } + }, this); + return output.toString() +}; +goog.dom.browserrange.AbstractRange.prototype.getValidHtml = goog.abstractMethod; +goog.dom.browserrange.AbstractRange.prototype.__iterator__ = function(opt_keys) { + return new goog.dom.TextRangeIterator(this.getStartNode(), this.getStartOffset(), this.getEndNode(), this.getEndOffset()) +}; +goog.dom.browserrange.AbstractRange.prototype.select = goog.abstractMethod; +goog.dom.browserrange.AbstractRange.prototype.removeContents = goog.abstractMethod; +goog.dom.browserrange.AbstractRange.prototype.surroundContents = goog.abstractMethod; +goog.dom.browserrange.AbstractRange.prototype.insertNode = goog.abstractMethod; +goog.dom.browserrange.AbstractRange.prototype.surroundWithNodes = goog.abstractMethod; +goog.dom.browserrange.AbstractRange.prototype.collapse = goog.abstractMethod; +goog.provide("goog.dom.browserrange.W3cRange"); +goog.require("goog.dom"); +goog.require("goog.dom.NodeType"); +goog.require("goog.dom.RangeEndpoint"); +goog.require("goog.dom.browserrange.AbstractRange"); +goog.require("goog.string"); +goog.dom.browserrange.W3cRange = function(range) { + this.range_ = range +}; +goog.inherits(goog.dom.browserrange.W3cRange, goog.dom.browserrange.AbstractRange); +goog.dom.browserrange.W3cRange.getBrowserRangeForNode = function(node) { + var nodeRange = goog.dom.getOwnerDocument(node).createRange(); + if(node.nodeType == goog.dom.NodeType.TEXT) { + nodeRange.setStart(node, 0); + nodeRange.setEnd(node, node.length) + }else { + if(!goog.dom.browserrange.canContainRangeEndpoint(node)) { + var rangeParent = node.parentNode; + var rangeStartOffset = goog.array.indexOf(rangeParent.childNodes, node); + nodeRange.setStart(rangeParent, rangeStartOffset); + nodeRange.setEnd(rangeParent, rangeStartOffset + 1) + }else { + var tempNode, leaf = node; + while((tempNode = leaf.firstChild) && goog.dom.browserrange.canContainRangeEndpoint(tempNode)) { + leaf = tempNode + } + nodeRange.setStart(leaf, 0); + leaf = node; + while((tempNode = leaf.lastChild) && goog.dom.browserrange.canContainRangeEndpoint(tempNode)) { + leaf = tempNode + } + nodeRange.setEnd(leaf, leaf.nodeType == goog.dom.NodeType.ELEMENT ? leaf.childNodes.length : leaf.length) + } + } + return nodeRange +}; +goog.dom.browserrange.W3cRange.getBrowserRangeForNodes = function(startNode, startOffset, endNode, endOffset) { + var nodeRange = goog.dom.getOwnerDocument(startNode).createRange(); + nodeRange.setStart(startNode, startOffset); + nodeRange.setEnd(endNode, endOffset); + return nodeRange +}; +goog.dom.browserrange.W3cRange.createFromNodeContents = function(node) { + return new goog.dom.browserrange.W3cRange(goog.dom.browserrange.W3cRange.getBrowserRangeForNode(node)) +}; +goog.dom.browserrange.W3cRange.createFromNodes = function(startNode, startOffset, endNode, endOffset) { + return new goog.dom.browserrange.W3cRange(goog.dom.browserrange.W3cRange.getBrowserRangeForNodes(startNode, startOffset, endNode, endOffset)) +}; +goog.dom.browserrange.W3cRange.prototype.clone = function() { + return new this.constructor(this.range_.cloneRange()) +}; +goog.dom.browserrange.W3cRange.prototype.getBrowserRange = function() { + return this.range_ +}; +goog.dom.browserrange.W3cRange.prototype.getContainer = function() { + return this.range_.commonAncestorContainer +}; +goog.dom.browserrange.W3cRange.prototype.getStartNode = function() { + return this.range_.startContainer +}; +goog.dom.browserrange.W3cRange.prototype.getStartOffset = function() { + return this.range_.startOffset +}; +goog.dom.browserrange.W3cRange.prototype.getEndNode = function() { + return this.range_.endContainer +}; +goog.dom.browserrange.W3cRange.prototype.getEndOffset = function() { + return this.range_.endOffset +}; +goog.dom.browserrange.W3cRange.prototype.compareBrowserRangeEndpoints = function(range, thisEndpoint, otherEndpoint) { + return this.range_.compareBoundaryPoints(otherEndpoint == goog.dom.RangeEndpoint.START ? thisEndpoint == goog.dom.RangeEndpoint.START ? goog.global["Range"].START_TO_START : goog.global["Range"].START_TO_END : thisEndpoint == goog.dom.RangeEndpoint.START ? goog.global["Range"].END_TO_START : goog.global["Range"].END_TO_END, range) +}; +goog.dom.browserrange.W3cRange.prototype.isCollapsed = function() { + return this.range_.collapsed +}; +goog.dom.browserrange.W3cRange.prototype.getText = function() { + return this.range_.toString() +}; +goog.dom.browserrange.W3cRange.prototype.getValidHtml = function() { + var div = goog.dom.getDomHelper(this.range_.startContainer).createDom("div"); + div.appendChild(this.range_.cloneContents()); + var result = div.innerHTML; + if(goog.string.startsWith(result, "<") || !this.isCollapsed() && !goog.string.contains(result, "<")) { + return result + } + var container = this.getContainer(); + container = container.nodeType == goog.dom.NodeType.ELEMENT ? container : container.parentNode; + var html = goog.dom.getOuterHtml(container.cloneNode(false)); + return html.replace(">", ">" + result) +}; +goog.dom.browserrange.W3cRange.prototype.select = function(reverse) { + var win = goog.dom.getWindow(goog.dom.getOwnerDocument(this.getStartNode())); + this.selectInternal(win.getSelection(), reverse) +}; +goog.dom.browserrange.W3cRange.prototype.selectInternal = function(selection, reverse) { + selection.removeAllRanges(); + selection.addRange(this.range_) +}; +goog.dom.browserrange.W3cRange.prototype.removeContents = function() { + var range = this.range_; + range.extractContents(); + if(range.startContainer.hasChildNodes()) { + var rangeStartContainer = range.startContainer.childNodes[range.startOffset]; + if(rangeStartContainer) { + var rangePrevious = rangeStartContainer.previousSibling; + if(goog.dom.getRawTextContent(rangeStartContainer) == "") { + goog.dom.removeNode(rangeStartContainer) + } + if(rangePrevious && goog.dom.getRawTextContent(rangePrevious) == "") { + goog.dom.removeNode(rangePrevious) + } + } + } +}; +goog.dom.browserrange.W3cRange.prototype.surroundContents = function(element) { + this.range_.surroundContents(element); + return element +}; +goog.dom.browserrange.W3cRange.prototype.insertNode = function(node, before) { + var range = this.range_.cloneRange(); + range.collapse(before); + range.insertNode(node); + range.detach(); + return node +}; +goog.dom.browserrange.W3cRange.prototype.surroundWithNodes = function(startNode, endNode) { + var win = goog.dom.getWindow(goog.dom.getOwnerDocument(this.getStartNode())); + var selectionRange = goog.dom.Range.createFromWindow(win); + if(selectionRange) { + var sNode = selectionRange.getStartNode(); + var eNode = selectionRange.getEndNode(); + var sOffset = selectionRange.getStartOffset(); + var eOffset = selectionRange.getEndOffset() + } + var clone1 = this.range_.cloneRange(); + var clone2 = this.range_.cloneRange(); + clone1.collapse(false); + clone2.collapse(true); + clone1.insertNode(endNode); + clone2.insertNode(startNode); + clone1.detach(); + clone2.detach(); + if(selectionRange) { + var isInsertedNode = function(n) { + return n == startNode || n == endNode + }; + if(sNode.nodeType == goog.dom.NodeType.TEXT) { + while(sOffset > sNode.length) { + sOffset -= sNode.length; + do { + sNode = sNode.nextSibling + }while(isInsertedNode(sNode)) + } + } + if(eNode.nodeType == goog.dom.NodeType.TEXT) { + while(eOffset > eNode.length) { + eOffset -= eNode.length; + do { + eNode = eNode.nextSibling + }while(isInsertedNode(eNode)) + } + } + goog.dom.Range.createFromNodes(sNode, sOffset, eNode, eOffset).select() + } +}; +goog.dom.browserrange.W3cRange.prototype.collapse = function(toStart) { + this.range_.collapse(toStart) +}; +goog.provide("goog.dom.browserrange.GeckoRange"); +goog.require("goog.dom.browserrange.W3cRange"); +goog.dom.browserrange.GeckoRange = function(range) { + goog.dom.browserrange.W3cRange.call(this, range) +}; +goog.inherits(goog.dom.browserrange.GeckoRange, goog.dom.browserrange.W3cRange); +goog.dom.browserrange.GeckoRange.createFromNodeContents = function(node) { + return new goog.dom.browserrange.GeckoRange(goog.dom.browserrange.W3cRange.getBrowserRangeForNode(node)) +}; +goog.dom.browserrange.GeckoRange.createFromNodes = function(startNode, startOffset, endNode, endOffset) { + return new goog.dom.browserrange.GeckoRange(goog.dom.browserrange.W3cRange.getBrowserRangeForNodes(startNode, startOffset, endNode, endOffset)) +}; +goog.dom.browserrange.GeckoRange.prototype.selectInternal = function(selection, reversed) { + var anchorNode = reversed ? this.getEndNode() : this.getStartNode(); + var anchorOffset = reversed ? this.getEndOffset() : this.getStartOffset(); + var focusNode = reversed ? this.getStartNode() : this.getEndNode(); + var focusOffset = reversed ? this.getStartOffset() : this.getEndOffset(); + selection.collapse(anchorNode, anchorOffset); + if(anchorNode != focusNode || anchorOffset != focusOffset) { + selection.extend(focusNode, focusOffset) + } +}; +goog.provide("goog.dom.NodeIterator"); +goog.require("goog.dom.TagIterator"); +goog.dom.NodeIterator = function(opt_node, opt_reversed, opt_unconstrained, opt_depth) { + goog.dom.TagIterator.call(this, opt_node, opt_reversed, opt_unconstrained, null, opt_depth) +}; +goog.inherits(goog.dom.NodeIterator, goog.dom.TagIterator); +goog.dom.NodeIterator.prototype.next = function() { + do { + goog.dom.NodeIterator.superClass_.next.call(this) + }while(this.isEndTag()); + return this.node +}; +goog.provide("goog.dom.browserrange.IeRange"); +goog.require("goog.array"); +goog.require("goog.debug.Logger"); +goog.require("goog.dom"); +goog.require("goog.dom.NodeIterator"); +goog.require("goog.dom.NodeType"); +goog.require("goog.dom.RangeEndpoint"); +goog.require("goog.dom.TagName"); +goog.require("goog.dom.browserrange.AbstractRange"); +goog.require("goog.iter"); +goog.require("goog.iter.StopIteration"); +goog.require("goog.string"); +goog.dom.browserrange.IeRange = function(range, doc) { + this.range_ = range; + this.doc_ = doc +}; +goog.inherits(goog.dom.browserrange.IeRange, goog.dom.browserrange.AbstractRange); +goog.dom.browserrange.IeRange.logger_ = goog.debug.Logger.getLogger("goog.dom.browserrange.IeRange"); +goog.dom.browserrange.IeRange.getBrowserRangeForNode_ = function(node) { + var nodeRange = goog.dom.getOwnerDocument(node).body.createTextRange(); + if(node.nodeType == goog.dom.NodeType.ELEMENT) { + nodeRange.moveToElementText(node); + if(goog.dom.browserrange.canContainRangeEndpoint(node) && !node.childNodes.length) { + nodeRange.collapse(false) + } + }else { + var offset = 0; + var sibling = node; + while(sibling = sibling.previousSibling) { + var nodeType = sibling.nodeType; + if(nodeType == goog.dom.NodeType.TEXT) { + offset += sibling.length + }else { + if(nodeType == goog.dom.NodeType.ELEMENT) { + nodeRange.moveToElementText(sibling); + break + } + } + } + if(!sibling) { + nodeRange.moveToElementText(node.parentNode) + } + nodeRange.collapse(!sibling); + if(offset) { + nodeRange.move("character", offset) + } + nodeRange.moveEnd("character", node.length) + } + return nodeRange +}; +goog.dom.browserrange.IeRange.getBrowserRangeForNodes_ = function(startNode, startOffset, endNode, endOffset) { + var child, collapse = false; + if(startNode.nodeType == goog.dom.NodeType.ELEMENT) { + if(startOffset > startNode.childNodes.length) { + goog.dom.browserrange.IeRange.logger_.severe("Cannot have startOffset > startNode child count") + } + child = startNode.childNodes[startOffset]; + collapse = !child; + startNode = child || startNode.lastChild || startNode; + startOffset = 0 + } + var leftRange = goog.dom.browserrange.IeRange.getBrowserRangeForNode_(startNode); + if(startOffset) { + leftRange.move("character", startOffset) + } + if(startNode == endNode && startOffset == endOffset) { + leftRange.collapse(true); + return leftRange + } + if(collapse) { + leftRange.collapse(false) + } + collapse = false; + if(endNode.nodeType == goog.dom.NodeType.ELEMENT) { + if(endOffset > endNode.childNodes.length) { + goog.dom.browserrange.IeRange.logger_.severe("Cannot have endOffset > endNode child count") + } + child = endNode.childNodes[endOffset]; + endNode = child || endNode.lastChild || endNode; + endOffset = 0; + collapse = !child + } + var rightRange = goog.dom.browserrange.IeRange.getBrowserRangeForNode_(endNode); + rightRange.collapse(!collapse); + if(endOffset) { + rightRange.moveEnd("character", endOffset) + } + leftRange.setEndPoint("EndToEnd", rightRange); + return leftRange +}; +goog.dom.browserrange.IeRange.createFromNodeContents = function(node) { + var range = new goog.dom.browserrange.IeRange(goog.dom.browserrange.IeRange.getBrowserRangeForNode_(node), goog.dom.getOwnerDocument(node)); + if(!goog.dom.browserrange.canContainRangeEndpoint(node)) { + range.startNode_ = range.endNode_ = range.parentNode_ = node.parentNode; + range.startOffset_ = goog.array.indexOf(range.parentNode_.childNodes, node); + range.endOffset_ = range.startOffset_ + 1 + }else { + var tempNode, leaf = node; + while((tempNode = leaf.firstChild) && goog.dom.browserrange.canContainRangeEndpoint(tempNode)) { + leaf = tempNode + } + range.startNode_ = leaf; + range.startOffset_ = 0; + leaf = node; + while((tempNode = leaf.lastChild) && goog.dom.browserrange.canContainRangeEndpoint(tempNode)) { + leaf = tempNode + } + range.endNode_ = leaf; + range.endOffset_ = leaf.nodeType == goog.dom.NodeType.ELEMENT ? leaf.childNodes.length : leaf.length; + range.parentNode_ = node + } + return range +}; +goog.dom.browserrange.IeRange.createFromNodes = function(startNode, startOffset, endNode, endOffset) { + var range = new goog.dom.browserrange.IeRange(goog.dom.browserrange.IeRange.getBrowserRangeForNodes_(startNode, startOffset, endNode, endOffset), goog.dom.getOwnerDocument(startNode)); + range.startNode_ = startNode; + range.startOffset_ = startOffset; + range.endNode_ = endNode; + range.endOffset_ = endOffset; + return range +}; +goog.dom.browserrange.IeRange.prototype.parentNode_ = null; +goog.dom.browserrange.IeRange.prototype.startNode_ = null; +goog.dom.browserrange.IeRange.prototype.endNode_ = null; +goog.dom.browserrange.IeRange.prototype.startOffset_ = -1; +goog.dom.browserrange.IeRange.prototype.endOffset_ = -1; +goog.dom.browserrange.IeRange.prototype.clone = function() { + var range = new goog.dom.browserrange.IeRange(this.range_.duplicate(), this.doc_); + range.parentNode_ = this.parentNode_; + range.startNode_ = this.startNode_; + range.endNode_ = this.endNode_; + return range +}; +goog.dom.browserrange.IeRange.prototype.getBrowserRange = function() { + return this.range_ +}; +goog.dom.browserrange.IeRange.prototype.clearCachedValues_ = function() { + this.parentNode_ = this.startNode_ = this.endNode_ = null; + this.startOffset_ = this.endOffset_ = -1 +}; +goog.dom.browserrange.IeRange.prototype.getContainer = function() { + if(!this.parentNode_) { + var selectText = this.range_.text; + var range = this.range_.duplicate(); + var rightTrimmedSelectText = selectText.replace(/ +$/, ""); + var numSpacesAtEnd = selectText.length - rightTrimmedSelectText.length; + if(numSpacesAtEnd) { + range.moveEnd("character", -numSpacesAtEnd) + } + var parent = range.parentElement(); + var htmlText = range.htmlText; + var htmlTextLen = goog.string.stripNewlines(htmlText).length; + if(this.isCollapsed() && htmlTextLen > 0) { + return this.parentNode_ = parent + } + while(htmlTextLen > goog.string.stripNewlines(parent.outerHTML).length) { + parent = parent.parentNode + } + while(parent.childNodes.length == 1 && parent.innerText == goog.dom.browserrange.IeRange.getNodeText_(parent.firstChild)) { + if(!goog.dom.browserrange.canContainRangeEndpoint(parent.firstChild)) { + break + } + parent = parent.firstChild + } + if(selectText.length == 0) { + parent = this.findDeepestContainer_(parent) + } + this.parentNode_ = parent + } + return this.parentNode_ +}; +goog.dom.browserrange.IeRange.prototype.findDeepestContainer_ = function(node) { + var childNodes = node.childNodes; + for(var i = 0, len = childNodes.length;i < len;i++) { + var child = childNodes[i]; + if(goog.dom.browserrange.canContainRangeEndpoint(child)) { + var childRange = goog.dom.browserrange.IeRange.getBrowserRangeForNode_(child); + var start = goog.dom.RangeEndpoint.START; + var end = goog.dom.RangeEndpoint.END; + var isChildRangeErratic = childRange.htmlText != child.outerHTML; + var isNativeInRangeErratic = this.isCollapsed() && isChildRangeErratic; + var inChildRange = isNativeInRangeErratic ? this.compareBrowserRangeEndpoints(childRange, start, start) >= 0 && this.compareBrowserRangeEndpoints(childRange, start, end) <= 0 : this.range_.inRange(childRange); + if(inChildRange) { + return this.findDeepestContainer_(child) + } + } + } + return node +}; +goog.dom.browserrange.IeRange.prototype.getStartNode = function() { + if(!this.startNode_) { + this.startNode_ = this.getEndpointNode_(goog.dom.RangeEndpoint.START); + if(this.isCollapsed()) { + this.endNode_ = this.startNode_ + } + } + return this.startNode_ +}; +goog.dom.browserrange.IeRange.prototype.getStartOffset = function() { + if(this.startOffset_ < 0) { + this.startOffset_ = this.getOffset_(goog.dom.RangeEndpoint.START); + if(this.isCollapsed()) { + this.endOffset_ = this.startOffset_ + } + } + return this.startOffset_ +}; +goog.dom.browserrange.IeRange.prototype.getEndNode = function() { + if(this.isCollapsed()) { + return this.getStartNode() + } + if(!this.endNode_) { + this.endNode_ = this.getEndpointNode_(goog.dom.RangeEndpoint.END) + } + return this.endNode_ +}; +goog.dom.browserrange.IeRange.prototype.getEndOffset = function() { + if(this.isCollapsed()) { + return this.getStartOffset() + } + if(this.endOffset_ < 0) { + this.endOffset_ = this.getOffset_(goog.dom.RangeEndpoint.END); + if(this.isCollapsed()) { + this.startOffset_ = this.endOffset_ + } + } + return this.endOffset_ +}; +goog.dom.browserrange.IeRange.prototype.compareBrowserRangeEndpoints = function(range, thisEndpoint, otherEndpoint) { + return this.range_.compareEndPoints((thisEndpoint == goog.dom.RangeEndpoint.START ? "Start" : "End") + "To" + (otherEndpoint == goog.dom.RangeEndpoint.START ? "Start" : "End"), range) +}; +goog.dom.browserrange.IeRange.prototype.getEndpointNode_ = function(endpoint, opt_node) { + var node = opt_node || this.getContainer(); + if(!node || !node.firstChild) { + return node + } + var start = goog.dom.RangeEndpoint.START, end = goog.dom.RangeEndpoint.END; + var isStartEndpoint = endpoint == start; + for(var j = 0, length = node.childNodes.length;j < length;j++) { + var i = isStartEndpoint ? j : length - j - 1; + var child = node.childNodes[i]; + var childRange; + try { + childRange = goog.dom.browserrange.createRangeFromNodeContents(child) + }catch(e) { + continue + } + var ieRange = childRange.getBrowserRange(); + if(this.isCollapsed()) { + if(!goog.dom.browserrange.canContainRangeEndpoint(child)) { + if(this.compareBrowserRangeEndpoints(ieRange, start, start) == 0) { + this.startOffset_ = this.endOffset_ = i; + return node + } + }else { + if(childRange.containsRange(this)) { + return this.getEndpointNode_(endpoint, child) + } + } + }else { + if(this.containsRange(childRange)) { + if(!goog.dom.browserrange.canContainRangeEndpoint(child)) { + if(isStartEndpoint) { + this.startOffset_ = i + }else { + this.endOffset_ = i + 1 + } + return node + } + return this.getEndpointNode_(endpoint, child) + }else { + if(this.compareBrowserRangeEndpoints(ieRange, start, end) < 0 && this.compareBrowserRangeEndpoints(ieRange, end, start) > 0) { + return this.getEndpointNode_(endpoint, child) + } + } + } + } + return node +}; +goog.dom.browserrange.IeRange.prototype.compareNodeEndpoints_ = function(node, thisEndpoint, otherEndpoint) { + return this.range_.compareEndPoints((thisEndpoint == goog.dom.RangeEndpoint.START ? "Start" : "End") + "To" + (otherEndpoint == goog.dom.RangeEndpoint.START ? "Start" : "End"), goog.dom.browserrange.createRangeFromNodeContents(node).getBrowserRange()) +}; +goog.dom.browserrange.IeRange.prototype.getOffset_ = function(endpoint, opt_container) { + var isStartEndpoint = endpoint == goog.dom.RangeEndpoint.START; + var container = opt_container || (isStartEndpoint ? this.getStartNode() : this.getEndNode()); + if(container.nodeType == goog.dom.NodeType.ELEMENT) { + var children = container.childNodes; + var len = children.length; + var edge = isStartEndpoint ? 0 : len - 1; + var sign = isStartEndpoint ? 1 : -1; + for(var i = edge;i >= 0 && i < len;i += sign) { + var child = children[i]; + if(goog.dom.browserrange.canContainRangeEndpoint(child)) { + continue + } + var endPointCompare = this.compareNodeEndpoints_(child, endpoint, endpoint); + if(endPointCompare == 0) { + return isStartEndpoint ? i : i + 1 + } + } + return i == -1 ? 0 : i + }else { + var range = this.range_.duplicate(); + var nodeRange = goog.dom.browserrange.IeRange.getBrowserRangeForNode_(container); + range.setEndPoint(isStartEndpoint ? "EndToEnd" : "StartToStart", nodeRange); + var rangeLength = range.text.length; + return isStartEndpoint ? container.length - rangeLength : rangeLength + } +}; +goog.dom.browserrange.IeRange.getNodeText_ = function(node) { + return node.nodeType == goog.dom.NodeType.TEXT ? node.nodeValue : node.innerText +}; +goog.dom.browserrange.IeRange.prototype.isRangeInDocument = function() { + var range = this.doc_.body.createTextRange(); + range.moveToElementText(this.doc_.body); + return this.containsRange(new goog.dom.browserrange.IeRange(range, this.doc_), true) +}; +goog.dom.browserrange.IeRange.prototype.isCollapsed = function() { + return this.range_.compareEndPoints("StartToEnd", this.range_) == 0 +}; +goog.dom.browserrange.IeRange.prototype.getText = function() { + return this.range_.text +}; +goog.dom.browserrange.IeRange.prototype.getValidHtml = function() { + return this.range_.htmlText +}; +goog.dom.browserrange.IeRange.prototype.select = function(opt_reverse) { + this.range_.select() +}; +goog.dom.browserrange.IeRange.prototype.removeContents = function() { + if(this.range_.htmlText) { + var startNode = this.getStartNode(); + var endNode = this.getEndNode(); + var oldText = this.range_.text; + var clone = this.range_.duplicate(); + clone.moveStart("character", 1); + clone.moveStart("character", -1); + if(clone.text != oldText) { + var iter = new goog.dom.NodeIterator(startNode, false, true); + var toDelete = []; + goog.iter.forEach(iter, function(node) { + if(node.nodeType != goog.dom.NodeType.TEXT && this.containsNode(node)) { + toDelete.push(node); + iter.skipTag() + } + if(node == endNode) { + throw goog.iter.StopIteration; + } + }); + this.collapse(true); + goog.array.forEach(toDelete, goog.dom.removeNode); + this.clearCachedValues_(); + return + } + this.range_ = clone; + this.range_.text = ""; + this.clearCachedValues_(); + var newStartNode = this.getStartNode(); + var newStartOffset = this.getStartOffset(); + try { + var sibling = startNode.nextSibling; + if(startNode == endNode && startNode.parentNode && startNode.nodeType == goog.dom.NodeType.TEXT && sibling && sibling.nodeType == goog.dom.NodeType.TEXT) { + startNode.nodeValue += sibling.nodeValue; + goog.dom.removeNode(sibling); + this.range_ = goog.dom.browserrange.IeRange.getBrowserRangeForNode_(newStartNode); + this.range_.move("character", newStartOffset); + this.clearCachedValues_() + } + }catch(e) { + } + } +}; +goog.dom.browserrange.IeRange.getDomHelper_ = function(range) { + return goog.dom.getDomHelper(range.parentElement()) +}; +goog.dom.browserrange.IeRange.pasteElement_ = function(range, element, opt_domHelper) { + opt_domHelper = opt_domHelper || goog.dom.browserrange.IeRange.getDomHelper_(range); + var id; + var originalId = id = element.id; + if(!id) { + id = element.id = goog.string.createUniqueString() + } + range.pasteHTML(element.outerHTML); + element = opt_domHelper.getElement(id); + if(element) { + if(!originalId) { + element.removeAttribute("id") + } + } + return element +}; +goog.dom.browserrange.IeRange.prototype.surroundContents = function(element) { + goog.dom.removeNode(element); + element.innerHTML = this.range_.htmlText; + element = goog.dom.browserrange.IeRange.pasteElement_(this.range_, element); + if(element) { + this.range_.moveToElementText(element) + } + this.clearCachedValues_(); + return element +}; +goog.dom.browserrange.IeRange.insertNode_ = function(clone, node, before, opt_domHelper) { + opt_domHelper = opt_domHelper || goog.dom.browserrange.IeRange.getDomHelper_(clone); + var isNonElement; + if(node.nodeType != goog.dom.NodeType.ELEMENT) { + isNonElement = true; + node = opt_domHelper.createDom(goog.dom.TagName.DIV, null, node) + } + clone.collapse(before); + node = goog.dom.browserrange.IeRange.pasteElement_(clone, node, opt_domHelper); + if(isNonElement) { + var newNonElement = node.firstChild; + opt_domHelper.flattenElement(node); + node = newNonElement + } + return node +}; +goog.dom.browserrange.IeRange.prototype.insertNode = function(node, before) { + var output = goog.dom.browserrange.IeRange.insertNode_(this.range_.duplicate(), node, before); + this.clearCachedValues_(); + return output +}; +goog.dom.browserrange.IeRange.prototype.surroundWithNodes = function(startNode, endNode) { + var clone1 = this.range_.duplicate(); + var clone2 = this.range_.duplicate(); + goog.dom.browserrange.IeRange.insertNode_(clone1, startNode, true); + goog.dom.browserrange.IeRange.insertNode_(clone2, endNode, false); + this.clearCachedValues_() +}; +goog.dom.browserrange.IeRange.prototype.collapse = function(toStart) { + this.range_.collapse(toStart); + if(toStart) { + this.endNode_ = this.startNode_; + this.endOffset_ = this.startOffset_ + }else { + this.startNode_ = this.endNode_; + this.startOffset_ = this.endOffset_ + } +}; +goog.provide("goog.dom.browserrange.OperaRange"); +goog.require("goog.dom.browserrange.W3cRange"); +goog.dom.browserrange.OperaRange = function(range) { + goog.dom.browserrange.W3cRange.call(this, range) +}; +goog.inherits(goog.dom.browserrange.OperaRange, goog.dom.browserrange.W3cRange); +goog.dom.browserrange.OperaRange.createFromNodeContents = function(node) { + return new goog.dom.browserrange.OperaRange(goog.dom.browserrange.W3cRange.getBrowserRangeForNode(node)) +}; +goog.dom.browserrange.OperaRange.createFromNodes = function(startNode, startOffset, endNode, endOffset) { + return new goog.dom.browserrange.OperaRange(goog.dom.browserrange.W3cRange.getBrowserRangeForNodes(startNode, startOffset, endNode, endOffset)) +}; +goog.dom.browserrange.OperaRange.prototype.selectInternal = function(selection, reversed) { + selection.collapse(this.getStartNode(), this.getStartOffset()); + if(this.getEndNode() != this.getStartNode() || this.getEndOffset() != this.getStartOffset()) { + selection.extend(this.getEndNode(), this.getEndOffset()) + } + if(selection.rangeCount == 0) { + selection.addRange(this.range_) + } +}; +goog.provide("goog.dom.browserrange.WebKitRange"); +goog.require("goog.dom.RangeEndpoint"); +goog.require("goog.dom.browserrange.W3cRange"); +goog.require("goog.userAgent"); +goog.dom.browserrange.WebKitRange = function(range) { + goog.dom.browserrange.W3cRange.call(this, range) +}; +goog.inherits(goog.dom.browserrange.WebKitRange, goog.dom.browserrange.W3cRange); +goog.dom.browserrange.WebKitRange.createFromNodeContents = function(node) { + return new goog.dom.browserrange.WebKitRange(goog.dom.browserrange.W3cRange.getBrowserRangeForNode(node)) +}; +goog.dom.browserrange.WebKitRange.createFromNodes = function(startNode, startOffset, endNode, endOffset) { + return new goog.dom.browserrange.WebKitRange(goog.dom.browserrange.W3cRange.getBrowserRangeForNodes(startNode, startOffset, endNode, endOffset)) +}; +goog.dom.browserrange.WebKitRange.prototype.compareBrowserRangeEndpoints = function(range, thisEndpoint, otherEndpoint) { + if(goog.userAgent.isVersion("528")) { + return goog.dom.browserrange.WebKitRange.superClass_.compareBrowserRangeEndpoints.call(this, range, thisEndpoint, otherEndpoint) + } + return this.range_.compareBoundaryPoints(otherEndpoint == goog.dom.RangeEndpoint.START ? thisEndpoint == goog.dom.RangeEndpoint.START ? goog.global["Range"].START_TO_START : goog.global["Range"].END_TO_START : thisEndpoint == goog.dom.RangeEndpoint.START ? goog.global["Range"].START_TO_END : goog.global["Range"].END_TO_END, range) +}; +goog.dom.browserrange.WebKitRange.prototype.selectInternal = function(selection, reversed) { + selection.removeAllRanges(); + if(reversed) { + selection.setBaseAndExtent(this.getEndNode(), this.getEndOffset(), this.getStartNode(), this.getStartOffset()) + }else { + selection.setBaseAndExtent(this.getStartNode(), this.getStartOffset(), this.getEndNode(), this.getEndOffset()) + } +}; +goog.provide("goog.dom.browserrange"); +goog.provide("goog.dom.browserrange.Error"); +goog.require("goog.dom"); +goog.require("goog.dom.browserrange.GeckoRange"); +goog.require("goog.dom.browserrange.IeRange"); +goog.require("goog.dom.browserrange.OperaRange"); +goog.require("goog.dom.browserrange.W3cRange"); +goog.require("goog.dom.browserrange.WebKitRange"); +goog.require("goog.userAgent"); +goog.dom.browserrange.Error = {NOT_IMPLEMENTED:"Not Implemented"}; +goog.dom.browserrange.createRange = function(range) { + if(goog.userAgent.IE && !goog.userAgent.isVersion("9")) { + return new goog.dom.browserrange.IeRange(range, goog.dom.getOwnerDocument(range.parentElement())) + }else { + if(goog.userAgent.WEBKIT) { + return new goog.dom.browserrange.WebKitRange(range) + }else { + if(goog.userAgent.GECKO) { + return new goog.dom.browserrange.GeckoRange(range) + }else { + if(goog.userAgent.OPERA) { + return new goog.dom.browserrange.OperaRange(range) + }else { + return new goog.dom.browserrange.W3cRange(range) + } + } + } + } +}; +goog.dom.browserrange.createRangeFromNodeContents = function(node) { + if(goog.userAgent.IE && !goog.userAgent.isVersion("9")) { + return goog.dom.browserrange.IeRange.createFromNodeContents(node) + }else { + if(goog.userAgent.WEBKIT) { + return goog.dom.browserrange.WebKitRange.createFromNodeContents(node) + }else { + if(goog.userAgent.GECKO) { + return goog.dom.browserrange.GeckoRange.createFromNodeContents(node) + }else { + if(goog.userAgent.OPERA) { + return goog.dom.browserrange.OperaRange.createFromNodeContents(node) + }else { + return goog.dom.browserrange.W3cRange.createFromNodeContents(node) + } + } + } + } +}; +goog.dom.browserrange.createRangeFromNodes = function(startNode, startOffset, endNode, endOffset) { + if(goog.userAgent.IE && !goog.userAgent.isVersion("9")) { + return goog.dom.browserrange.IeRange.createFromNodes(startNode, startOffset, endNode, endOffset) + }else { + if(goog.userAgent.WEBKIT) { + return goog.dom.browserrange.WebKitRange.createFromNodes(startNode, startOffset, endNode, endOffset) + }else { + if(goog.userAgent.GECKO) { + return goog.dom.browserrange.GeckoRange.createFromNodes(startNode, startOffset, endNode, endOffset) + }else { + if(goog.userAgent.OPERA) { + return goog.dom.browserrange.OperaRange.createFromNodes(startNode, startOffset, endNode, endOffset) + }else { + return goog.dom.browserrange.W3cRange.createFromNodes(startNode, startOffset, endNode, endOffset) + } + } + } + } +}; +goog.dom.browserrange.canContainRangeEndpoint = function(node) { + return goog.dom.canHaveChildren(node) || node.nodeType == goog.dom.NodeType.TEXT +}; +goog.provide("goog.dom.TextRange"); +goog.require("goog.array"); +goog.require("goog.dom"); +goog.require("goog.dom.AbstractRange"); +goog.require("goog.dom.RangeType"); +goog.require("goog.dom.SavedRange"); +goog.require("goog.dom.TagName"); +goog.require("goog.dom.TextRangeIterator"); +goog.require("goog.dom.browserrange"); +goog.require("goog.string"); +goog.require("goog.userAgent"); +goog.dom.TextRange = function() { +}; +goog.inherits(goog.dom.TextRange, goog.dom.AbstractRange); +goog.dom.TextRange.createFromBrowserRange = function(range, opt_isReversed) { + return goog.dom.TextRange.createFromBrowserRangeWrapper_(goog.dom.browserrange.createRange(range), opt_isReversed) +}; +goog.dom.TextRange.createFromBrowserRangeWrapper_ = function(browserRange, opt_isReversed) { + var range = new goog.dom.TextRange; + range.browserRangeWrapper_ = browserRange; + range.isReversed_ = !!opt_isReversed; + return range +}; +goog.dom.TextRange.createFromNodeContents = function(node, opt_isReversed) { + return goog.dom.TextRange.createFromBrowserRangeWrapper_(goog.dom.browserrange.createRangeFromNodeContents(node), opt_isReversed) +}; +goog.dom.TextRange.createFromNodes = function(anchorNode, anchorOffset, focusNode, focusOffset) { + var range = new goog.dom.TextRange; + range.isReversed_ = goog.dom.Range.isReversed(anchorNode, anchorOffset, focusNode, focusOffset); + if(anchorNode.tagName == "BR") { + var parent = anchorNode.parentNode; + anchorOffset = goog.array.indexOf(parent.childNodes, anchorNode); + anchorNode = parent + } + if(focusNode.tagName == "BR") { + var parent = focusNode.parentNode; + focusOffset = goog.array.indexOf(parent.childNodes, focusNode); + focusNode = parent + } + if(range.isReversed_) { + range.startNode_ = focusNode; + range.startOffset_ = focusOffset; + range.endNode_ = anchorNode; + range.endOffset_ = anchorOffset + }else { + range.startNode_ = anchorNode; + range.startOffset_ = anchorOffset; + range.endNode_ = focusNode; + range.endOffset_ = focusOffset + } + return range +}; +goog.dom.TextRange.prototype.browserRangeWrapper_ = null; +goog.dom.TextRange.prototype.startNode_ = null; +goog.dom.TextRange.prototype.startOffset_ = null; +goog.dom.TextRange.prototype.endNode_ = null; +goog.dom.TextRange.prototype.endOffset_ = null; +goog.dom.TextRange.prototype.isReversed_ = false; +goog.dom.TextRange.prototype.clone = function() { + var range = new goog.dom.TextRange; + range.browserRangeWrapper_ = this.browserRangeWrapper_; + range.startNode_ = this.startNode_; + range.startOffset_ = this.startOffset_; + range.endNode_ = this.endNode_; + range.endOffset_ = this.endOffset_; + range.isReversed_ = this.isReversed_; + return range +}; +goog.dom.TextRange.prototype.getType = function() { + return goog.dom.RangeType.TEXT +}; +goog.dom.TextRange.prototype.getBrowserRangeObject = function() { + return this.getBrowserRangeWrapper_().getBrowserRange() +}; +goog.dom.TextRange.prototype.setBrowserRangeObject = function(nativeRange) { + if(goog.dom.AbstractRange.isNativeControlRange(nativeRange)) { + return false + } + this.browserRangeWrapper_ = goog.dom.browserrange.createRange(nativeRange); + this.clearCachedValues_(); + return true +}; +goog.dom.TextRange.prototype.clearCachedValues_ = function() { + this.startNode_ = this.startOffset_ = this.endNode_ = this.endOffset_ = null +}; +goog.dom.TextRange.prototype.getTextRangeCount = function() { + return 1 +}; +goog.dom.TextRange.prototype.getTextRange = function(i) { + return this +}; +goog.dom.TextRange.prototype.getBrowserRangeWrapper_ = function() { + return this.browserRangeWrapper_ || (this.browserRangeWrapper_ = goog.dom.browserrange.createRangeFromNodes(this.getStartNode(), this.getStartOffset(), this.getEndNode(), this.getEndOffset())) +}; +goog.dom.TextRange.prototype.getContainer = function() { + return this.getBrowserRangeWrapper_().getContainer() +}; +goog.dom.TextRange.prototype.getStartNode = function() { + return this.startNode_ || (this.startNode_ = this.getBrowserRangeWrapper_().getStartNode()) +}; +goog.dom.TextRange.prototype.getStartOffset = function() { + return this.startOffset_ != null ? this.startOffset_ : this.startOffset_ = this.getBrowserRangeWrapper_().getStartOffset() +}; +goog.dom.TextRange.prototype.getEndNode = function() { + return this.endNode_ || (this.endNode_ = this.getBrowserRangeWrapper_().getEndNode()) +}; +goog.dom.TextRange.prototype.getEndOffset = function() { + return this.endOffset_ != null ? this.endOffset_ : this.endOffset_ = this.getBrowserRangeWrapper_().getEndOffset() +}; +goog.dom.TextRange.prototype.moveToNodes = function(startNode, startOffset, endNode, endOffset, isReversed) { + this.startNode_ = startNode; + this.startOffset_ = startOffset; + this.endNode_ = endNode; + this.endOffset_ = endOffset; + this.isReversed_ = isReversed; + this.browserRangeWrapper_ = null +}; +goog.dom.TextRange.prototype.isReversed = function() { + return this.isReversed_ +}; +goog.dom.TextRange.prototype.containsRange = function(otherRange, opt_allowPartial) { + var otherRangeType = otherRange.getType(); + if(otherRangeType == goog.dom.RangeType.TEXT) { + return this.getBrowserRangeWrapper_().containsRange(otherRange.getBrowserRangeWrapper_(), opt_allowPartial) + }else { + if(otherRangeType == goog.dom.RangeType.CONTROL) { + var elements = otherRange.getElements(); + var fn = opt_allowPartial ? goog.array.some : goog.array.every; + return fn(elements, function(el) { + return this.containsNode(el, opt_allowPartial) + }, this) + } + } + return false +}; +goog.dom.TextRange.isAttachedNode = function(node) { + if(goog.userAgent.IE) { + var returnValue = false; + try { + returnValue = node.parentNode + }catch(e) { + } + return!!returnValue + }else { + return goog.dom.contains(node.ownerDocument.body, node) + } +}; +goog.dom.TextRange.prototype.isRangeInDocument = function() { + return(!this.startNode_ || goog.dom.TextRange.isAttachedNode(this.startNode_)) && (!this.endNode_ || goog.dom.TextRange.isAttachedNode(this.endNode_)) && (!goog.userAgent.IE || this.getBrowserRangeWrapper_().isRangeInDocument()) +}; +goog.dom.TextRange.prototype.isCollapsed = function() { + return this.getBrowserRangeWrapper_().isCollapsed() +}; +goog.dom.TextRange.prototype.getText = function() { + return this.getBrowserRangeWrapper_().getText() +}; +goog.dom.TextRange.prototype.getHtmlFragment = function() { + return this.getBrowserRangeWrapper_().getHtmlFragment() +}; +goog.dom.TextRange.prototype.getValidHtml = function() { + return this.getBrowserRangeWrapper_().getValidHtml() +}; +goog.dom.TextRange.prototype.getPastableHtml = function() { + var html = this.getValidHtml(); + if(html.match(/^\s*<td\b/i)) { + html = "<table><tbody><tr>" + html + "</tr></tbody></table>" + }else { + if(html.match(/^\s*<tr\b/i)) { + html = "<table><tbody>" + html + "</tbody></table>" + }else { + if(html.match(/^\s*<tbody\b/i)) { + html = "<table>" + html + "</table>" + }else { + if(html.match(/^\s*<li\b/i)) { + var container = this.getContainer(); + var tagType = goog.dom.TagName.UL; + while(container) { + if(container.tagName == goog.dom.TagName.OL) { + tagType = goog.dom.TagName.OL; + break + }else { + if(container.tagName == goog.dom.TagName.UL) { + break + } + } + container = container.parentNode + } + html = goog.string.buildString("<", tagType, ">", html, "</", tagType, ">") + } + } + } + } + return html +}; +goog.dom.TextRange.prototype.__iterator__ = function(opt_keys) { + return new goog.dom.TextRangeIterator(this.getStartNode(), this.getStartOffset(), this.getEndNode(), this.getEndOffset()) +}; +goog.dom.TextRange.prototype.select = function() { + this.getBrowserRangeWrapper_().select(this.isReversed_) +}; +goog.dom.TextRange.prototype.removeContents = function() { + this.getBrowserRangeWrapper_().removeContents(); + this.clearCachedValues_() +}; +goog.dom.TextRange.prototype.surroundContents = function(element) { + var output = this.getBrowserRangeWrapper_().surroundContents(element); + this.clearCachedValues_(); + return output +}; +goog.dom.TextRange.prototype.insertNode = function(node, before) { + var output = this.getBrowserRangeWrapper_().insertNode(node, before); + this.clearCachedValues_(); + return output +}; +goog.dom.TextRange.prototype.surroundWithNodes = function(startNode, endNode) { + this.getBrowserRangeWrapper_().surroundWithNodes(startNode, endNode); + this.clearCachedValues_() +}; +goog.dom.TextRange.prototype.saveUsingDom = function() { + return new goog.dom.DomSavedTextRange_(this) +}; +goog.dom.TextRange.prototype.collapse = function(toAnchor) { + var toStart = this.isReversed() ? !toAnchor : toAnchor; + if(this.browserRangeWrapper_) { + this.browserRangeWrapper_.collapse(toStart) + } + if(toStart) { + this.endNode_ = this.startNode_; + this.endOffset_ = this.startOffset_ + }else { + this.startNode_ = this.endNode_; + this.startOffset_ = this.endOffset_ + } + this.isReversed_ = false +}; +goog.dom.DomSavedTextRange_ = function(range) { + this.anchorNode_ = range.getAnchorNode(); + this.anchorOffset_ = range.getAnchorOffset(); + this.focusNode_ = range.getFocusNode(); + this.focusOffset_ = range.getFocusOffset() +}; +goog.inherits(goog.dom.DomSavedTextRange_, goog.dom.SavedRange); +goog.dom.DomSavedTextRange_.prototype.restoreInternal = function() { + return goog.dom.Range.createFromNodes(this.anchorNode_, this.anchorOffset_, this.focusNode_, this.focusOffset_) +}; +goog.dom.DomSavedTextRange_.prototype.disposeInternal = function() { + goog.dom.DomSavedTextRange_.superClass_.disposeInternal.call(this); + this.anchorNode_ = null; + this.focusNode_ = null +}; +goog.provide("goog.dom.ControlRange"); +goog.provide("goog.dom.ControlRangeIterator"); +goog.require("goog.array"); +goog.require("goog.dom"); +goog.require("goog.dom.AbstractMultiRange"); +goog.require("goog.dom.AbstractRange"); +goog.require("goog.dom.RangeIterator"); +goog.require("goog.dom.RangeType"); +goog.require("goog.dom.SavedRange"); +goog.require("goog.dom.TagWalkType"); +goog.require("goog.dom.TextRange"); +goog.require("goog.iter.StopIteration"); +goog.require("goog.userAgent"); +goog.dom.ControlRange = function() { +}; +goog.inherits(goog.dom.ControlRange, goog.dom.AbstractMultiRange); +goog.dom.ControlRange.createFromBrowserRange = function(controlRange) { + var range = new goog.dom.ControlRange; + range.range_ = controlRange; + return range +}; +goog.dom.ControlRange.createFromElements = function(var_args) { + var range = goog.dom.getOwnerDocument(arguments[0]).body.createControlRange(); + for(var i = 0, len = arguments.length;i < len;i++) { + range.addElement(arguments[i]) + } + return goog.dom.ControlRange.createFromBrowserRange(range) +}; +goog.dom.ControlRange.prototype.range_ = null; +goog.dom.ControlRange.prototype.elements_ = null; +goog.dom.ControlRange.prototype.sortedElements_ = null; +goog.dom.ControlRange.prototype.clearCachedValues_ = function() { + this.elements_ = null; + this.sortedElements_ = null +}; +goog.dom.ControlRange.prototype.clone = function() { + return goog.dom.ControlRange.createFromElements.apply(this, this.getElements()) +}; +goog.dom.ControlRange.prototype.getType = function() { + return goog.dom.RangeType.CONTROL +}; +goog.dom.ControlRange.prototype.getBrowserRangeObject = function() { + return this.range_ || document.body.createControlRange() +}; +goog.dom.ControlRange.prototype.setBrowserRangeObject = function(nativeRange) { + if(!goog.dom.AbstractRange.isNativeControlRange(nativeRange)) { + return false + } + this.range_ = nativeRange; + return true +}; +goog.dom.ControlRange.prototype.getTextRangeCount = function() { + return this.range_ ? this.range_.length : 0 +}; +goog.dom.ControlRange.prototype.getTextRange = function(i) { + return goog.dom.TextRange.createFromNodeContents(this.range_.item(i)) +}; +goog.dom.ControlRange.prototype.getContainer = function() { + return goog.dom.findCommonAncestor.apply(null, this.getElements()) +}; +goog.dom.ControlRange.prototype.getStartNode = function() { + return this.getSortedElements()[0] +}; +goog.dom.ControlRange.prototype.getStartOffset = function() { + return 0 +}; +goog.dom.ControlRange.prototype.getEndNode = function() { + var sorted = this.getSortedElements(); + var startsLast = goog.array.peek(sorted); + return goog.array.find(sorted, function(el) { + return goog.dom.contains(el, startsLast) + }) +}; +goog.dom.ControlRange.prototype.getEndOffset = function() { + return this.getEndNode().childNodes.length +}; +goog.dom.ControlRange.prototype.getElements = function() { + if(!this.elements_) { + this.elements_ = []; + if(this.range_) { + for(var i = 0;i < this.range_.length;i++) { + this.elements_.push(this.range_.item(i)) + } + } + } + return this.elements_ +}; +goog.dom.ControlRange.prototype.getSortedElements = function() { + if(!this.sortedElements_) { + this.sortedElements_ = this.getElements().concat(); + this.sortedElements_.sort(function(a, b) { + return a.sourceIndex - b.sourceIndex + }) + } + return this.sortedElements_ +}; +goog.dom.ControlRange.prototype.isRangeInDocument = function() { + var returnValue = false; + try { + returnValue = goog.array.every(this.getElements(), function(element) { + return goog.userAgent.IE ? element.parentNode : goog.dom.contains(element.ownerDocument.body, element) + }) + }catch(e) { + } + return returnValue +}; +goog.dom.ControlRange.prototype.isCollapsed = function() { + return!this.range_ || !this.range_.length +}; +goog.dom.ControlRange.prototype.getText = function() { + return"" +}; +goog.dom.ControlRange.prototype.getHtmlFragment = function() { + return goog.array.map(this.getSortedElements(), goog.dom.getOuterHtml).join("") +}; +goog.dom.ControlRange.prototype.getValidHtml = function() { + return this.getHtmlFragment() +}; +goog.dom.ControlRange.prototype.getPastableHtml = goog.dom.ControlRange.prototype.getValidHtml; +goog.dom.ControlRange.prototype.__iterator__ = function(opt_keys) { + return new goog.dom.ControlRangeIterator(this) +}; +goog.dom.ControlRange.prototype.select = function() { + if(this.range_) { + this.range_.select() + } +}; +goog.dom.ControlRange.prototype.removeContents = function() { + if(this.range_) { + var nodes = []; + for(var i = 0, len = this.range_.length;i < len;i++) { + nodes.push(this.range_.item(i)) + } + goog.array.forEach(nodes, goog.dom.removeNode); + this.collapse(false) + } +}; +goog.dom.ControlRange.prototype.replaceContentsWithNode = function(node) { + var result = this.insertNode(node, true); + if(!this.isCollapsed()) { + this.removeContents() + } + return result +}; +goog.dom.ControlRange.prototype.saveUsingDom = function() { + return new goog.dom.DomSavedControlRange_(this) +}; +goog.dom.ControlRange.prototype.collapse = function(toAnchor) { + this.range_ = null; + this.clearCachedValues_() +}; +goog.dom.DomSavedControlRange_ = function(range) { + this.elements_ = range.getElements() +}; +goog.inherits(goog.dom.DomSavedControlRange_, goog.dom.SavedRange); +goog.dom.DomSavedControlRange_.prototype.restoreInternal = function() { + var doc = this.elements_.length ? goog.dom.getOwnerDocument(this.elements_[0]) : document; + var controlRange = doc.body.createControlRange(); + for(var i = 0, len = this.elements_.length;i < len;i++) { + controlRange.addElement(this.elements_[i]) + } + return goog.dom.ControlRange.createFromBrowserRange(controlRange) +}; +goog.dom.DomSavedControlRange_.prototype.disposeInternal = function() { + goog.dom.DomSavedControlRange_.superClass_.disposeInternal.call(this); + delete this.elements_ +}; +goog.dom.ControlRangeIterator = function(range) { + if(range) { + this.elements_ = range.getSortedElements(); + this.startNode_ = this.elements_.shift(); + this.endNode_ = goog.array.peek(this.elements_) || this.startNode_ + } + goog.dom.RangeIterator.call(this, this.startNode_, false) +}; +goog.inherits(goog.dom.ControlRangeIterator, goog.dom.RangeIterator); +goog.dom.ControlRangeIterator.prototype.startNode_ = null; +goog.dom.ControlRangeIterator.prototype.endNode_ = null; +goog.dom.ControlRangeIterator.prototype.elements_ = null; +goog.dom.ControlRangeIterator.prototype.getStartTextOffset = function() { + return 0 +}; +goog.dom.ControlRangeIterator.prototype.getEndTextOffset = function() { + return 0 +}; +goog.dom.ControlRangeIterator.prototype.getStartNode = function() { + return this.startNode_ +}; +goog.dom.ControlRangeIterator.prototype.getEndNode = function() { + return this.endNode_ +}; +goog.dom.ControlRangeIterator.prototype.isLast = function() { + return!this.depth && !this.elements_.length +}; +goog.dom.ControlRangeIterator.prototype.next = function() { + if(this.isLast()) { + throw goog.iter.StopIteration; + }else { + if(!this.depth) { + var el = this.elements_.shift(); + this.setPosition(el, goog.dom.TagWalkType.START_TAG, goog.dom.TagWalkType.START_TAG); + return el + } + } + return goog.dom.ControlRangeIterator.superClass_.next.call(this) +}; +goog.dom.ControlRangeIterator.prototype.copyFrom = function(other) { + this.elements_ = other.elements_; + this.startNode_ = other.startNode_; + this.endNode_ = other.endNode_; + goog.dom.ControlRangeIterator.superClass_.copyFrom.call(this, other) +}; +goog.dom.ControlRangeIterator.prototype.clone = function() { + var copy = new goog.dom.ControlRangeIterator(null); + copy.copyFrom(this); + return copy +}; +goog.provide("goog.dom.MultiRange"); +goog.provide("goog.dom.MultiRangeIterator"); +goog.require("goog.array"); +goog.require("goog.debug.Logger"); +goog.require("goog.dom.AbstractMultiRange"); +goog.require("goog.dom.AbstractRange"); +goog.require("goog.dom.RangeIterator"); +goog.require("goog.dom.RangeType"); +goog.require("goog.dom.SavedRange"); +goog.require("goog.dom.TextRange"); +goog.require("goog.iter.StopIteration"); +goog.dom.MultiRange = function() { + this.browserRanges_ = []; + this.ranges_ = []; + this.sortedRanges_ = null; + this.container_ = null +}; +goog.inherits(goog.dom.MultiRange, goog.dom.AbstractMultiRange); +goog.dom.MultiRange.createFromBrowserSelection = function(selection) { + var range = new goog.dom.MultiRange; + for(var i = 0, len = selection.rangeCount;i < len;i++) { + range.browserRanges_.push(selection.getRangeAt(i)) + } + return range +}; +goog.dom.MultiRange.createFromBrowserRanges = function(browserRanges) { + var range = new goog.dom.MultiRange; + range.browserRanges_ = goog.array.clone(browserRanges); + return range +}; +goog.dom.MultiRange.createFromTextRanges = function(textRanges) { + var range = new goog.dom.MultiRange; + range.ranges_ = textRanges; + range.browserRanges_ = goog.array.map(textRanges, function(range) { + return range.getBrowserRangeObject() + }); + return range +}; +goog.dom.MultiRange.prototype.logger_ = goog.debug.Logger.getLogger("goog.dom.MultiRange"); +goog.dom.MultiRange.prototype.clearCachedValues_ = function() { + this.ranges_ = []; + this.sortedRanges_ = null; + this.container_ = null +}; +goog.dom.MultiRange.prototype.clone = function() { + return goog.dom.MultiRange.createFromBrowserRanges(this.browserRanges_) +}; +goog.dom.MultiRange.prototype.getType = function() { + return goog.dom.RangeType.MULTI +}; +goog.dom.MultiRange.prototype.getBrowserRangeObject = function() { + if(this.browserRanges_.length > 1) { + this.logger_.warning("getBrowserRangeObject called on MultiRange with more than 1 range") + } + return this.browserRanges_[0] +}; +goog.dom.MultiRange.prototype.setBrowserRangeObject = function(nativeRange) { + return false +}; +goog.dom.MultiRange.prototype.getTextRangeCount = function() { + return this.browserRanges_.length +}; +goog.dom.MultiRange.prototype.getTextRange = function(i) { + if(!this.ranges_[i]) { + this.ranges_[i] = goog.dom.TextRange.createFromBrowserRange(this.browserRanges_[i]) + } + return this.ranges_[i] +}; +goog.dom.MultiRange.prototype.getContainer = function() { + if(!this.container_) { + var nodes = []; + for(var i = 0, len = this.getTextRangeCount();i < len;i++) { + nodes.push(this.getTextRange(i).getContainer()) + } + this.container_ = goog.dom.findCommonAncestor.apply(null, nodes) + } + return this.container_ +}; +goog.dom.MultiRange.prototype.getSortedRanges = function() { + if(!this.sortedRanges_) { + this.sortedRanges_ = this.getTextRanges(); + this.sortedRanges_.sort(function(a, b) { + var aStartNode = a.getStartNode(); + var aStartOffset = a.getStartOffset(); + var bStartNode = b.getStartNode(); + var bStartOffset = b.getStartOffset(); + if(aStartNode == bStartNode && aStartOffset == bStartOffset) { + return 0 + } + return goog.dom.Range.isReversed(aStartNode, aStartOffset, bStartNode, bStartOffset) ? 1 : -1 + }) + } + return this.sortedRanges_ +}; +goog.dom.MultiRange.prototype.getStartNode = function() { + return this.getSortedRanges()[0].getStartNode() +}; +goog.dom.MultiRange.prototype.getStartOffset = function() { + return this.getSortedRanges()[0].getStartOffset() +}; +goog.dom.MultiRange.prototype.getEndNode = function() { + return goog.array.peek(this.getSortedRanges()).getEndNode() +}; +goog.dom.MultiRange.prototype.getEndOffset = function() { + return goog.array.peek(this.getSortedRanges()).getEndOffset() +}; +/* +goog.dom.MultiRange.prototype.isRangeInDocument = function() { + return goog.array.every(this.getTextRanges(), function(range) { + return range.isRangeInDocument() + }) +}; +*/ +goog.dom.MultiRange.prototype.isCollapsed = function() { + return this.browserRanges_.length == 0 || this.browserRanges_.length == 1 && this.getTextRange(0).isCollapsed() +}; +goog.dom.MultiRange.prototype.getText = function() { + return goog.array.map(this.getTextRanges(), function(range) { + return range.getText() + }).join("") +}; +goog.dom.MultiRange.prototype.getHtmlFragment = function() { + return this.getValidHtml() +}; +goog.dom.MultiRange.prototype.getValidHtml = function() { + return goog.array.map(this.getTextRanges(), function(range) { + return range.getValidHtml() + }).join("") +}; +goog.dom.MultiRange.prototype.getPastableHtml = function() { + return this.getValidHtml() +}; +goog.dom.MultiRange.prototype.__iterator__ = function(opt_keys) { + return new goog.dom.MultiRangeIterator(this) +}; +goog.dom.MultiRange.prototype.select = function() { + var selection = goog.dom.AbstractRange.getBrowserSelectionForWindow(this.getWindow()); + selection.removeAllRanges(); + for(var i = 0, len = this.getTextRangeCount();i < len;i++) { + selection.addRange(this.getTextRange(i).getBrowserRangeObject()) + } +}; +goog.dom.MultiRange.prototype.removeContents = function() { + goog.array.forEach(this.getTextRanges(), function(range) { + range.removeContents() + }) +}; +goog.dom.MultiRange.prototype.saveUsingDom = function() { + return new goog.dom.DomSavedMultiRange_(this) +}; +goog.dom.MultiRange.prototype.collapse = function(toAnchor) { + if(!this.isCollapsed()) { + var range = toAnchor ? this.getTextRange(0) : this.getTextRange(this.getTextRangeCount() - 1); + this.clearCachedValues_(); + range.collapse(toAnchor); + this.ranges_ = [range]; + this.sortedRanges_ = [range]; + this.browserRanges_ = [range.getBrowserRangeObject()] + } +}; +goog.dom.DomSavedMultiRange_ = function(range) { + this.savedRanges_ = goog.array.map(range.getTextRanges(), function(range) { + return range.saveUsingDom() + }) +}; +goog.inherits(goog.dom.DomSavedMultiRange_, goog.dom.SavedRange); +goog.dom.DomSavedMultiRange_.prototype.restoreInternal = function() { + var ranges = goog.array.map(this.savedRanges_, function(savedRange) { + return savedRange.restore() + }); + return goog.dom.MultiRange.createFromTextRanges(ranges) +}; +goog.dom.DomSavedMultiRange_.prototype.disposeInternal = function() { + goog.dom.DomSavedMultiRange_.superClass_.disposeInternal.call(this); + goog.array.forEach(this.savedRanges_, function(savedRange) { + savedRange.dispose() + }); + delete this.savedRanges_ +}; +goog.dom.MultiRangeIterator = function(range) { + if(range) { + this.iterators_ = goog.array.map(range.getSortedRanges(), function(r) { + return goog.iter.toIterator(r) + }) + } + goog.dom.RangeIterator.call(this, range ? this.getStartNode() : null, false) +}; +goog.inherits(goog.dom.MultiRangeIterator, goog.dom.RangeIterator); +goog.dom.MultiRangeIterator.prototype.iterators_ = null; +goog.dom.MultiRangeIterator.prototype.currentIdx_ = 0; +goog.dom.MultiRangeIterator.prototype.getStartTextOffset = function() { + return this.iterators_[this.currentIdx_].getStartTextOffset() +}; +goog.dom.MultiRangeIterator.prototype.getEndTextOffset = function() { + return this.iterators_[this.currentIdx_].getEndTextOffset() +}; +goog.dom.MultiRangeIterator.prototype.getStartNode = function() { + return this.iterators_[0].getStartNode() +}; +goog.dom.MultiRangeIterator.prototype.getEndNode = function() { + return goog.array.peek(this.iterators_).getEndNode() +}; +goog.dom.MultiRangeIterator.prototype.isLast = function() { + return this.iterators_[this.currentIdx_].isLast() +}; +goog.dom.MultiRangeIterator.prototype.next = function() { + try { + var it = this.iterators_[this.currentIdx_]; + var next = it.next(); + this.setPosition(it.node, it.tagType, it.depth); + return next + }catch(ex) { + if(ex !== goog.iter.StopIteration || this.iterators_.length - 1 == this.currentIdx_) { + throw ex; + }else { + this.currentIdx_++; + return this.next() + } + } +}; +goog.dom.MultiRangeIterator.prototype.copyFrom = function(other) { + this.iterators_ = goog.array.clone(other.iterators_); + goog.dom.MultiRangeIterator.superClass_.copyFrom.call(this, other) +}; +goog.dom.MultiRangeIterator.prototype.clone = function() { + var copy = new goog.dom.MultiRangeIterator(null); + copy.copyFrom(this); + return copy +}; +goog.provide("goog.dom.Range"); +goog.require("goog.dom"); +goog.require("goog.dom.AbstractRange"); +goog.require("goog.dom.ControlRange"); +goog.require("goog.dom.MultiRange"); +goog.require("goog.dom.NodeType"); +goog.require("goog.dom.TextRange"); +goog.require("goog.userAgent"); +goog.dom.Range.createFromWindow = function(opt_win) { + var sel = goog.dom.AbstractRange.getBrowserSelectionForWindow(opt_win || window); + return sel && goog.dom.Range.createFromBrowserSelection(sel) +}; +goog.dom.Range.createFromBrowserSelection = function(selection) { + var range; + var isReversed = false; + if(selection.createRange) { + try { + range = selection.createRange() + }catch(e) { + return null + } + }else { + if(selection.rangeCount) { + if(selection.rangeCount > 1) { + return goog.dom.MultiRange.createFromBrowserSelection(selection) + }else { + range = selection.getRangeAt(0); + isReversed = goog.dom.Range.isReversed(selection.anchorNode, selection.anchorOffset, selection.focusNode, selection.focusOffset) + } + }else { + return null + } + } + return goog.dom.Range.createFromBrowserRange(range, isReversed) +}; +goog.dom.Range.createFromBrowserRange = function(range, opt_isReversed) { + return goog.dom.AbstractRange.isNativeControlRange(range) ? goog.dom.ControlRange.createFromBrowserRange(range) : goog.dom.TextRange.createFromBrowserRange(range, opt_isReversed) +}; +goog.dom.Range.createFromNodeContents = function(node, opt_isReversed) { + return goog.dom.TextRange.createFromNodeContents(node, opt_isReversed) +}; +goog.dom.Range.createCaret = function(node, offset) { + return goog.dom.TextRange.createFromNodes(node, offset, node, offset) +}; +goog.dom.Range.createFromNodes = function(startNode, startOffset, endNode, endOffset) { + return goog.dom.TextRange.createFromNodes(startNode, startOffset, endNode, endOffset) +}; +goog.dom.Range.clearSelection = function(opt_win) { + var sel = goog.dom.AbstractRange.getBrowserSelectionForWindow(opt_win || window); + if(!sel) { + return + } + if(sel.empty) { + try { + sel.empty() + }catch(e) { + } + }else { + sel.removeAllRanges() + } +}; +goog.dom.Range.hasSelection = function(opt_win) { + var sel = goog.dom.AbstractRange.getBrowserSelectionForWindow(opt_win || window); + return!!sel && (goog.userAgent.IE ? sel.type != "None" : !!sel.rangeCount) +}; +goog.dom.Range.isReversed = function(anchorNode, anchorOffset, focusNode, focusOffset) { + if(anchorNode == focusNode) { + return focusOffset < anchorOffset + } + var child; + if(anchorNode.nodeType == goog.dom.NodeType.ELEMENT && anchorOffset) { + child = anchorNode.childNodes[anchorOffset]; + if(child) { + anchorNode = child; + anchorOffset = 0 + }else { + if(goog.dom.contains(anchorNode, focusNode)) { + return true + } + } + } + if(focusNode.nodeType == goog.dom.NodeType.ELEMENT && focusOffset) { + child = focusNode.childNodes[focusOffset]; + if(child) { + focusNode = child; + focusOffset = 0 + }else { + if(goog.dom.contains(focusNode, anchorNode)) { + return false + } + } + } + return(goog.dom.compareNodeOrder(anchorNode, focusNode) || anchorOffset - focusOffset) > 0 +}; +window.createFromWindow = goog.dom.Range.createFromWindow; +window.createFromNodes = goog.dom.Range.createFromNodes; +window.createCaret = goog.dom.Range.createCaret;
\ No newline at end of file diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/run.js b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/run.js new file mode 100644 index 0000000000..6e2acd937c --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/run.js @@ -0,0 +1,383 @@ +/** + * @fileoverview + * Main functions used in running the RTE test suite. + * + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the 'License') + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @version 0.1 + * @author rolandsteiner@google.com + */ + +/** + * Info function: returns true if the suite (mainly) tests the result HTML/Text. + * + * @param suite {String} the test suite + * @return {boolean} Whether the suite main focus is the output HTML/Text + */ +function suiteChecksHTMLOrText(suite) { + return suite.id[0] != 'S'; +} + +/** + * Info function: returns true if the suite checks the result selection. + * + * @param suite {String} the test suite + * @return {boolean} Whether the suite checks the selection + */ +function suiteChecksSelection(suite) { + return suite.id[0] != 'Q'; +} + +/** + * Helper function returning the effective value of a test parameter. + * + * @param suite {Object} the test suite + * @param group {Object} group of tests within the suite the test belongs to + * @param test {Object} the test + * @param param {String} the test parameter to be checked + * @return {Any} the effective value of the parameter (can be undefined) + */ +function getTestParameter(suite, group, test, param) { + var val = test[param]; + if (val === undefined) { + val = group[param]; + } + if (val === undefined) { + val = suite[param]; + } + return val; +} + +/** + * Helper function returning the effective value of a container/test parameter. + * + * @param suite {Object} the test suite + * @param group {Object} group of tests within the suite the test belongs to + * @param test {Object} the test + * @param container {Object} the container descriptor object + * @param param {String} the test parameter to be checked + * @return {Any} the effective value of the parameter (can be undefined) + */ +function getContainerParameter(suite, group, test, container, param) { + var val = undefined; + if (test[container.id]) { + val = test[container.id][param]; + } + if (val === undefined) { + val = test[param]; + } + if (val === undefined) { + val = group[param]; + } + if (val === undefined) { + val = suite[param]; + } + return val; +} + +/** + * Initializes the global variables before any tests are run. + */ +function initVariables() { + results = { + count: 0, + valscore: 0, + selscore: 0 + }; +} + +/** + * Runs a single test - outputs and sets the result variables. + * + * @param suite {Object} suite that test originates in as object reference + * @param group {Object} group of tests within the suite the test belongs to + * @param test {Object} test to be run as object reference + * @param container {Object} container descriptor as object reference + * @see variables.js for RESULT... values + */ +function runSingleTest(suite, group, test, container) { + var result = { + valscore: 0, + selscore: 0, + valresult: VALRESULT_NOT_RUN, + selresult: SELRESULT_NOT_RUN, + output: '' + }; + + // 1.) Populate the editor element with the initial test setup HTML. + try { + initContainer(suite, group, test, container); + } catch(ex) { + result.valresult = VALRESULT_SETUP_EXCEPTION; + result.selresult = SELRESULT_NA; + result.output = SETUP_EXCEPTION + ex.toString(); + return result; + } + + // 2.) Run the test command, general function or query function. + var isHTMLTest = false; + + try { + var cmd = undefined; + + if (cmd = getTestParameter(suite, group, test, PARAM_EXECCOMMAND)) { + isHTMLTest = true; + // Note: "getTestParameter(suite, group, test, PARAM_VALUE) || null" + // doesn't work, since value might be the empty string, e.g., for 'insertText'! + var value = getTestParameter(suite, group, test, PARAM_VALUE); + if (value === undefined) { + value = null; + } + container.doc.execCommand(cmd, false, value); + } else if (cmd = getTestParameter(suite, group, test, PARAM_FUNCTION)) { + isHTMLTest = true; + eval(cmd); + } else if (cmd = getTestParameter(suite, group, test, PARAM_QUERYCOMMANDSUPPORTED)) { + result.output = container.doc.queryCommandSupported(cmd); + } else if (cmd = getTestParameter(suite, group, test, PARAM_QUERYCOMMANDENABLED)) { + result.output = container.doc.queryCommandEnabled(cmd); + } else if (cmd = getTestParameter(suite, group, test, PARAM_QUERYCOMMANDINDETERM)) { + result.output = container.doc.queryCommandIndeterm(cmd); + } else if (cmd = getTestParameter(suite, group, test, PARAM_QUERYCOMMANDSTATE)) { + result.output = container.doc.queryCommandState(cmd); + } else if (cmd = getTestParameter(suite, group, test, PARAM_QUERYCOMMANDVALUE)) { + result.output = container.doc.queryCommandValue(cmd); + if (result.output === false) { + // A return value of boolean 'false' for queryCommandValue means 'not supported'. + result.valresult = VALRESULT_UNSUPPORTED; + result.selresult = SELRESULT_NA; + result.output = UNSUPPORTED; + return result; + } + } else { + result.valresult = VALRESULT_SETUP_EXCEPTION; + result.selresult = SELRESULT_NA; + result.output = SETUP_EXCEPTION + SETUP_NOCOMMAND; + return result; + } + } catch (ex) { + result.valresult = VALRESULT_EXECUTION_EXCEPTION; + result.selresult = SELRESULT_NA; + result.output = EXECUTION_EXCEPTION + ex.toString(); + return result; + } + + // 4.) Verify test result + try { + if (isHTMLTest) { + // First, retrieve HTML from container + prepareHTMLTestResult(container, result); + + // Compare result to expectations + compareHTMLTestResult(suite, group, test, container, result); + + result.valscore = (result.valresult === VALRESULT_EQUAL) ? 1 : 0; + result.selscore = (result.selresult === SELRESULT_EQUAL) ? 1 : 0; + } else { + compareTextTestResult(suite, group, test, result); + + result.selresult = SELRESULT_NA; + result.valscore = (result.valresult === VALRESULT_EQUAL) ? 1 : 0; + } + } catch (ex) { + result.valresult = VALRESULT_VERIFICATION_EXCEPTION; + result.selresult = SELRESULT_NA; + result.output = VERIFICATION_EXCEPTION + ex.toString(); + return result; + } + + return result; +} + +/** + * Initializes the results dictionary for a given test suite. + * (for all classes -> tests -> containers) + * + * @param {Object} suite as object reference + */ +function initTestSuiteResults(suite) { + var suiteID = suite.id; + + // Initialize results entries for this suite + results[suiteID] = { + count: 0, + valscore: 0, + selscore: 0, + time: 0 + }; + var totalTestCount = 0; + + for (var clsIdx = 0; clsIdx < testClassCount; ++clsIdx) { + var clsID = testClassIDs[clsIdx]; + var cls = suite[clsID]; + if (!cls) + continue; + + results[suiteID][clsID] = { + count: 0, + valscore: 0, + selscore: 0 + }; + var clsTestCount = 0; + + var groupCount = cls.length; + for (var groupIdx = 0; groupIdx < groupCount; ++groupIdx) { + var group = cls[groupIdx]; + var testCount = group.tests.length; + + clsTestCount += testCount; + totalTestCount += testCount; + + for (var testIdx = 0; testIdx < testCount; ++testIdx) { + var test = group.tests[testIdx]; + + results[suiteID][clsID ][test.id] = { + valscore: 0, + selscore: 0, + valresult: VALRESULT_NOT_RUN, + selresult: SELRESULT_NOT_RUN + }; + for (var cntIdx = 0; cntIdx < containers.length; ++cntIdx) { + var cntID = containers[cntIdx].id; + + results[suiteID][clsID][test.id][cntID] = { + valscore: 0, + selscore: 0, + valresult: VALRESULT_NOT_RUN, + selresult: SELRESULT_NOT_RUN, + output: '' + } + } + } + } + results[suiteID][clsID].count = clsTestCount; + } + results[suiteID].count = totalTestCount; +} + +/** + * Runs a single test suite (such as DELETE tests or INSERT tests). + * + * @param suite {Object} suite as object reference + */ +function runTestSuite(suite) { + var suiteID = suite.id; + var suiteStartTime = new Date().getTime(); + + initTestSuiteResults(suite); + + for (var clsIdx = 0; clsIdx < testClassCount; ++clsIdx) { + var clsID = testClassIDs[clsIdx]; + var cls = suite[clsID]; + if (!cls) + continue; + + var groupCount = cls.length; + + for (var groupIdx = 0; groupIdx < groupCount; ++groupIdx) { + var group = cls[groupIdx]; + var testCount = group.tests.length; + + for (var testIdx = 0; testIdx < testCount; ++testIdx) { + var test = group.tests[testIdx]; + + var valscore = 1; + var selscore = 1; + var valresult = VALRESULT_EQUAL; + var selresult = SELRESULT_EQUAL; + + for (var cntIdx = 0; cntIdx < containers.length; ++cntIdx) { + var container = containers[cntIdx]; + var cntID = container.id; + + var result = runSingleTest(suite, group, test, container); + + results[suiteID][clsID][test.id][cntID] = result; + + valscore = Math.min(valscore, result.valscore); + selscore = Math.min(selscore, result.selscore); + valresult = Math.min(valresult, result.valresult); + selresult = Math.min(selresult, result.selresult); + + resetContainer(container); + } + + results[suiteID][clsID][test.id].valscore = valscore; + results[suiteID][clsID][test.id].selscore = selscore; + results[suiteID][clsID][test.id].valresult = valresult; + results[suiteID][clsID][test.id].selresult = selresult; + + results[suiteID][clsID].valscore += valscore; + results[suiteID][clsID].selscore += selscore; + results[suiteID].valscore += valscore; + results[suiteID].selscore += selscore; + results.valscore += valscore; + results.selscore += selscore; + } + } + } + + results[suiteID].time = new Date().getTime() - suiteStartTime; +} + +/** + * Runs a single test suite (such as DELETE tests or INSERT tests) + * and updates the output HTML. + * + * @param {Object} suite as object reference + */ +function runAndOutputTestSuite(suite) { + runTestSuite(suite); + outputTestSuiteResults(suite); +} + +/** + * Fills the beacon with the test results. + */ +function fillResults() { + // Result totals of the individual categories + categoryTotals = [ + 'selection=' + results['S'].selscore, + 'apply=' + results['A'].valscore, + 'applyCSS=' + results['AC'].valscore, + 'change=' + results['C'].valscore, + 'changeCSS=' + results['CC'].valscore, + 'unapply=' + results['U'].valscore, + 'unapplyCSS=' + results['UC'].valscore, + 'delete=' + results['D'].valscore, + 'forwarddelete=' + results['FD'].valscore, + 'insert=' + results['I'].valscore, + 'selectionResult=' + (results['A'].selscore + + results['AC'].selscore + + results['C'].selscore + + results['CC'].selscore + + results['U'].selscore + + results['UC'].selscore + + results['D'].selscore + + results['FD'].selscore + + results['I'].selscore), + 'querySupported=' + results['Q'].valscore, + 'queryEnabled=' + results['QE'].valscore, + 'queryIndeterm=' + results['QI'].valscore, + 'queryState=' + results['QS'].valscore, + 'queryStateCSS=' + results['QSC'].valscore, + 'queryValue=' + results['QV'].valscore, + 'queryValueCSS=' + results['QVC'].valscore + ]; + + // Beacon copies category results + beacon = categoryTotals.slice(0); +} + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/units.js b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/units.js new file mode 100644 index 0000000000..f2c23fbe50 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/units.js @@ -0,0 +1,416 @@ +/** + * @fileoverview + * Common constants and variables used in the RTE test suite. + * + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the 'License') + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @version 0.1 + * @author rolandsteiner@google.com + */ + +// All colors defined in CSS3. +var colorChart = { + 'aliceblue': {red: 0xF0, green: 0xF8, blue: 0xFF}, + 'antiquewhite': {red: 0xFA, green: 0xEB, blue: 0xD7}, + 'aqua': {red: 0x00, green: 0xFF, blue: 0xFF}, + 'aquamarine': {red: 0x7F, green: 0xFF, blue: 0xD4}, + 'azure': {red: 0xF0, green: 0xFF, blue: 0xFF}, + 'beige': {red: 0xF5, green: 0xF5, blue: 0xDC}, + 'bisque': {red: 0xFF, green: 0xE4, blue: 0xC4}, + 'black': {red: 0x00, green: 0x00, blue: 0x00}, + 'blanchedalmond': {red: 0xFF, green: 0xEB, blue: 0xCD}, + 'blue': {red: 0x00, green: 0x00, blue: 0xFF}, + 'blueviolet': {red: 0x8A, green: 0x2B, blue: 0xE2}, + 'brown': {red: 0xA5, green: 0x2A, blue: 0x2A}, + 'burlywood': {red: 0xDE, green: 0xB8, blue: 0x87}, + 'cadetblue': {red: 0x5F, green: 0x9E, blue: 0xA0}, + 'chartreuse': {red: 0x7F, green: 0xFF, blue: 0x00}, + 'chocolate': {red: 0xD2, green: 0x69, blue: 0x1E}, + 'coral': {red: 0xFF, green: 0x7F, blue: 0x50}, + 'cornflowerblue': {red: 0x64, green: 0x95, blue: 0xED}, + 'cornsilk': {red: 0xFF, green: 0xF8, blue: 0xDC}, + 'crimson': {red: 0xDC, green: 0x14, blue: 0x3C}, + 'cyan': {red: 0x00, green: 0xFF, blue: 0xFF}, + 'darkblue': {red: 0x00, green: 0x00, blue: 0x8B}, + 'darkcyan': {red: 0x00, green: 0x8B, blue: 0x8B}, + 'darkgoldenrod': {red: 0xB8, green: 0x86, blue: 0x0B}, + 'darkgray': {red: 0xA9, green: 0xA9, blue: 0xA9}, + 'darkgreen': {red: 0x00, green: 0x64, blue: 0x00}, + 'darkgrey': {red: 0xA9, green: 0xA9, blue: 0xA9}, + 'darkkhaki': {red: 0xBD, green: 0xB7, blue: 0x6B}, + 'darkmagenta': {red: 0x8B, green: 0x00, blue: 0x8B}, + 'darkolivegreen': {red: 0x55, green: 0x6B, blue: 0x2F}, + 'darkorange': {red: 0xFF, green: 0x8C, blue: 0x00}, + 'darkorchid': {red: 0x99, green: 0x32, blue: 0xCC}, + 'darkred': {red: 0x8B, green: 0x00, blue: 0x00}, + 'darksalmon': {red: 0xE9, green: 0x96, blue: 0x7A}, + 'darkseagreen': {red: 0x8F, green: 0xBC, blue: 0x8F}, + 'darkslateblue': {red: 0x48, green: 0x3D, blue: 0x8B}, + 'darkslategray': {red: 0x2F, green: 0x4F, blue: 0x4F}, + 'darkslategrey': {red: 0x2F, green: 0x4F, blue: 0x4F}, + 'darkturquoise': {red: 0x00, green: 0xCE, blue: 0xD1}, + 'darkviolet': {red: 0x94, green: 0x00, blue: 0xD3}, + 'deeppink': {red: 0xFF, green: 0x14, blue: 0x93}, + 'deepskyblue': {red: 0x00, green: 0xBF, blue: 0xFF}, + 'dimgray': {red: 0x69, green: 0x69, blue: 0x69}, + 'dimgrey': {red: 0x69, green: 0x69, blue: 0x69}, + 'dodgerblue': {red: 0x1E, green: 0x90, blue: 0xFF}, + 'firebrick': {red: 0xB2, green: 0x22, blue: 0x22}, + 'floralwhite': {red: 0xFF, green: 0xFA, blue: 0xF0}, + 'forestgreen': {red: 0x22, green: 0x8B, blue: 0x22}, + 'fuchsia': {red: 0xFF, green: 0x00, blue: 0xFF}, + 'gainsboro': {red: 0xDC, green: 0xDC, blue: 0xDC}, + 'ghostwhite': {red: 0xF8, green: 0xF8, blue: 0xFF}, + 'gold': {red: 0xFF, green: 0xD7, blue: 0x00}, + 'goldenrod': {red: 0xDA, green: 0xA5, blue: 0x20}, + 'gray': {red: 0x80, green: 0x80, blue: 0x80}, + 'green': {red: 0x00, green: 0x80, blue: 0x00}, + 'greenyellow': {red: 0xAD, green: 0xFF, blue: 0x2F}, + 'grey': {red: 0x80, green: 0x80, blue: 0x80}, + 'honeydew': {red: 0xF0, green: 0xFF, blue: 0xF0}, + 'hotpink': {red: 0xFF, green: 0x69, blue: 0xB4}, + 'indianred': {red: 0xCD, green: 0x5C, blue: 0x5C}, + 'indigo': {red: 0x4B, green: 0x00, blue: 0x82}, + 'ivory': {red: 0xFF, green: 0xFF, blue: 0xF0}, + 'khaki': {red: 0xF0, green: 0xE6, blue: 0x8C}, + 'lavender': {red: 0xE6, green: 0xE6, blue: 0xFA}, + 'lavenderblush': {red: 0xFF, green: 0xF0, blue: 0xF5}, + 'lawngreen': {red: 0x7C, green: 0xFC, blue: 0x00}, + 'lemonchiffon': {red: 0xFF, green: 0xFA, blue: 0xCD}, + 'lightblue': {red: 0xAD, green: 0xD8, blue: 0xE6}, + 'lightcoral': {red: 0xF0, green: 0x80, blue: 0x80}, + 'lightcyan': {red: 0xE0, green: 0xFF, blue: 0xFF}, + 'lightgoldenrodyellow': {red: 0xFA, green: 0xFA, blue: 0xD2}, + 'lightgray': {red: 0xD3, green: 0xD3, blue: 0xD3}, + 'lightgreen': {red: 0x90, green: 0xEE, blue: 0x90}, + 'lightgrey': {red: 0xD3, green: 0xD3, blue: 0xD3}, + 'lightpink': {red: 0xFF, green: 0xB6, blue: 0xC1}, + 'lightsalmon': {red: 0xFF, green: 0xA0, blue: 0x7A}, + 'lightseagreen': {red: 0x20, green: 0xB2, blue: 0xAA}, + 'lightskyblue': {red: 0x87, green: 0xCE, blue: 0xFA}, + 'lightslategray': {red: 0x77, green: 0x88, blue: 0x99}, + 'lightslategrey': {red: 0x77, green: 0x88, blue: 0x99}, + 'lightsteelblue': {red: 0xB0, green: 0xC4, blue: 0xDE}, + 'lightyellow': {red: 0xFF, green: 0xFF, blue: 0xE0}, + 'lime': {red: 0x00, green: 0xFF, blue: 0x00}, + 'limegreen': {red: 0x32, green: 0xCD, blue: 0x32}, + 'linen': {red: 0xFA, green: 0xF0, blue: 0xE6}, + 'magenta': {red: 0xFF, green: 0x00, blue: 0xFF}, + 'maroon': {red: 0x80, green: 0x00, blue: 0x00}, + 'mediumaquamarine': {red: 0x66, green: 0xCD, blue: 0xAA}, + 'mediumblue': {red: 0x00, green: 0x00, blue: 0xCD}, + 'mediumorchid': {red: 0xBA, green: 0x55, blue: 0xD3}, + 'mediumpurple': {red: 0x93, green: 0x70, blue: 0xDB}, + 'mediumseagreen': {red: 0x3C, green: 0xB3, blue: 0x71}, + 'mediumslateblue': {red: 0x7B, green: 0x68, blue: 0xEE}, + 'mediumspringgreen': {red: 0x00, green: 0xFA, blue: 0x9A}, + 'mediumturquoise': {red: 0x48, green: 0xD1, blue: 0xCC}, + 'mediumvioletred': {red: 0xC7, green: 0x15, blue: 0x85}, + 'midnightblue': {red: 0x19, green: 0x19, blue: 0x70}, + 'mintcream': {red: 0xF5, green: 0xFF, blue: 0xFA}, + 'mistyrose': {red: 0xFF, green: 0xE4, blue: 0xE1}, + 'moccasin': {red: 0xFF, green: 0xE4, blue: 0xB5}, + 'navajowhite': {red: 0xFF, green: 0xDE, blue: 0xAD}, + 'navy': {red: 0x00, green: 0x00, blue: 0x80}, + 'oldlace': {red: 0xFD, green: 0xF5, blue: 0xE6}, + 'olive': {red: 0x80, green: 0x80, blue: 0x00}, + 'olivedrab': {red: 0x6B, green: 0x8E, blue: 0x23}, + 'orange': {red: 0xFF, green: 0xA5, blue: 0x00}, + 'orangered': {red: 0xFF, green: 0x45, blue: 0x00}, + 'orchid': {red: 0xDA, green: 0x70, blue: 0xD6}, + 'palegoldenrod': {red: 0xEE, green: 0xE8, blue: 0xAA}, + 'palegreen': {red: 0x98, green: 0xFB, blue: 0x98}, + 'paleturquoise': {red: 0xAF, green: 0xEE, blue: 0xEE}, + 'palevioletred': {red: 0xDB, green: 0x70, blue: 0x93}, + 'papayawhip': {red: 0xFF, green: 0xEF, blue: 0xD5}, + 'peachpuff': {red: 0xFF, green: 0xDA, blue: 0xB9}, + 'peru': {red: 0xCD, green: 0x85, blue: 0x3F}, + 'pink': {red: 0xFF, green: 0xC0, blue: 0xCB}, + 'plum': {red: 0xDD, green: 0xA0, blue: 0xDD}, + 'powderblue': {red: 0xB0, green: 0xE0, blue: 0xE6}, + 'purple': {red: 0x80, green: 0x00, blue: 0x80}, + 'red': {red: 0xFF, green: 0x00, blue: 0x00}, + 'rosybrown': {red: 0xBC, green: 0x8F, blue: 0x8F}, + 'royalblue': {red: 0x41, green: 0x69, blue: 0xE1}, + 'saddlebrown': {red: 0x8B, green: 0x45, blue: 0x13}, + 'salmon': {red: 0xFA, green: 0x80, blue: 0x72}, + 'sandybrown': {red: 0xF4, green: 0xA4, blue: 0x60}, + 'seagreen': {red: 0x2E, green: 0x8B, blue: 0x57}, + 'seashell': {red: 0xFF, green: 0xF5, blue: 0xEE}, + 'sienna': {red: 0xA0, green: 0x52, blue: 0x2D}, + 'silver': {red: 0xC0, green: 0xC0, blue: 0xC0}, + 'skyblue': {red: 0x87, green: 0xCE, blue: 0xEB}, + 'slateblue': {red: 0x6A, green: 0x5A, blue: 0xCD}, + 'slategray': {red: 0x70, green: 0x80, blue: 0x90}, + 'slategrey': {red: 0x70, green: 0x80, blue: 0x90}, + 'snow': {red: 0xFF, green: 0xFA, blue: 0xFA}, + 'springgreen': {red: 0x00, green: 0xFF, blue: 0x7F}, + 'steelblue': {red: 0x46, green: 0x82, blue: 0xB4}, + 'tan': {red: 0xD2, green: 0xB4, blue: 0x8C}, + 'teal': {red: 0x00, green: 0x80, blue: 0x80}, + 'thistle': {red: 0xD8, green: 0xBF, blue: 0xD8}, + 'tomato': {red: 0xFF, green: 0x63, blue: 0x47}, + 'turquoise': {red: 0x40, green: 0xE0, blue: 0xD0}, + 'violet': {red: 0xEE, green: 0x82, blue: 0xEE}, + 'wheat': {red: 0xF5, green: 0xDE, blue: 0xB3}, + 'white': {red: 0xFF, green: 0xFF, blue: 0xFF}, + 'whitesmoke': {red: 0xF5, green: 0xF5, blue: 0xF5}, + 'yellow': {red: 0xFF, green: 0xFF, blue: 0x00}, + 'yellowgreen': {red: 0x9A, green: 0xCD, blue: 0x32}, + + 'transparent': {red: 0x00, green: 0x00, blue: 0x00, alpha: 0.0} +}; + +/** + * Color class allows cross-browser comparison of values, which can + * be returned from queryCommandValue in several formats: + * #ff00ff + * #f0f + * rgb(255, 0, 0) + * rgb(100%, 0%, 28%) // disabled for the time being (see below) + * rgba(127, 0, 64, 0.25) + * rgba(50%, 0%, 10%, 0.65) // disabled for the time being (see below) + * palegoldenrod + * transparent + * + * @constructor + * @param value {String} original value + */ +function Color(value) { + this.compare = function(other) { + if (!this.valid || !other.valid) { + return false; + } + if (this.alpha != other.alpha) { + return false; + } + if (this.alpha == 0.0) { + // both are fully transparent -> ignore the specific color information + return true; + } + // TODO(rolandsteiner): handle hsl/hsla values + return this.red == other.red && this.green == other.green && this.blue == other.blue; + } + this.parse = function(value) { + if (!value) + return false; + value = String(value).toLowerCase(); + var match; + // '#' + 6 hex digits, e.g., #ff3300 + match = value.match(/#([0-9a-f]{6})/i); + if (match) { + this.red = parseInt(match[1].substring(0, 2), 16); + this.green = parseInt(match[1].substring(2, 4), 16); + this.blue = parseInt(match[1].substring(4, 6), 16); + this.alpha = 1.0; + return true; + } + // '#' + 3 hex digits, e.g., #f30 + match = value.match(/#([0-9a-f]{3})/i); + if (match) { + this.red = parseInt(match[1].substring(0, 1), 16) * 16; + this.green = parseInt(match[1].substring(1, 2), 16) * 16; + this.blue = parseInt(match[1].substring(2, 3), 16) * 16; + this.alpha = 1.0; + return true; + } + // a color name, e.g., springgreen + match = colorChart[value]; + if (match) { + this.red = match.red; + this.green = match.green; + this.blue = match.blue; + this.alpha = (match.alpha === undefined) ? 1.0 : match.alpha; + return true; + } + // rgb(r, g, b), e.g., rgb(128, 12, 217) + match = value.match(/rgb\(([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/i); + if (match) { + this.red = Number(match[1]); + this.green = Number(match[2]); + this.blue = Number(match[3]); + this.alpha = 1.0; + return true; + } + // rgb(r%, g%, b%), e.g., rgb(100%, 0%, 50%) +// Commented out for the time being, since it seems likely that the resulting +// decimal values will create false negatives when compared with non-% values. +// +// => store as separate percent values and do exact matching when compared with % values +// and fuzzy matching when compared with non-% values? +// +// match = value.match(/rgb\(([0-9]{0,3}(?:\.[0-9]+)?)%\s*,\s*([0-9]{0,3}(?:\.[0-9]+)?)%\s*,\s*([0-9]{0,3}(?:\.[0-9]+)?)%\s*\)/i); +// if (match) { +// this.red = Number(match[1]) * 255 / 100; +// this.green = Number(match[2]) * 255 / 100; +// this.blue = Number(match[3]) * 255 / 100; +// this.alpha = 1.0; +// return true; +// } + // rgba(r, g, b, a), e.g., rgb(128, 12, 217, 0.2) + match = value.match(/rgba\(([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]?(?:\.[0-9]+)?)\s*\)/i); + if (match) { + this.red = Number(match[1]); + this.green = Number(match[2]); + this.blue = Number(match[3]); + this.alpha = Number(match[4]); + return true; + } + // rgba(r%, g%, b%, a), e.g., rgb(100%, 0%, 50%, 0.3) +// Commented out for the time being (cf. rgb() matching above) +// match = value.match(/rgba\(([0-9]{0,3}(?:\.[0-9]+)?)%\s*,\s*([0-9]{0,3}(?:\.[0-9]+)?)%\s*,\s*([0-9]{0,3}(?:\.[0-9]+)?)%,\s*([0-9]?(?:\.[0-9]+)?)\s*\)/i); +// if (match) { +// this.red = Number(match[1]) * 255 / 100; +// this.green = Number(match[2]) * 255 / 100; +// this.blue = Number(match[3]) * 255 / 100; +// this.alpha = Number(match[4]); +// return true; +// } + // TODO(rolandsteiner): handle "hsl(h, s, l)" and "hsla(h, s, l, a)" notation + return false; + } + this.toString = function() { + return this.valid ? this.red + ',' + this.green + ',' + this.blue : '(invalid)'; + } + this.toHexString = function() { + if (!this.valid) + return '(invalid)'; + return ((this.red < 16) ? '0' : '') + this.red.toString(16) + + ((this.green < 16) ? '0' : '') + this.green.toString(16) + + ((this.blue < 16) ? '0' : '') + this.blue.toString(16); + } + this.valid = this.parse(value); +} + +/** + * Utility class for converting font sizes to the size + * attribute in a font tag. Currently only converts px because + * only the sizes and px ever come from queryCommandValue. + * + * @constructor + * @param value {String} original value + */ +function FontSize(value) { + this.parse = function(str) { + if (!str) + this.valid = false; + var match; + if (match = String(str).match(/([0-9]+)px/)) { + var px = Number(match[1]); + if (px <= 0 || px > 47) + return false; + if (px <= 10) { + this.size = '1'; + } else if (px <= 13) { + this.size = '2'; + } else if (px <= 16) { + this.size = '3'; + } else if (px <= 18) { + this.size = '4'; + } else if (px <= 24) { + this.size = '5'; + } else if (px <= 32) { + this.size = '6'; + } else { + this.size = '7'; + } + return true; + } + if (match = String(str).match(/([+-][0-9]+)/)) { + this.size = match[1]; + return this.size >= 1 && this.size <= 7; + } + if (Number(str)) { + this.size = String(Number(str)); + return this.size >= 1 && this.size <= 7; + } + switch (str) { + case 'x-small': + this.size = '1'; + return true; + case 'small': + this.size = '2'; + return true; + case 'medium': + this.size = '3'; + return true; + case 'large': + this.size = '4'; + return true; + case 'x-large': + this.size = '5'; + return true; + case 'xx-large': + this.size = '6'; + return true; + case 'xxx-large': + this.size = '7'; + return true; + case '-webkit-xxx-large': + this.size = '7'; + return true; + case 'larger': + this.size = '+1'; + return true; + case 'smaller': + this.size = '-1'; + return true; + } + return false; + } + this.compare = function(other) { + return this.valid && other.valid && this.size === other.size; + } + this.toString = function() { + return this.valid ? this.size : '(invalid)'; + } + this.valid = this.parse(value); +} + +/** + * Utility class for converting & canonicalizing font names. + * + * @constructor + * @param value {String} original value + */ +function FontName(value) { + this.parse = function(str) { + if (!str) + return false; + str = String(str).toLowerCase(); + switch (str) { + case 'arial new': + this.fontname = 'arial'; + return true; + case 'courier new': + this.fontname = 'courier'; + return true; + case 'times new': + case 'times roman': + case 'times new roman': + this.fontname = 'times'; + return true; + } + this.fontname = value; + return true; + } + this.compare = function(other) { + return this.valid && other.valid && this.fontname === other.fontname; + } + this.toString = function() { + return this.valid ? this.fontname : '(invalid)'; + } + this.valid = this.parse(value); +} diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/variables.js b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/variables.js new file mode 100644 index 0000000000..cdc6f1e929 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/static/js/variables.js @@ -0,0 +1,227 @@ +/** + * @fileoverview + * Common constants and variables used in the RTE test suite. + * + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the 'License') + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @version 0.1 + * @author rolandsteiner@google.com + */ + +// Constant for indicating a test setup is unsupported or incorrect +// (threw exception). +var INTERNAL_ERR = 'INTERNAL ERROR: '; +var SETUP_EXCEPTION = 'SETUP EXCEPTION: '; +var EXECUTION_EXCEPTION = 'EXECUTION EXCEPTION: '; +var VERIFICATION_EXCEPTION = 'VERIFICATION EXCEPTION: '; + +var SETUP_CONTAINER = 'WHEN INITIALIZING TEST CONTAINER'; +var SETUP_BAD_SELECTION_SPEC = 'BAD SELECTION SPECIFICATION IN TEST OR EXPECTATION STRING'; +var SETUP_HTML = 'WHEN SETTING TEST HTML'; +var SETUP_SELECTION = 'WHEN SETTING SELECTION'; +var SETUP_NOCOMMAND = 'NO COMMAND, GENERAL FUNCTION OR QUERY FUNCTION GIVEN'; +var HTML_COMPARISON = 'WHEN COMPARING OUTPUT HTML'; + +// Exceptiona to be thrown on unsupported selection operations +var SELMODIFY_UNSUPPORTED = 'UNSUPPORTED selection.modify()'; +var SELALLCHILDREN_UNSUPPORTED = 'UNSUPPORTED selection.selectAllChildren()'; + +// Output string for unsupported functions +// (returning bool 'false' as opposed to throwing an exception) +var UNSUPPORTED = '<i>false</i> (UNSUPPORTED)'; + +// HTML comparison result contants. +var VALRESULT_NOT_RUN = 0; // test hasn't been run yet +var VALRESULT_SETUP_EXCEPTION = 1; +var VALRESULT_EXECUTION_EXCEPTION = 2; +var VALRESULT_VERIFICATION_EXCEPTION = 3; +var VALRESULT_UNSUPPORTED = 4; +var VALRESULT_CANARY = 5; // HTML changes bled into the canary. +var VALRESULT_DIFF = 6; +var VALRESULT_ACCEPT = 7; // HTML technically correct, but not ideal. +var VALRESULT_EQUAL = 8; + +var VALOUTPUT = [ // IMPORTANT: this array MUST be coordinated with the values above!! + {css: 'grey', output: '???', title: 'The test has not been run yet.'}, // VALRESULT_NOT_RUN + {css: 'exception', output: 'EXC.', title: 'Exception was thrown during setup.'}, // VALRESULT_SETUP_EXCEPTION + {css: 'exception', output: 'EXC.', title: 'Exception was thrown during execution.'}, // VALRESULT_EXECUTION_EXCEPTION + {css: 'exception', output: 'EXC.', title: 'Exception was thrown during result verification.'}, // VALRESULT_VERIFICATION_EXCEPTION + {css: 'unsupported', output: 'UNS.', title: 'Unsupported command or value'}, // VALRESULT_UNSUPPORTED + {css: 'canary', output: 'CANARY', title: 'The command affected the contentEditable root element, or outside HTML.'}, // VALRESULT_CANARY + {css: 'fail', output: 'FAIL', title: 'The result differs from the expectation(s).'}, // VALRESULT_DIFF + {css: 'accept', output: 'ACC.', title: 'The result is technically correct, but sub-optimal.'}, // VALRESULT_ACCEPT + {css: 'pass', output: 'PASS', title: 'The test result matches the expectation.'} // VALRESULT_EQUAL +] + +// Selection comparison result contants. +var SELRESULT_NOT_RUN = 0; // test hasn't been run yet +var SELRESULT_CANARY = 1; // selection escapes the contentEditable element +var SELRESULT_DIFF = 2; +var SELRESULT_NA = 3; +var SELRESULT_ACCEPT = 4; // Selection is acceptable, but not ideal. +var SELRESULT_EQUAL = 5; + +var SELOUTPUT = [ // IMPORTANT: this array MUST be coordinated with the values above!! + {css: 'grey', output: 'grey', title: 'The test has not been run yet.'}, // SELRESULT_NOT_RUN + {css: 'canary', output: 'CANARY', title: 'The selection escaped the contentEditable boundary!'}, // SELRESULT_CANARY + {css: 'fail', output: 'FAIL', title: 'The selection differs from the expectation(s).'}, // SELRESULT_DIFF + {css: 'na', output: 'N/A', title: 'The correctness of the selection could not be verified.'}, // SELRESULT_NA + {css: 'accept', output: 'ACC.', title: 'The selection is technically correct, but sub-optimal.'}, // SELRESULT_ACCEPT + {css: 'pass', output: 'PASS', title: 'The selection matches the expectation.'} // SELRESULT_EQUAL +]; + +// RegExp for selection markers +var SELECTION_MARKERS = /[\[\]\{\}\|\^]/; + +// Special attributes used to mark selections within elements that otherwise +// have no children. Important: attribute name MUST be lower case! +var ATTRNAME_SEL_START = 'bsselstart'; +var ATTRNAME_SEL_END = 'bsselend'; + +// DOM node type constants. +var DOM_NODE_TYPE_ELEMENT = 1; +var DOM_NODE_TYPE_TEXT = 3; +var DOM_NODE_TYPE_COMMENT = 8; + +// Test parameter names +var PARAM_DESCRIPTION = 'desc'; +var PARAM_PAD = 'pad'; +var PARAM_EXECCOMMAND = 'command'; +var PARAM_FUNCTION = 'function'; +var PARAM_QUERYCOMMANDSUPPORTED = 'qcsupported'; +var PARAM_QUERYCOMMANDENABLED = 'qcenabled'; +var PARAM_QUERYCOMMANDINDETERM = 'qcindeterm'; +var PARAM_QUERYCOMMANDSTATE = 'qcstate'; +var PARAM_QUERYCOMMANDVALUE = 'qcvalue'; +var PARAM_VALUE = 'value'; +var PARAM_EXPECTED = 'expected'; +var PARAM_EXPECTED_OUTER = 'expOuter'; +var PARAM_ACCEPT = 'accept'; +var PARAM_ACCEPT_OUTER = 'accOuter'; +var PARAM_CHECK_ATTRIBUTES = 'checkAttrs'; +var PARAM_CHECK_STYLE = 'checkStyle'; +var PARAM_CHECK_CLASS = 'checkClass'; +var PARAM_CHECK_ID = 'checkID'; +var PARAM_STYLE_WITH_CSS = 'styleWithCSS'; + +// ID suffixes for the output columns +var IDOUT_TR = '_:TR:'; // per container +var IDOUT_TESTID = '_:tid'; // per test +var IDOUT_COMMAND = '_:cmd'; // per test +var IDOUT_VALUE = '_:val'; // per test +var IDOUT_CHECKATTRS = '_:att'; // per test +var IDOUT_CHECKSTYLE = '_:sty'; // per test +var IDOUT_CONTAINER = '_:cnt:'; // per container +var IDOUT_STATUSVAL = '_:sta:'; // per container +var IDOUT_STATUSSEL = '_:sel:'; // per container +var IDOUT_PAD = '_:pad'; // per test +var IDOUT_EXPECTED = '_:exp'; // per test +var IDOUT_ACTUAL = '_:act:'; // per container + +// Output strings to use for yes/no/NA +var OUTSTR_YES = '●'; +var OUTSTR_NO = '○'; +var OUTSTR_NA = '-'; + +// Tags at the start of HTML strings where they were taken from +var HTMLTAG_BODY = 'B:'; +var HTMLTAG_OUTER = 'O:'; +var HTMLTAG_INNER = 'I:'; + +// What to use for the canary +var CANARY = 'CAN<br>ARY'; + +// Containers for tests, and their associated DOM elements: +// iframe, win, doc, body, elem +var containers = [ + { id: 'dM', + iframe: null, + win: null, + doc: null, + body: null, + editor: null, + tagOpen: '<body>', + tagClose: '</body>', + editorID: null, + canary: '', + }, + { id: 'body', + iframe: null, + win: null, + doc: null, + body: null, + editor: null, + tagOpen: '<body contenteditable="true">', + tagClose: '</body>', + editorID: null, + canary: '' + }, + { id: 'div', + iframe: null, + win: null, + doc: null, + body: null, + editor: null, + tagOpen: '<div contenteditable="true" id="editor-div">', + tagClose: '</div>', + editorID: 'editor-div', + canary: CANARY + } +]; + +// Helper variables to use in test functions +var win = null; // window object to use for test functions +var doc = null; // document object to use for test functions +var body = null; // The <body> element of the current document +var editor = null; // The contentEditable element (i.e., the <body> or <div>) +var sel = null; // The current selection after the pad is set up + +// Canonicalization emit flags for various purposes +var emitFlagsForCanary = { + emitAttrs: true, + emitStyle: true, + emitClass: true, + emitID: true, + lowercase: true, + canonicalizeUnits: true +}; +var emitFlagsForOutput = { + emitAttrs: true, + emitStyle: true, + emitClass: true, + emitID: true, + lowercase: false, + canonicalizeUnits: false +}; + +// Shades of output colors +var colorShades = ['Lo', 'Hi']; + +// Classes of tests +var testClassIDs = ['Finalized', 'RFC', 'Proposed']; +var testClassCount = testClassIDs.length; + +// Dictionary storing the detailed test results. +var results = { + count: 0, + score: 0 +}; + +// Results - populated by the fillResults() function. +var beacon = []; + +// "compatibility" between Python and JS for test quines +var True = true; +var False = false; diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/templates/output.html b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/templates/output.html new file mode 100644 index 0000000000..62d917d697 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/templates/output.html @@ -0,0 +1,138 @@ +<!-- Legend --> +<TABLE CLASS="legend framed"> + <THEAD> + <TR><TH COLSPAN=3 CLASS="legendHdr">Result Description</TH></TR> + <TR><TH>Status</TH><TH ALIGN="LEFT">Meaning</TH><TH ALIGN="LEFT">Explanation</TH><TH>Scoring</TH></TR> + </THEAD> + <TBODY> + <TR CLASS="lo"><TD CLASS="pass" ALIGN="CENTER"> PASS </TD><TD CLASS="legend" ROWSPAN=2>Passed</TD><TD CLASS="legend" ROWSPAN=2>The result matches the expectation.</TD><TD ROWSPAN=2 ALIGN="CENTER" CLASS="pass">PASS (+1)</TD></TR> + <TR CLASS="hi"><TD CLASS="pass" ALIGN="CENTER"> PASS </TD></TR> + <TR CLASS="lo"><TD CLASS="accept" ALIGN="CENTER"> ACC. </TD><TD CLASS="legend" ROWSPAN=2>Acceptable</TD><TD CLASS="legend" ROWSPAN=2>The result is technically correct, but not ideal (too verbose, deprecated usage, etc.) - for informative purposes only.</TD><TD ROWSPAN=2 ALIGN="CENTER" CLASS="fail">FAIL (+0)</TD></TR> + <TR CLASS="hi"><TD CLASS="accept" ALIGN="CENTER"> ACC. </TD></TR> + <TR CLASS="lo"><TD CLASS="fail" ALIGN="CENTER"> FAIL </TD><TD CLASS="legend" ROWSPAN=2>Failure</TD><TD CLASS="legend" ROWSPAN=2>The result does not match any given expectation.</TD><TD ROWSPAN=2 ALIGN="CENTER" CLASS="fail">FAIL (+0)</TD></TR> + <TR CLASS="hi"><TD CLASS="fail" ALIGN="CENTER"> FAIL </TD></TR> + <TR CLASS="lo"><TD CLASS="canary" ALIGN="CENTER"> CANARY </TD><TD CLASS="legend" ROWSPAN=2>Canary</TD><TD CLASS="legend" ROWSPAN=2>The result changes HTML other than children of the contentEditable element.</TD><TD ROWSPAN=2 ALIGN="CENTER" CLASS="fail">FAIL (+0)</TD></TR> + <TR CLASS="hi"><TD CLASS="canary" ALIGN="CENTER"> CANARY </TD></TR> + <TR CLASS="lo"><TD CLASS="unsupported" ALIGN="CENTER"> UNS. </TD><TD CLASS="legend" ROWSPAN=2>Unsupported</TD><TD CLASS="legend" ROWSPAN=2>The specific function or value is unsupported (returned boolean 'false').</TD><TD ROWSPAN=2 ALIGN="CENTER" CLASS="fail">FAIL (+0)</TD></TR> + <TR CLASS="hi"><TD CLASS="unsupported" ALIGN="CENTER"> UNS. </TD></TR> + <TR CLASS="lo"><TD CLASS="exception" ALIGN="CENTER"> EXC. </TD><TD CLASS="legend" ROWSPAN=2>Exception</TD><TD CLASS="legend" ROWSPAN=2>An unexpected exception was thrown during the execution of the test.</TD><TD ROWSPAN=2 ALIGN="CENTER" CLASS="fail">FAIL (+0)</TD></TR> + <TR CLASS="hi"><TD CLASS="exception" ALIGN="CENTER"> EXC. </TD></TR> + <TR CLASS="lo"><TD CLASS="na" ALIGN="CENTER"> N/A </TD><TD CLASS="legend" ROWSPAN=2>Not Applicable</TD><TD CLASS="legend" ROWSPAN=2>The selection could not be tested, because the tested function failed to return a known result.</TD><TD ROWSPAN=2 ALIGN="CENTER" CLASS="fail">FAIL (+0)</TD></TR> + <TR CLASS="hi"><TD CLASS="na" ALIGN="CENTER"> N/A </TD></TR> + </TBODY> +</TABLE> +<TABLE CLASS="legend framed"> + <THEAD> + <TR><TH COLSPAN=2 CLASS="legendHdr">Selection and Result Display</TH></TR> + <TR><TH>Character</TH><TH ALIGN="LEFT">Explanation</TH></TR> + </THEAD> + <TBODY> + <TR><TD CLASS="sel" ALIGN="CENTER">[</TD><TD>Start of selection - selection point is within a text node.</TD></TR> + <TR><TD CLASS="sel" ALIGN="CENTER">]</TD><TD>End of selection - selection point is within a text node.</TD></TR> + <TR><TD CLASS="sel" ALIGN="CENTER">^</TD><TD>Collapsed selection - selection point is within a text node.</TD></TR> + <TR><TD COLSPAN=2> </TD></TR> + <TR><TD CLASS="sel" ALIGN="CENTER">{</TD><TD>Start of selection - selection point is within an element node.</TD></TR> + <TR><TD CLASS="sel" ALIGN="CENTER">}</TD><TD>End of selection - selection point is within an element node.</TD></TR> + <TR><TD CLASS="sel" ALIGN="CENTER">|</TD><TD>Collapsed selection - selection point is within an element node.</TD></TR> + <TR><TD COLSPAN=2> </TD></TR> + <TR><TD ALIGN="CENTER"><SPAN CLASS="fade">foo</SPAN></TD><TD>Greyed text indicates parts of the output that are ignored for the purposes of checking the result.</TD></TR> + <TR><TD ALIGN="CENTER"><SPAN CLASS="txt">foo</SPAN></TD><TD>Grey border indicates extent of text nodes in the result.</TD></TR> + </TBODY> +</TABLE> +<!-- progress meter --> +<HR ID="divider"> +<H1>Running Test Suites: {% for s in suites %}<A HREF="#{{ s.id }}" ID="{{ s.id }}-progress" STYLE="color: #eeeeee">{{ s.id }}</A> {% endfor %}<SPAN ID="done"> </SPAN></H1> +<HR> +<!-- main output --> +{% for s in suites %} + <H1 ID="{{ s.id }}"><A NAME="{{ s.id }}" HREF="#{{ s.id }}">{{ s.id }}</A> - {{ s.caption }}: + <SPAN ID="{{ s.id }}-{% ifequal s.id.0 'S' %}sel{% endifequal %}score">?/?</SPAN> + {% ifnotequal s.id.0 "Q" %}{% ifnotequal s.id.0 "S" %} + (Selection: <SPAN ID="{{ s.id }}-selscore">?/?</SPAN>) + {% endifnotequal %}{% endifnotequal %} + (time: <SPAN ID="{{ s.id }}-time">?</SPAN> ms) + </H1> + {% if s.comment %} + <DIV CLASS="comment">{{ s.comment|safe }}</DIV> + {% endif %} + {% for cls in classes %}{% for pk, pv in s.items %}{% ifequal pk cls %} + <H2 ID="{{ s.id }}-{{ cls }}"><A NAME="{{ s.id }}-{{ cls }}" HREF="#{{ s.id }}-{{ cls }}">{{ cls }} Tests</A>: + <SPAN ID="{{ s.id }}-{{ cls }}-{% ifequal s.id.0 'S' %}sel{% endifequal %}score">?/?</SPAN> + {% ifnotequal s.id.0 "Q" %}{% ifnotequal s.id.0 "S" %} + (Selection: <SPAN ID="{{ s.id }}-{{ cls }}-selscore">?/?</SPAN>) + {% endifnotequal %}{% endifnotequal %} + </H2> + <TABLE WIDTH=100%> + <THEAD> + <TR> + <TH TITLE="Unique ID of the test" ALIGN="LEFT">ID</TH> + <TH TITLE="Command or function used in the test" ALIGN="LEFT">Command</TH> + <TH TITLE="Value field for commands" ALIGN="LEFT">Value</TH> + {% ifnotequal s.id.0 "S" %}{% ifnotequal s.id.0 "Q" %}{% comment %} Don't output attribute and style columns for selection and "queryCommand..." tests. {% endcomment %} + <TH TITLE="check Atributes?">A</TH> + <TH TITLE="check Style">S</TH> + {% endifnotequal %}{% endifnotequal %} + <TH TITLE="Testing HTML Element">Env.</TH> + {% ifnotequal s.id.0 "S" %}{% comment %} Don't output HTML status column for selection tests. {% endcomment %} + <TH TITLE="State of the test">Status</TH> + {% endifnotequal %} + {% ifnotequal s.id.0 "Q" %}{% comment %} Don't output selection result column for "queryCommand..." tests. {% endcomment %} + <TH TITLE="State of the test regarding the selection">Selection</TH> + {% endifnotequal %} + <TH TITLE="Initial HTML and selection" ALIGN="LEFT">Initial</TH> + <TH TITLE="Expected HTML and selection" ALIGN="LEFT">Expected</TH> + <TH TITLE="Actual result HTML and selection" ALIGN="LEFT">Actual (lower case, canonicalized, selection marks)</TH> + <TH TITLE="Short description of the test" ALIGN="LEFT">Description</TH> + </TR> + </THEAD> + <TBODY> + {% for g in pv %}{% for t in g.tests %} + <TR ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:TR:dM" CLASS="{% cycle 'lo' 'lo' 'lo' 'hi' 'hi' 'hi' as shade %}"> + <TD ROWSPAN=3 ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:tid"><A CLASS="idLabel" NAME="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}" HREF="#{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}">{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}</A></TD> + <TD ROWSPAN=3 ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:cmd"> </TD> + <TD ROWSPAN=3 ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:val"> </TD> + {% ifnotequal s.id.0 "S" %}{% ifnotequal s.id.0 "Q" %}{% comment %} Don't output attribute and style columns for selection and "queryCommand..." tests. {% endcomment %} + <TD ROWSPAN=3 ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:att" ALIGN="CENTER"> </TD> + <TD ROWSPAN=3 ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:sty" ALIGN="CENTER"> </TD> + {% endifnotequal %}{% endifnotequal %} + <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:cnt:dM" TITLE="designMode="on"" ALIGN="CENTER">dM</TD> + {% ifnotequal s.id.0 "S" %} + <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:sta:dM" ALIGN="CENTER">NONE</TD> + {% endifnotequal %} + {% ifnotequal s.id.0 "Q" %} + <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:sel:dM" ALIGN="CENTER">NONE</TD> + {% endifnotequal %} + <TD ROWSPAN=3 ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:pad"> </TD> + <TD ROWSPAN=3 ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:exp"> </TD> + <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:act:dM"><I>Processing...</I></TD> + <TD ROWSPAN=3>{{ t.desc|default:" " }}</TD> + </TR> + <TR ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:TR:body" CLASS="{% cycle shade %}"> + <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:cnt:body" TITLE="<body contentEditable="true">" ALIGN="CENTER">body</TD> + {% ifnotequal s.id.0 "S" %} + <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:sta:body" ALIGN="CENTER">NONE</TD> + {% endifnotequal %} + {% ifnotequal s.id.0 "Q" %} + <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:sel:body" ALIGN="CENTER">NONE</TD> + {% endifnotequal %} + <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:act:body"><I>Processing...</I></TD> + </TR> + <TR ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:TR:div" CLASS="{% cycle shade %}"> + <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:cnt:div" TITLE="<div contentEditable="true">" ALIGN="CENTER">div</TD> + {% ifnotequal s.id.0 "S" %} + <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:sta:div" ALIGN="CENTER">NONE</TD> + {% endifnotequal %} + {% ifnotequal s.id.0 "Q" %} + <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:sel:div" ALIGN="CENTER">NONE</TD> + {% endifnotequal %} + <TD ID="{{ commonIDPrefix }}-{{ s.id }}_{{ t.id }}_:act:div"><I>Processing...</I></TD> + </TR> + {% endfor %}{% endfor %} + </TBODY> + </TABLE> + {% endifequal %}{% endfor %}{% endfor %} +{% endfor %} + + + + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/templates/richtext2.html b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/templates/richtext2.html new file mode 100644 index 0000000000..98de8796da --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/templates/richtext2.html @@ -0,0 +1,107 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta http-equiv="content-type" content="text/html; charset=utf-8" /> + <meta http-equiv="X-UA-Compatible" content="IE=edge" /> + + <title>New Rich Text Tests</title> + + <link rel="stylesheet" href="static/common.css" type="text/css"> + <link rel="stylesheet" href="static/editable.css" type="text/css"> + + <!-- utility scripts --> + <script src="static/js/variables.js"></script> + + <script src="static/js/canonicalize.js"></script> + <script src="static/js/compare.js"></script> + <script src="static/js/output.js"></script> + <script src="static/js/pad.js"></script> + <script src="static/js/range.js"></script> + <script src="static/js/units.js"></script> + + <script src="static/js/run.js"></script> + + <!-- new tests --> + <script type="text/javascript"> + {% autoescape off %} + + var commonIDPrefix = '{{ commonIDPrefix }}'; + {% for s in suites %} + var {{ s.id }}_TESTS = {{ s }}; + {% endfor %} + + /** + * Stuff to do after all tests are run: + * - write a nice "DONE!" at the end of the progress meter + * - beacon the results + * - remove the testing <iframe>s + */ + function finish() { + var span = document.getElementById('done'); + if (span) + span.innerHTML = ' ... DONE!'; + + fillResults(); + parent.sendScore(beacon, categoryTotals); + + cleanUp(); + } + + /** + * Run every individual suite, with a a brief timeout in between + * to allow for screen updates. + */ +{% for s in suites %} + {% if not forloop.first %} + setTimeout("runSuite{{ s.id }}()", 100); + } + {% endif %} + + function runSuite{{ s.id }}() { + runAndOutputTestSuite({{ s.id }}_TESTS); +{% endfor %} + finish(); + } + + /** + * Runs all tests in all suites. + */ + function doRunTests() { + initVariables(); + initEditorDocs(); + + // Start with the first test suite + runSuite{{ suites.0.id }}(); + } + + /** + * Runs after allowing for some time to have everything loaded + * (aka. horrible IE9 kludge) + */ + function runTests() { + setTimeout("doRunTests()", 1500); + } + + /** + * Removes the <iframe>s after all tests are finished + */ + function cleanUp() { + var e = document.getElementById('iframe-dM'); + e.parentNode.removeChild(e); + e = document.getElementById('iframe-body'); + e.parentNode.removeChild(e); + e = document.getElementById('iframe-div'); + e.parentNode.removeChild(e); + } + {% endautoescape %} + </script> +</head> + +<body onload="runTests()"> + {% include "richtext2/templates/output.html" %} + <hr> + <iframe name="iframe-dM" id="iframe-dM" src="static/editable-dM.html"></iframe> + <iframe name="iframe-body" id="iframe-body" src="static/editable-body.html"></iframe> + <iframe name="iframe-div" id="iframe-div" src="static/editable-div.html"></iframe> +</body> +</html> diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/__init__.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/__init__.py new file mode 100644 index 0000000000..a1f5279ad5 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/__init__.py @@ -0,0 +1,17 @@ +__all__ = [ + 'apply', + 'applyCSS', + 'change', + 'changeCSS', + 'delete', + 'forwarddelete', + 'insert', + 'queryEnabled', + 'queryIndeterm', + 'queryState', + 'querySupported', + 'queryValue', + 'selection', + 'unapply', + 'unapplyCSS' +]
\ No newline at end of file diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/apply.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/apply.py new file mode 100644 index 0000000000..3eb465c84c --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/apply.py @@ -0,0 +1,364 @@ + +APPLY_TESTS = { + 'id': 'A', + 'caption': 'Apply Formatting Tests', + 'checkAttrs': True, + 'checkStyle': True, + 'styleWithCSS': False, + + 'Proposed': [ + { 'desc': '', + 'command': '', + 'tests': [ + ] + }, + + { 'desc': '[HTML5] bold', + 'command': 'bold', + 'tests': [ + { 'id': 'B_TEXT-1_SI', + 'rte1-id': 'a-bold-0', + 'desc': 'Bold selection', + 'pad': 'foo[bar]baz', + 'expected': [ 'foo<b>[bar]</b>baz', + 'foo<strong>[bar]</strong>baz' ] }, + + { 'id': 'B_TEXT-1_SIR', + 'desc': 'Bold reversed selection', + 'pad': 'foo]bar[baz', + 'expected': [ 'foo<b>[bar]</b>baz', + 'foo<strong>[bar]</strong>baz' ] }, + + { 'id': 'B_I-1_SL', + 'desc': 'Bold selection, partially including italic', + 'pad': 'foo[bar<i>baz]qoz</i>quz', + 'expected': [ 'foo<b>[bar</b><i><b>baz]</b>qoz</i>quz', + 'foo<b>[bar<i>baz]</i></b><i>qoz</i>quz', + 'foo<strong>[bar</strong><i><strong>baz]</strong>qoz</i>quz', + 'foo<strong>[bar<i>baz]</i></strong><i>qoz</i>quz' ] } + ] + }, + + { 'desc': '[HTML5] italic', + 'command': 'italic', + 'tests': [ + { 'id': 'I_TEXT-1_SI', + 'rte1-id': 'a-italic-0', + 'desc': 'Italicize selection', + 'pad': 'foo[bar]baz', + 'expected': [ 'foo<i>[bar]</i>baz', + 'foo<em>[bar]</em>baz' ] } + ] + }, + + { 'desc': '[HTML5] underline', + 'command': 'underline', + 'tests': [ + { 'id': 'U_TEXT-1_SI', + 'rte1-id': 'a-underline-0', + 'desc': 'Underline selection', + 'pad': 'foo[bar]baz', + 'expected': 'foo<u>[bar]</u>baz' } + ] + }, + + { 'desc': '[HTML5] strikethrough', + 'command': 'strikethrough', + 'tests': [ + { 'id': 'S_TEXT-1_SI', + 'rte1-id': 'a-strikethrough-0', + 'desc': 'Strike-through selection', + 'pad': 'foo[bar]baz', + 'expected': [ 'foo<s>[bar]</s>baz', + 'foo<strike>[bar]</strike>baz', + 'foo<del>[bar]</del>baz' ] } + ] + }, + + { 'desc': '[HTML5] subscript', + 'command': 'subscript', + 'tests': [ + { 'id': 'SUB_TEXT-1_SI', + 'rte1-id': 'a-subscript-0', + 'desc': 'Change selection to subscript', + 'pad': 'foo[bar]baz', + 'expected': 'foo<sub>[bar]</sub>baz' } + ] + }, + + { 'desc': '[HTML5] superscript', + 'command': 'superscript', + 'tests': [ + { 'id': 'SUP_TEXT-1_SI', + 'rte1-id': 'a-superscript-0', + 'desc': 'Change selection to superscript', + 'pad': 'foo[bar]baz', + 'expected': 'foo<sup>[bar]</sup>baz' } + ] + }, + + { 'desc': '[HTML5] createlink', + 'command': 'createlink', + 'tests': [ + { 'id': 'CL:url_TEXT-1_SI', + 'rte1-id': 'a-createlink-0', + 'desc': 'create a link around the selection', + 'value': '#foo', + 'pad': 'foo[bar]baz', + 'expected': 'foo<a href="#foo">[bar]</a>baz' } + ] + }, + + { 'desc': '[HTML5] formatBlock', + 'command': 'formatblock', + 'tests': [ + { 'id': 'FB:H1_TEXT-1_SI', + 'rte1-id': 'a-formatblock-0', + 'desc': 'format the selection into a block: use <h1>', + 'value': 'h1', + 'pad': 'foo[bar]baz', + 'expected': '<h1>foo[bar]baz</h1>' }, + + { 'id': 'FB:P_TEXT-1_SI', + 'desc': 'format the selection into a block: use <p>', + 'value': 'p', + 'pad': 'foo[bar]baz', + 'expected': '<p>foo[bar]baz</p>' }, + + { 'id': 'FB:PRE_TEXT-1_SI', + 'desc': 'format the selection into a block: use <pre>', + 'value': 'pre', + 'pad': 'foo[bar]baz', + 'expected': '<pre>foo[bar]baz</pre>' }, + + { 'id': 'FB:ADDRESS_TEXT-1_SI', + 'desc': 'format the selection into a block: use <address>', + 'value': 'address', + 'pad': 'foo[bar]baz', + 'expected': '<address>foo[bar]baz</address>' }, + + { 'id': 'FB:BQ_TEXT-1_SI', + 'desc': 'format the selection into a block: use <blockquote>', + 'value': 'blockquote', + 'pad': 'foo[bar]baz', + 'expected': '<blockquote>foo[bar]baz</blockquote>' }, + + { 'id': 'FB:BQ_BR.BR-1_SM', + 'desc': 'format a multi-line selection into a block: use <blockquote>', + 'command': 'formatblock', + 'value': 'blockquote', + 'pad': 'fo[o<br>bar<br>b]az', + 'expected': '<blockquote>fo[o<br>bar<br>b]az</blockquote>' } + ] + }, + + + { 'desc': '[MIDAS] backcolor', + 'command': 'backcolor', + 'tests': [ + { 'id': 'BC:blue_TEXT-1_SI', + 'rte1-id': 'a-backcolor-0', + 'desc': 'Change background color (note: no non-CSS variant available)', + 'value': 'blue', + 'pad': 'foo[bar]baz', + 'expected': [ 'foo<span style="background-color: blue">[bar]</span>baz', + 'foo<font style="background-color: blue">[bar]</font>baz' ] } + ] + }, + + { 'desc': '[MIDAS] forecolor', + 'command': 'forecolor', + 'tests': [ + { 'id': 'FC:blue_TEXT-1_SI', + 'rte1-id': 'a-forecolor-0', + 'desc': 'Change the text color', + 'value': 'blue', + 'pad': 'foo[bar]baz', + 'expected': 'foo<font color="blue">[bar]</font>baz' } + ] + }, + + { 'desc': '[MIDAS] hilitecolor', + 'command': 'hilitecolor', + 'tests': [ + { 'id': 'HC:blue_TEXT-1_SI', + 'rte1-id': 'a-hilitecolor-0', + 'desc': 'Change the hilite color', + 'value': 'blue', + 'pad': 'foo[bar]baz', + 'expected': [ 'foo<span style="background-color: blue">[bar]</span>baz', + 'foo<font style="background-color: blue">[bar]</font>baz' ] } + ] + }, + + { 'desc': '[MIDAS] fontname', + 'command': 'fontname', + 'tests': [ + { 'id': 'FN:a_TEXT-1_SI', + 'rte1-id': 'a-fontname-0', + 'desc': 'Change the font name', + 'value': 'arial', + 'pad': 'foo[bar]baz', + 'expected': 'foo<font face="arial">[bar]</font>baz' } + ] + }, + + { 'desc': '[MIDAS] fontsize', + 'command': 'fontsize', + 'tests': [ + { 'id': 'FS:2_TEXT-1_SI', + 'rte1-id': 'a-fontsize-0', + 'desc': 'Change the font size to "2"', + 'value': '2', + 'pad': 'foo[bar]baz', + 'expected': 'foo<font size="2">[bar]</font>baz' }, + + { 'id': 'FS:18px_TEXT-1_SI', + 'desc': 'Change the font size to "18px"', + 'value': '18px', + 'pad': 'foo[bar]baz', + 'expected': 'foo<font size="18px">[bar]</font>baz' }, + + { 'id': 'FS:large_TEXT-1_SI', + 'desc': 'Change the font size to "large"', + 'value': 'large', + 'pad': 'foo[bar]baz', + 'expected': 'foo<font size="large">[bar]</font>baz' } + ] + }, + + { 'desc': '[MIDAS] increasefontsize', + 'command': 'increasefontsize', + 'tests': [ + { 'id': 'INCFS:2_TEXT-1_SI', + 'desc': 'Decrease the font size (to small)', + 'pad': 'foo[bar]baz', + 'expected': [ 'foo<font size="4">[bar]</font>baz', + 'foo<font size="+1">[bar]</font>baz', + 'foo<big>[bar]</big>baz' ] } + ] + }, + + { 'desc': '[MIDAS] decreasefontsize', + 'command': 'decreasefontsize', + 'tests': [ + { 'id': 'DECFS:2_TEXT-1_SI', + 'rte1-id': 'a-decreasefontsize-0', + 'desc': 'Decrease the font size (to small)', + 'pad': 'foo[bar]baz', + 'expected': [ 'foo<font size="2">[bar]</font>baz', + 'foo<font size="-1">[bar]</font>baz', + 'foo<small>[bar]</small>baz' ] } + ] + }, + + { 'desc': '[MIDAS] indent (note: accept the de-facto standard indent of 40px)', + 'command': 'indent', + 'tests': [ + { 'id': 'IND_TEXT-1_SI', + 'rte1-id': 'a-indent-0', + 'desc': 'Indent the text (accept the de-facto standard of 40px indent)', + 'pad': 'foo[bar]baz', + 'checkAttrs': False, + 'expected': [ '<blockquote>foo[bar]baz</blockquote>', + '<div style="margin-left: 40px">foo[bar]baz</div>' ], + 'div': { + 'accOuter': '<div contenteditable="true" style="margin-left: 40px">foo[bar]baz</div>' } } + ] + }, + + { 'desc': '[MIDAS] outdent (-> unapply tests)', + 'command': 'outdent', + 'tests': [ + ] + }, + + { 'desc': '[MIDAS] justifycenter', + 'command': 'justifycenter', + 'tests': [ + { 'id': 'JC_TEXT-1_SC', + 'rte1-id': 'a-justifycenter-0', + 'desc': 'justify the text centrally', + 'pad': 'foo^bar', + 'expected': [ '<center>foo^bar</center>', + '<p align="center">foo^bar</p>', + '<p align="middle">foo^bar</p>', + '<div align="center">foo^bar</div>', + '<div align="middle">foo^bar</div>' ], + 'div': { + 'accOuter': [ '<div align="center" contenteditable="true">foo^bar</div>', + '<div align="middle" contenteditable="true">foo^bar</div>' ] } } + ] + }, + + { 'desc': '[MIDAS] justifyfull', + 'command': 'justifyfull', + 'tests': [ + { 'id': 'JF_TEXT-1_SC', + 'rte1-id': 'a-justifyfull-0', + 'desc': 'justify the text fully', + 'pad': 'foo^bar', + 'expected': [ '<p align="justify">foo^bar</p>', + '<div align="justify">foo^bar</div>' ], + 'div': { + 'accOuter': '<div align="justify" contenteditable="true">foo^bar</div>' } } + ] + }, + + { 'desc': '[MIDAS] justifyleft', + 'command': 'justifyleft', + 'tests': [ + { 'id': 'JL_TEXT-1_SC', + 'rte1-id': 'a-justifyleft-0', + 'desc': 'justify the text left', + 'pad': 'foo^bar', + 'expected': [ '<p align="left">foo^bar</p>', + '<div align="left">foo^bar</div>' ], + 'div': { + 'accOuter': '<div align="left" contenteditable="true">foo^bar</div>' } } + ] + }, + + { 'desc': '[MIDAS] justifyright', + 'command': 'justifyright', + 'tests': [ + { 'id': 'JR_TEXT-1_SC', + 'rte1-id': 'a-justifyright-0', + 'desc': 'justify the text right', + 'pad': 'foo^bar', + 'expected': [ '<p align="right">foo^bar</p>', + '<div align="right">foo^bar</div>' ], + 'div': { + 'accOuter': '<div align="right" contenteditable="true">foo^bar</div>' } } + ] + }, + + { 'desc': '[MIDAS] heading', + 'command': 'heading', + 'tests': [ + { 'id': 'H:H1_TEXT-1_SC', + 'desc': 'create a heading from the paragraph that contains the selection', + 'value': 'h1', + 'pad': 'foo[bar]baz', + 'expected': '<h1>foo[bar]baz</h1>' } + ] + }, + + + { 'desc': '[Other] createbookmark', + 'command': 'createbookmark', + 'tests': [ + { 'id': 'CB:name_TEXT-1_SI', + 'rte1-id': 'a-createbookmark-0', + 'desc': 'create a bookmark (named link) around selection', + 'value': 'created', + 'pad': 'foo[bar]baz', + 'expected': 'foo<a name="created">[bar]</a>baz' } + ] + } + ] +} + + + + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/applyCSS.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/applyCSS.py new file mode 100644 index 0000000000..94cdad83fb --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/applyCSS.py @@ -0,0 +1,244 @@ + +APPLY_TESTS_CSS = { + 'id': 'AC', + 'caption': 'Apply Formatting Tests, using styleWithCSS', + 'checkAttrs': True, + 'checkStyle': True, + 'styleWithCSS': True, + + 'Proposed': [ + { 'desc': '', + 'command': '', + 'tests': [ + ] + }, + + { 'desc': '[HTML5] bold', + 'command': 'bold', + 'tests': [ + { 'id': 'B_TEXT-1_SI', + 'rte1-id': 'a-bold-1', + 'desc': 'Bold selection', + 'pad': 'foo[bar]baz', + 'expected': 'foo<span style="font-weight: bold">[bar]</span>baz' } + ] + }, + + { 'desc': '[HTML5] italic', + 'command': 'italic', + 'tests': [ + { 'id': 'I_TEXT-1_SI', + 'rte1-id': 'a-italic-1', + 'desc': 'Italicize selection', + 'pad': 'foo[bar]baz', + 'expected': 'foo<span style="font-style: italic">[bar]</span>baz' } + ] + }, + + { 'desc': '[HTML5] underline', + 'command': 'underline', + 'tests': [ + { 'id': 'U_TEXT-1_SI', + 'rte1-id': 'a-underline-1', + 'desc': 'Underline selection', + 'pad': 'foo[bar]baz', + 'expected': 'foo<span style="text-decoration: underline">[bar]</span>baz' } + ] + }, + + { 'desc': '[HTML5] strikethrough', + 'command': 'strikethrough', + 'tests': [ + { 'id': 'S_TEXT-1_SI', + 'rte1-id': 'a-strikethrough-1', + 'desc': 'Strike-through selection', + 'pad': 'foo[bar]baz', + 'expected': 'foo<span style="text-decoration: line-through">[bar]</span>baz' } + ] + }, + + { 'desc': '[HTML5] subscript', + 'command': 'subscript', + 'tests': [ + { 'id': 'SUB_TEXT-1_SI', + 'rte1-id': 'a-subscript-1', + 'desc': 'Change selection to subscript', + 'pad': 'foo[bar]baz', + 'expected': 'foo<span style="vertical-align: sub">[bar]</span>baz' } + ] + }, + + { 'desc': '[HTML5] superscript', + 'command': 'superscript', + 'tests': [ + { 'id': 'SUP_TEXT-1_SI', + 'rte1-id': 'a-superscript-1', + 'desc': 'Change selection to superscript', + 'pad': 'foo[bar]baz', + 'expected': 'foo<span style="vertical-align: super">[bar]</span>baz' } + ] + }, + + + { 'desc': '[MIDAS] backcolor', + 'command': 'backcolor', + 'tests': [ + { 'id': 'BC:blue_TEXT-1_SI', + 'rte1-id': 'a-backcolor-1', + 'desc': 'Change background color', + 'value': 'blue', + 'pad': 'foo[bar]baz', + 'expected': [ 'foo<span style="background-color: blue">[bar]</span>baz', + 'foo<font style="background-color: blue">[bar]</font>baz' ] } + ] + }, + + { 'desc': '[MIDAS] forecolor', + 'command': 'forecolor', + 'tests': [ + { 'id': 'FC:blue_TEXT-1_SI', + 'rte1-id': 'a-forecolor-1', + 'desc': 'Change the text color', + 'value': 'blue', + 'pad': 'foo[bar]baz', + 'expected': [ 'foo<span style="color: blue">[bar]</span>baz', + 'foo<font style="color: blue">[bar]</font>baz' ] } + ] + }, + + { 'desc': '[MIDAS] hilitecolor', + 'command': 'hilitecolor', + 'tests': [ + { 'id': 'HC:blue_TEXT-1_SI', + 'rte1-id': 'a-hilitecolor-1', + 'desc': 'Change the hilite color', + 'value': 'blue', + 'pad': 'foo[bar]baz', + 'expected': [ 'foo<span style="background-color: blue">[bar]</span>baz', + 'foo<font style="background-color: blue">[bar]</font>baz' ] } + ] + }, + + { 'desc': '[MIDAS] fontname', + 'command': 'fontname', + 'tests': [ + { 'id': 'FN:a_TEXT-1_SI', + 'rte1-id': 'a-fontname-1', + 'desc': 'Change the font name', + 'value': 'arial', + 'pad': 'foo[bar]baz', + 'expected': [ 'foo<span style="font-family: arial">[bar]</span>baz', + 'foo<font style="font-family: blue">[bar]</font>baz' ] } + ] + }, + + { 'desc': '[MIDAS] fontsize', + 'command': 'fontsize', + 'tests': [ + { 'id': 'FS:2_TEXT-1_SI', + 'rte1-id': 'a-fontsize-1', + 'desc': 'Change the font size to "2"', + 'value': '2', + 'pad': 'foo[bar]baz', + 'expected': [ 'foo<span style="font-size: small">[bar]</span>baz', + 'foo<font style="font-size: small">[bar]</font>baz' ] }, + + { 'id': 'FS:18px_TEXT-1_SI', + 'desc': 'Change the font size to "18px"', + 'value': '18px', + 'pad': 'foo[bar]baz', + 'expected': [ 'foo<span style="font-size: 18px">[bar]</span>baz', + 'foo<font style="font-size: 18px">[bar]</font>baz' ] }, + + { 'id': 'FS:large_TEXT-1_SI', + 'desc': 'Change the font size to "large"', + 'value': 'large', + 'pad': 'foo[bar]baz', + 'expected': [ 'foo<span style="font-size: large">[bar]</span>baz', + 'foo<font style="font-size: large">[bar]</font>baz' ] } + ] + }, + + { 'desc': '[MIDAS] indent', + 'command': 'indent', + 'tests': [ + { 'id': 'IND_TEXT-1_SI', + 'rte1-id': 'a-indent-1', + 'desc': 'Indent the text (assume "standard" 40px)', + 'pad': 'foo[bar]baz', + 'expected': [ '<div style="margin-left: 40px">foo[bar]baz</div>', + '<div style="margin: 0 0 0 40px">foo[bar]baz</div>', + '<blockquote style="margin-left: 40px">foo[bar]baz</blockquote>', + '<blockquote style="margin: 0 0 0 40px">foo[bar]baz</blockquote>' ], + 'div': { + 'accOuter': [ '<div contenteditable="true" style="margin-left: 40px">foo[bar]baz</div>', + '<div contenteditable="true" style="margin: 0 0 0 40px">foo[bar]baz</div>' ] } } + ] + }, + + { 'desc': '[MIDAS] outdent (-> unapply tests)', + 'command': 'outdent', + 'tests': [ + ] + }, + + { 'desc': '[MIDAS] justifycenter', + 'command': 'justifycenter', + 'tests': [ + { 'id': 'JC_TEXT-1_SC', + 'rte1-id': 'a-justifycenter-1', + 'desc': 'justify the text centrally', + 'pad': 'foo^bar', + 'expected': [ '<p style="text-align: center">foo^bar</p>', + '<div style="text-align: center">foo^bar</div>' ], + 'div': { + 'accOuter': '<div contenteditable="true" style="text-align: center">foo^bar</div>' } } + ] + }, + + { 'desc': '[MIDAS] justifyfull', + 'command': 'justifyfull', + 'tests': [ + { 'id': 'JF_TEXT-1_SC', + 'rte1-id': 'a-justifyfull-1', + 'desc': 'justify the text fully', + 'pad': 'foo^bar', + 'expected': [ '<p style="text-align: justify">foo^bar</p>', + '<div style="text-align: justify">foo^bar</div>' ], + 'div': { + 'accOuter': '<div contenteditable="true" style="text-align: justify">foo^bar</div>' } } + ] + }, + + { 'desc': '[MIDAS] justifyleft', + 'command': 'justifyleft', + 'tests': [ + { 'id': 'JL_TEXT-1_SC', + 'rte1-id': 'a-justifyleft-1', + 'desc': 'justify the text left', + 'pad': 'foo^bar', + 'expected': [ '<p style="text-align: left">foo^bar</p>', + '<div style="text-align: left">foo^bar</div>' ], + 'div': { + 'accOuter': '<div contenteditable="true" style="text-align: left">foo^bar</div>' } } + ] + }, + + { 'desc': '[MIDAS] justifyright', + 'command': 'justifyright', + 'tests': [ + { 'id': 'JR_TEXT-1_SC', + 'rte1-id': 'a-justifyright-1', + 'desc': 'justify the text right', + 'pad': 'foo^bar', + 'expected': [ '<p style="text-align: right">foo^bar</p>', + '<div style="text-align: right">foo^bar</div>' ], + 'div': { + 'accOuter': '<div contenteditable="true" style="text-align: right">foo^bar</div>' } } + ] + } + ] +} + + + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/change.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/change.py new file mode 100644 index 0000000000..6a76d3d5fd --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/change.py @@ -0,0 +1,273 @@ + +CHANGE_TESTS = { + 'id': 'C', + 'caption': 'Change Existing Format to Different Format Tests', + 'checkAttrs': True, + 'checkStyle': True, + 'styleWithCSS': False, + + 'Proposed': [ + { 'desc': '', + 'command': '', + 'tests': [ + ] + }, + + { 'desc': '[HTML5] italic', + 'command': 'italic', + 'tests': [ + { 'id': 'I_I-1_SL', + 'desc': 'Italicize partially italicized text', + 'pad': 'foo[bar<i>baz]</i>qoz', + 'expected': 'foo<i>[barbaz]</i>qoz' }, + + { 'id': 'I_B-I-1_SO', + 'desc': 'Italicize partially italicized text in bold context', + 'pad': '<b>foo[bar<i>baz</i>}</b>', + 'expected': '<b>foo<i>[barbaz]</i></b>' } + ] + }, + + { 'desc': '[HTML5] underline', + 'command': 'underline', + 'tests': [ + { 'id': 'U_U-1_SO', + 'desc': 'Underline partially underlined text', + 'pad': 'foo[bar<u>baz</u>qoz]quz', + 'expected': 'foo<u>[barbazqoz]</u>quz' }, + + { 'id': 'U_U-1_SL', + 'desc': 'Underline partially underlined text', + 'pad': 'foo[bar<u>baz]qoz</u>quz', + 'expected': 'foo<u>[barbaz]qoz</u>quz' }, + + { 'id': 'U_S-U-1_SO', + 'desc': 'Underline partially underlined text in striked context', + 'pad': '<s>foo[bar<u>baz</u>}</s>', + 'expected': '<s>foo<u>[barbaz]</u></s>' } + ] + }, + + + { 'desc': '[MIDAS] backcolor', + 'command': 'backcolor', + 'tests': [ + { 'id': 'BC:842_FONTs:bc:fca-1_SW', + 'rte1-id': 'c-backcolor-0', + 'desc': 'Change background color to new color', + 'value': '#884422', + 'pad': '<font style="background-color: #ffccaa">[foobarbaz]</font>', + 'expected': [ '<font style="background-color: #884422">[foobarbaz]</font>', + '<span style="background-color: #884422">[foobarbaz]</span>' ] }, + + { 'id': 'BC:00f_SPANs:bc:f00-1_SW', + 'rte1-id': 'c-backcolor-2', + 'desc': 'Change background color to new color', + 'value': '#0000ff', + 'pad': '<span style="background-color: #ff0000">[foobarbaz]</span>', + 'expected': [ '<font style="background-color: #0000ff">[foobarbaz]</font>', + '<span style="background-color: #0000ff">[foobarbaz]</span>' ] }, + + { 'id': 'BC:ace_FONT.ass.s:bc:rgb-1_SW', + 'rte1-id': 'c-backcolor-1', + 'desc': 'Change background color in styled span to new color', + 'value': '#aaccee', + 'pad': '<span class="Apple-style-span" style="background-color: rgb(255, 0, 0)">[foobarbaz]</span>', + 'expected': [ '<font style="background-color: #aaccee">[foobarbaz]</font>', + '<span style="background-color: #aaccee">[foobarbaz]</span>' ] } + ] + }, + + { 'desc': '[MIDAS] forecolor', + 'command': 'forecolor', + 'tests': [ + { 'id': 'FC:g_FONTc:b-1_SW', + 'rte1-id': 'c-forecolor-0', + 'desc': 'Change the text color (without CSS)', + 'value': 'green', + 'pad': '<font color="blue">[foobarbaz]</font>', + 'expected': '<font color="green">[foobarbaz]</font>' }, + + { 'id': 'FC:g_SPANs:c:g-1_SW', + 'rte1-id': 'c-forecolor-1', + 'desc': 'Change the text color from a styled span (without CSS)', + 'value': 'green', + 'pad': '<span style="color: blue">[foobarbaz]</span>', + 'expected': '<font color="green">[foobarbaz]</font>' }, + + { 'id': 'FC:g_FONTc:b.s:c:r-1_SW', + 'rte1-id': 'c-forecolor-2', + 'desc': 'Change the text color from conflicting color and style (without CSS)', + 'value': 'green', + 'pad': '<font color="blue" style="color: red">[foobarbaz]</font>', + 'expected': '<font color="green">[foobarbaz]</font>' }, + + { 'id': 'FC:g_FONTc:b.sz:6-1_SI', + 'desc': 'Change the font color in content with a different font size and font color', + 'value': 'green', + 'pad': '<font color="blue" size="6">foo[bar]baz</font>', + 'expected': [ '<font color="blue" size="6">foo<font color="green">[bar]</font>baz</font>', + '<font size="6"><font color="blue">foo<font color="green">[bar]</font><font color="blue">baz</font></font>' ] } + ] + }, + + { 'desc': '[MIDAS] hilitecolor', + 'command': 'hilitecolor', + 'tests': [ + { 'id': 'HC:g_FONTs:c:b-1_SW', + 'rte1-id': 'c-hilitecolor-0', + 'desc': 'Change the hilite color (without CSS)', + 'value': 'green', + 'pad': '<font style="background-color: blue">[foobarbaz]</font>', + 'expected': [ '<font style="background-color: green">[foobarbaz]</font>', + '<span style="background-color: green">[foobarbaz]</span>' ] }, + + { 'id': 'HC:g_SPANs:c:g-1_SW', + 'rte1-id': 'c-hilitecolor-2', + 'desc': 'Change the hilite color from a styled span (without CSS)', + 'value': 'green', + 'pad': '<span style="background-color: blue">[foobarbaz]</span>', + 'expected': '<span style="background-color: green">[foobarbaz]</span>' }, + + { 'id': 'HC:g_SPAN.ass.s:c:rgb-1_SW', + 'rte1-id': 'c-hilitecolor-1', + 'desc': 'Change the hilite color from a styled span (without CSS)', + 'value': 'green', + 'pad': '<span class="Apple-style-span" style="background-color: rgb(255, 0, 0);">[foobarbaz]</span>', + 'expected': '<span style="background-color: green">[foobarbaz]</span>' } + ] + }, + + { 'desc': '[MIDAS] fontname', + 'command': 'fontname', + 'tests': [ + { 'id': 'FN:c_FONTf:a-1_SW', + 'rte1-id': 'c-fontname-0', + 'desc': 'Change existing font name to new font name (without CSS)', + 'value': 'courier', + 'pad': '<font face="arial">[foobarbaz]</font>', + 'expected': '<font face="courier">[foobarbaz]</font>' }, + + { 'id': 'FN:c_SPANs:ff:a-1_SW', + 'rte1-id': 'c-fontname-1', + 'desc': 'Change existing font name from style to new font name (without CSS)', + 'value': 'courier', + 'pad': '<span style="font-family: arial">[foobarbaz]</span>', + 'expected': '<font face="courier">[foobarbaz]</font>' }, + + { 'id': 'FN:c_FONTf:a.s:ff:v-1_SW', + 'rte1-id': 'c-fontname-2', + 'desc': 'Change existing font name with conflicting face and style to new font name (without CSS)', + 'value': 'courier', + 'pad': '<font face="arial" style="font-family: verdana">[foobarbaz]</font>', + 'expected': '<font face="courier">[foobarbaz]</font>' }, + + { 'id': 'FN:c_FONTf:a-1_SI', + 'desc': 'Change existing font name to new font name, text partially selected', + 'value': 'courier', + 'pad': '<font face="arial">foo[bar]baz</font>', + 'expected': '<font face="arial">foo</font><font face="courier">[bar]</font><font face="arial">baz</font>', + 'accept': '<font face="arial">foo<font face="courier">[bar]</font>baz</font>' }, + + { 'id': 'FN:c_FONTf:a-2_SL', + 'desc': 'Change existing font name to new font name, using CSS styling', + 'value': 'courier', + 'pad': 'foo[bar<font face="arial">baz]qoz</font>', + 'expected': 'foo<font face="courier">[barbaz]</font><font face="arial">qoz</font>' }, + + { 'id': 'FN:c_FONTf:v-FONTf:a-1_SW', + 'rte1-id': 'c-fontname-3', + 'desc': 'Change existing font name in nested <font> tags to new font name (without CSS)', + 'value': 'courier', + 'pad': '<font face="verdana"><font face="arial">[foobarbaz]</font></font>', + 'expected': '<font face="courier">[foobarbaz]</font>', + 'accept': '<font face="verdana"><font face="courier">[foobarbaz]</font></font>' }, + + { 'id': 'FN:c_SPANs:ff:v-FONTf:a-1_SW', + 'rte1-id': 'c-fontname-4', + 'desc': 'Change existing font name in nested mixed tags to new font name (without CSS)', + 'value': 'courier', + 'pad': '<span style="font-family: verdana"><font face="arial">[foobarbaz]</font></span>', + 'expected': '<font face="courier">[foobarbaz]</font>', + 'accept': '<span style="font-family: verdana"><font face="courier">[foobarbaz]</font></span>' } + ] + }, + + { 'desc': '[MIDAS] fontsize', + 'command': 'fontsize', + 'tests': [ + { 'id': 'FS:1_FONTsz:4-1_SW', + 'rte1-id': 'c-fontsize-0', + 'desc': 'Change existing font size to new size (without CSS)', + 'value': '1', + 'pad': '<font size="4">[foobarbaz]</font>', + 'expected': '<font size="1">[foobarbaz]</font>' }, + + { 'id': 'FS:1_SPAN.ass.s:fs:large-1_SW', + 'rte1-id': 'c-fontsize-1', + 'desc': 'Change existing font size from styled span to new size (without CSS)', + 'value': '1', + 'pad': '<span class="Apple-style-span" style="font-size: large">[foobarbaz]</span>', + 'expected': '<font size="1">[foobarbaz]</font>' }, + + { 'id': 'FS:5_FONTsz:1.s:fs:xs-1_SW', + 'rte1-id': 'c-fontsize-2', + 'desc': 'Change existing font size from tag with conflicting size and style to new size (without CSS)', + 'value': '5', + 'pad': '<font size="1" style="font-size:x-small">[foobarbaz]</font>', + 'expected': '<font size="5">[foobarbaz]</font>' }, + + { 'id': 'FS:2_FONTc:b.sz:6-1_SI', + 'desc': 'Change the font size in content with a different font size and font color', + 'value': '2', + 'pad': '<font color="blue" size="6">foo[bar]baz</font>', + 'expected': [ '<font color="blue" size="6">foo<font size="2">[bar]</font>baz</font>', + '<font color="blue"><font size="6">foo</font><font size="2">[bar]</font><font size="6">baz</font></font>' ] }, + + { 'id': 'FS:larger_FONTsz:4', + 'desc': 'Change selection to use next larger font', + 'value': 'larger', + 'pad': '<font size="4">foo[bar]baz</font>', + 'expected': '<font size="4">foo<font size="larger">[bar]</font>baz</font>', + 'accept': '<font size="4">foo</font><font size="5">[bar]</font><font size="4">baz</font>' }, + + { 'id': 'FS:smaller_FONTsz:4', + 'desc': 'Change selection to use next smaller font', + 'value': 'smaller', + 'pad': '<font size="4">foo[bar]baz</font>', + 'expected': '<font size="4">foo<font size="smaller">[bar]</font>baz</font>', + 'accept': '<font size="4">foo</font><font size="3">[bar]</font><font size="4">baz</font>' } + ] + }, + + { 'desc': '[MIDAS] formatblock', + 'command': 'formatblock', + 'tests': [ + { 'id': 'FB:h1_ADDRESS-1_SW', + 'desc': 'change block from <address> to <h1>', + 'value': 'h1', + 'pad': '<address>foo [bar] baz</address>', + 'expected': '<h1>foo [bar] baz</h1>' }, + + { 'id': 'FB:h1_ADDRESS-FONTsz:4-1_SO', + 'desc': 'change block from <address> with partially formatted content to <h1>', + 'value': 'h1', + 'pad': '<address>foo [<font size="4">bar</font>] baz</address>', + 'expected': '<h1>foo [bar] baz</h1>' }, + + { 'id': 'FB:h1_ADDRESS-FONTsz:4-1_SW', + 'desc': 'change block from <address> with partially formatted content to <h1>', + 'value': 'h1', + 'pad': '<address>foo <font size="4">[bar]</font> baz</address>', + 'expected': '<h1>foo [bar] baz</h1>' }, + + { 'id': 'FB:h1_ADDRESS-FONT.ass.sz:4-1_SW', + 'desc': 'change block from <address> with partially formatted content to <h1>', + 'value': 'h1', + 'pad': '<address>foo <font class="Apple-style-span" size="4">[bar]</font> baz</address>', + 'expected': '<h1>foo [bar] baz</h1>' } + ] + } + ] +} + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/changeCSS.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/changeCSS.py new file mode 100644 index 0000000000..4862b9b733 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/changeCSS.py @@ -0,0 +1,210 @@ + +CHANGE_TESTS_CSS = { + 'id': 'CC', + 'caption': 'Change Existing Format to Different Format Tests, using styleWithCSS', + 'checkAttrs': True, + 'checkStyle': True, + 'styleWithCSS': True, + + 'Proposed': [ + { 'desc': '', + 'command': '', + 'tests': [ + ] + }, + + { 'desc': '[HTML5] italic', + 'command': 'italic', + 'tests': [ + { 'id': 'I_I-1_SL', + 'desc': 'Italicize partially italicized text', + 'pad': 'foo[bar<i>baz]</i>qoz', + 'expected': 'foo<span style="font-style: italic">[barbaz]</span>qoz' }, + + { 'id': 'I_B-1_SL', + 'desc': 'Italicize partially bolded text', + 'pad': 'foo[bar<b>baz]</b>qoz', + 'expected': 'foo<span style="font-style: italic">[bar<b>baz]</b></span>qoz', + 'accept': 'foo<span style="font-style: italic">[bar<b>baz</b>}</span>qoz' }, + + { 'id': 'I_B-1_SW', + 'desc': 'Italicize bold text, ideally combining both', + 'pad': 'foobar<b>[baz]</b>qoz', + 'expected': 'foobar<span style="font-style: italic; font-weight: bold">[baz]</span>qoz', + 'accept': 'foobar<b><span style="font-style: italic">[baz]</span></b>qoz' } + ] + }, + + { 'desc': '[MIDAS] backcolor', + 'command': 'backcolor', + 'tests': [ + { 'id': 'BC:gray_SPANs:bc:b-1_SW', + 'desc': 'Change background color from blue to gray', + 'value': 'gray', + 'pad': '<span style="background-color: blue">[foobarbaz]</span>', + 'expected': '<span style="background-color: gray">[foobarbaz]</span>' }, + + { 'id': 'BC:gray_SPANs:bc:b-1_SO', + 'desc': 'Change background color from blue to gray', + 'value': 'gray', + 'pad': '{<span style="background-color: blue">foobarbaz</span>}', + 'expected': [ '{<span style="background-color: gray">foobarbaz</span>}', + '<span style="background-color: gray">[foobarbaz]</span>' ] }, + + { 'id': 'BC:gray_SPANs:bc:b-1_SI', + 'desc': 'Change background color from blue to gray', + 'value': 'gray', + 'pad': '<span style="background-color: blue">foo[bar]baz</span>', + 'expected': '<span style="background-color: blue">foo</span><span style="background-color: gray">[bar]</span><span style="background-color: blue">baz</span>', + 'accept': '<span style="background-color: blue">foo<span style="background-color: gray">[bar]</span>baz</span>' }, + + { 'id': 'BC:gray_P-SPANs:bc:b-1_SW', + 'desc': 'Change background color within a paragraph from blue to gray', + 'value': 'gray', + 'pad': '<p><span style="background-color: blue">[foobarbaz]</span></p>', + 'expected': [ '<p><span style="background-color: gray">[foobarbaz]</span></p>', + '<p style="background-color: gray">[foobarbaz]</p>' ] }, + + { 'id': 'BC:gray_P-SPANs:bc:b-2_SW', + 'desc': 'Change background color within a paragraph from blue to gray', + 'value': 'gray', + 'pad': '<p>foo<span style="background-color: blue">[bar]</span>baz</p>', + 'expected': '<p>foo<span style="background-color: gray">[bar]</span>baz</p>' }, + + { 'id': 'BC:gray_P-SPANs:bc:b-3_SO', + 'desc': 'Change background color within a paragraph from blue to gray (selection encloses more than previous span)', + 'value': 'gray', + 'pad': '<p>[foo<span style="background-color: blue">barbaz</span>qoz]quz</p>', + 'expected': '<p><span style="background-color: gray">[foobarbazqoz]</span>quz</p>' }, + + { 'id': 'BC:gray_P-SPANs:bc:b-3_SL', + 'desc': 'Change background color within a paragraph from blue to gray (previous span partially selected)', + 'value': 'gray', + 'pad': '<p>[foo<span style="background-color: blue">bar]baz</span>qozquz</p>', + 'expected': '<p><span style="background-color: gray">[foobar]</span><span style="background-color: blue">baz</span>qozquz</p>' }, + + { 'id': 'BC:gray_SPANs:bc:b-2_SL', + 'desc': 'Change background color from blue to gray on partially covered span, selection extends left', + 'value': 'gray', + 'pad': 'foo [bar <span style="background-color: blue">baz] qoz</span> quz sic', + 'expected': 'foo <span style="background-color: gray">[bar baz]</span><span style="background-color: blue"> qoz</span> quz sic' }, + + { 'id': 'BC:gray_SPANs:bc:b-2_SR', + 'desc': 'Change background color from blue to gray on partially covered span, selection extends right', + 'value': 'gray', + 'pad': 'foo bar <span style="background-color: blue">baz [qoz</span> quz] sic', + 'expected': 'foo bar <span style="background-color: blue">baz </span><span style="background-color: gray">[qoz quz]</span> sic' } + ] + }, + + { 'desc': '[MIDAS] fontname', + 'command': 'fontname', + 'tests': [ + { 'id': 'FN:c_SPANs:ff:a-1_SW', + 'desc': 'Change existing font name to new font name, using CSS styling', + 'value': 'courier', + 'pad': '<span style="font-family: arial">[foobarbaz]</span>', + 'expected': '<span style="font-family: courier">[foobarbaz]</span>' }, + + { 'id': 'FN:c_FONTf:a-1_SW', + 'desc': 'Change existing font name to new font name, using CSS styling', + 'value': 'courier', + 'pad': '<font face="arial">[foobarbaz]</font>', + 'expected': [ '<font style="font-family: courier">[foobarbaz]</font>', + '<span style="font-family: courier">[foobarbaz]</span>' ] }, + + { 'id': 'FN:c_FONTf:a-1_SI', + 'desc': 'Change existing font name to new font name, using CSS styling', + 'value': 'courier', + 'pad': '<font face="arial">foo[bar]baz</font>', + 'expected': '<font face="arial">foo</font><span style="font-family: courier">[bar]</span><font face="arial">baz</font>' }, + + { 'id': 'FN:a_FONTf:a-1_SI', + 'desc': 'Change existing font name to same font name, using CSS styling (should be noop)', + 'value': 'arial', + 'pad': '<font face="arial">foo[bar]baz</font>', + 'expected': '<font face="arial">foo[bar]baz</font>' }, + + { 'id': 'FN:a_FONTf:a-1_SW', + 'desc': 'Change existing font name to same font name, using CSS styling (should be noop or perhaps change tag)', + 'value': 'arial', + 'pad': '<font face="arial">[foobarbaz]</font>', + 'expected': [ '<font face="arial">[foobarbaz]</font>', + '<span style="font-family: arial">[foobarbaz]</span>' ] }, + + { 'id': 'FN:a_FONTf:a-1_SO', + 'desc': 'Change existing font name to same font name, using CSS styling (should be noop or perhaps change tag)', + 'value': 'arial', + 'pad': '{<font face="arial">foobarbaz</font>}', + 'expected': [ '{<font face="arial">foobarbaz</font>}', + '<font face="arial">[foobarbaz]</font>', + '{<span style="font-family: arial">foobarbaz</span>}', + '<span style="font-family: arial">[foobarbaz]</span>' ] }, + + { 'id': 'FN:a_SPANs:ff:a-1_SI', + 'desc': 'Change existing font name to same font name, using CSS styling (should be noop)', + 'value': 'arial', + 'pad': '<span style="font-family: arial">[foobarbaz]</span>', + 'expected': '<span style="font-family: arial">[foobarbaz]</span>' }, + + { 'id': 'FN:c_FONTf:a-2_SL', + 'desc': 'Change existing font name to new font name, using CSS styling', + 'value': 'courier', + 'pad': 'foo[bar<font face="arial">baz]qoz</font>', + 'expected': 'foo<span style="font-family: courier">[barbaz]</span><font face="arial">qoz</font>' } + ] + }, + + { 'desc': '[MIDAS] fontsize', + 'command': 'fontsize', + 'tests': [ + { 'id': 'FS:1_SPANs:fs:l-1_SW', + 'desc': 'Change existing font size to new size, using CSS styling', + 'value': '1', + 'pad': '<span style="font-size: large">[foobarbaz]</span>', + 'expected': '<span style="font-size: x-small">[foobarbaz]</span>' }, + + { 'id': 'FS:large_SPANs:fs:l-1_SW', + 'desc': 'Change existing font size to same size (should be noop)', + 'value': 'large', + 'pad': '<span style="font-size: large">[foobarbaz]</span>', + 'expected': '<span style="font-size: large">[foobarbaz]</span>' }, + + { 'id': 'FS:18px_SPANs:fs:l-1_SW', + 'desc': 'Change existing font size to equivalent px size (should be noop, or change unit)', + 'value': '18px', + 'pad': '<span style="font-size: large">[foobarbaz]</span>', + 'expected': [ '<span style="font-size: 18px">[foobarbaz]</span>', + '<span style="font-size: large">[foobarbaz]</span>' ] }, + + { 'id': 'FS:4_SPANs:fs:l-1_SW', + 'desc': 'Change existing font size to equivalent numeric size (should be noop)', + 'value': '4', + 'pad': '<span style="font-size: large">[foobarbaz]</span>', + 'expected': '<span style="font-size: large">[foobarbaz]</span>' }, + + { 'id': 'FS:4_SPANs:fs:18px-1_SW', + 'desc': 'Change existing font size to equivalent numeric size (should be noop)', + 'value': '4', + 'pad': '<span style="font-size: 18px">[foobarbaz]</span>', + 'expected': '<span style="font-size: 18px">[foobarbaz]</span>' }, + + { 'id': 'FS:larger_SPANs:fs:l-1_SI', + 'desc': 'Change selection to use next larger font', + 'value': 'larger', + 'pad': '<span style="font-size: large">foo[bar]baz</span>', + 'expected': [ '<span style="font-size: large">foo<span style="font-size: x-large">[bar]</span>baz</span>', + '<span style="font-size: large">foo</span><span style="font-size: x-large">[bar]</span><span style="font-size: large">baz</span>' ], + 'accept': '<span style="font-size: large">foo<font size="larger">[bar]</font>baz</span>' }, + + { 'id': 'FS:smaller_SPANs:fs:l-1_SI', + 'desc': 'Change selection to use next smaller font', + 'value': 'smaller', + 'pad': '<span style="font-size: large">foo[bar]baz</span>', + 'expected': [ '<span style="font-size: large">foo<span style="font-size: medium">[bar]</span>baz</span>', + '<span style="font-size: large">foo</span><span style="font-size: medium">[bar]</span><span style="font-size: large">baz</span>' ], + 'accept': '<span style="font-size: large">foo<font size="smaller">[bar]</font>baz</span>' } + ] + } + ] +} diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/delete.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/delete.py new file mode 100644 index 0000000000..6fb81f76cc --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/delete.py @@ -0,0 +1,330 @@ + +DELETE_TESTS = { + 'id': 'D', + 'caption': 'Delete Tests', + 'command': 'delete', + 'checkAttrs': True, + 'checkStyle': False, + + 'Proposed': [ + { 'desc': '', + 'tests': [ + ] + }, + + { 'desc': 'delete single characters', + 'tests': [ + { 'id': 'CHAR-1_SC', + 'desc': 'Delete 1 character', + 'pad': 'foo^barbaz', + 'expected': 'fo^barbaz' }, + + { 'id': 'CHAR-2_SC', + 'desc': 'Delete 1 pre-composed character o with diaeresis', + 'pad': 'foö^barbaz', + 'expected': 'fo^barbaz' }, + + { 'id': 'CHAR-3_SC', + 'desc': 'Delete 1 character with combining diaeresis above', + 'pad': 'foö^barbaz', + 'expected': 'fo^barbaz' }, + + { 'id': 'CHAR-4_SC', + 'desc': 'Delete 1 character with combining diaeresis below', + 'pad': 'foo̤^barbaz', + 'expected': 'fo^barbaz' }, + + { 'id': 'CHAR-5_SC', + 'desc': 'Delete 1 character with combining diaeresis above and below', + 'pad': 'foö̤^barbaz', + 'expected': 'fo^barbaz' }, + + { 'id': 'CHAR-5_SI-1', + 'desc': 'Delete 1 character with combining diaeresis above and below, selection on diaeresis above', + 'pad': 'foo[̈]̤barbaz', + 'expected': 'fo^barbaz' }, + + { 'id': 'CHAR-5_SI-2', + 'desc': 'Delete 1 character with combining diaeresis above and below, selection on diaeresis below', + 'pad': 'foö[̤]barbaz', + 'expected': 'fo^barbaz' }, + + { 'id': 'CHAR-5_SR', + 'desc': 'Delete 1 character with combining diaeresis above and below, selection oblique on diaeresis and following text', + 'pad': 'foö[̤bar]baz', + 'expected': 'fo^baz' }, + + { 'id': 'CHAR-6_SC', + 'desc': 'Delete 1 character with enclosing square', + 'pad': 'foo⃞^barbaz', + 'expected': 'fo^barbaz' }, + + { 'id': 'CHAR-7_SC', + 'desc': 'Delete 1 character with combining long solidus overlay', + 'pad': 'foo̸^barbaz', + 'expected': 'fo^barbaz' } + ] + }, + + { 'desc': 'delete text selection', + 'tests': [ + { 'id': 'TEXT-1_SI', + 'desc': 'Delete text selection', + 'pad': 'foo[bar]baz', + 'expected': 'foo^baz' }, + + { 'id': 'B-1_SS', + 'desc': 'Delete at start of span', + 'pad': 'foo<b>^bar</b>baz', + 'expected': 'fo^<b>bar</b>baz' }, + + { 'id': 'B-1_SA', + 'desc': 'Delete from position after span', + 'pad': 'foo<b>bar</b>^baz', + 'expected': 'foo<b>ba^</b>baz' }, + + { 'id': 'B-1_SW', + 'desc': 'Delete selection that wraps the whole span content', + 'pad': 'foo<b>[bar]</b>baz', + 'expected': 'foo^baz' }, + + { 'id': 'B-1_SO', + 'desc': 'Delete selection that wraps the whole span', + 'pad': 'foo[<b>bar</b>]baz', + 'expected': 'foo^baz' }, + + { 'id': 'B-1_SL', + 'desc': 'Delete oblique selection that starts before span', + 'pad': 'foo[bar<b>baz]quoz</b>quuz', + 'expected': 'foo^<b>quoz</b>quuz' }, + + { 'id': 'B-1_SR', + 'desc': 'Delete oblique selection that ends after span', + 'pad': 'foo<b>bar[baz</b>quoz]quuz', + 'expected': 'foo<b>bar^</b>quuz' }, + + { 'id': 'B.I-1_SM', + 'desc': 'Delete oblique selection that starts and ends in different spans', + 'pad': 'foo<b>bar[baz</b><i>qoz]quuz</i>quuuz', + 'expected': 'foo<b>bar^</b><i>quuz</i>quuuz' }, + + { 'id': 'GEN-1_SS', + 'desc': 'Delete at start of span with generated content', + 'pad': 'foo<gen>^bar</gen>baz', + 'expected': 'fo^<gen>bar</gen>baz' }, + + { 'id': 'GEN-1_SA', + 'desc': 'Delete from position after span with generated content', + 'pad': 'foo<gen>bar</gen>^baz', + 'expected': 'foo<gen>ba^</gen>baz' } + ] + }, + + { 'desc': 'delete paragraphs', + 'tests': [ + { 'id': 'P2-1_SS2', + 'desc': 'Delete from collapsed selection at start of paragraph - should merge with previous', + 'pad': '<p>foobar</p><p>^bazqoz</p>', + 'expected': '<p>foobar^bazqoz</p>' }, + + { 'id': 'P2-1_SI2', + 'desc': 'Delete non-collapsed selection at start of paragraph - should not merge with previous', + 'pad': '<p>foobar</p><p>[baz]qoz</p>', + 'expected': '<p>foobar</p><p>^qoz</p>' }, + + { 'id': 'P2-1_SM', + 'desc': 'Delete non-collapsed selection spanning 2 paragraphs - should merge them', + 'pad': '<p>foo[bar</p><p>baz]qoz</p>', + 'expected': '<p>foo^qoz</p>' } + ] + }, + + { 'desc': 'delete lists and list items', + 'tests': [ + { 'id': 'OL-LI2-1_SO1', + 'desc': 'Delete fully wrapped list item', + 'pad': 'foo<ol>{<li>bar</li>}<li>baz</li></ol>qoz', + 'expected': ['foo<ol>|<li>baz</li></ol>qoz', + 'foo<ol><li>^baz</li></ol>qoz'] }, + + { 'id': 'OL-LI2-1_SM', + 'desc': 'Delete oblique range between list items within same list', + 'pad': 'foo<ol><li>ba[r</li><li>b]az</li></ol>qoz', + 'expected': 'foo<ol><li>ba^az</li></ol>qoz' }, + + { 'id': 'OL-LI-1_SW', + 'desc': 'Delete contents of last list item (list should remain)', + 'pad': 'foo<ol><li>[foo]</li></ol>qoz', + 'expected': ['foo<ol><li>|</li></ol>qoz', + 'foo<ol><li>^</li></ol>qoz'] }, + + { 'id': 'OL-LI-1_SO', + 'desc': 'Delete last list item of list (should remove entire list)', + 'pad': 'foo<ol>{<li>foo</li>}</ol>qoz', + 'expected': 'foo^qoz' } + ] + }, + + { 'desc': 'delete with strange selections', + 'tests': [ + { 'id': 'HR.BR-1_SM', + 'desc': 'Delete selection that starts and ends within nodes that don\'t have children', + 'pad': 'foo<hr {>bar<br }>baz', + 'expected': 'foo<hr>|<br>baz' } + ] + }, + + { 'desc': 'delete after table', + 'tests': [ + { 'id': 'TABLE-1_SA', + 'desc': 'Delete from position immediately after table (should have no effect)', + 'pad': 'foo<table><tbody><tr><td>bar</td></tr></tbody></table>^baz', + 'expected': 'foo<table><tbody><tr><td>bar</td></tr></tbody></table>^baz' } + ] + }, + + { 'desc': 'delete within table cells', + 'tests': [ + { 'id': 'TD-1_SS', + 'desc': 'Delete from start of first cell (should have no effect)', + 'pad': 'foo<table><tbody><tr><td>^bar</td></tr></tbody></table>baz', + 'expected': 'foo<table><tbody><tr><td>^bar</td></tr></tbody></table>baz' }, + + { 'id': 'TD2-1_SS2', + 'desc': 'Delete from start of inner cell (should have no effect)', + 'pad': 'foo<table><tbody><tr><td>bar</td><td>^baz</td></tr></tbody></table>quoz', + 'expected': 'foo<table><tbody><tr><td>bar</td><td>^baz</td></tr></tbody></table>quoz' }, + + { 'id': 'TD2-1_SM', + 'desc': 'Delete with selection spanning 2 cells', + 'pad': 'foo<table><tbody><tr><td>ba[r</td><td>b]az</td></tr></tbody></table>quoz', + 'expected': 'foo<table><tbody><tr><td>ba^</td><td>az</td></tr></tbody></table>quoz' } + ] + }, + + { 'desc': 'delete table rows', + 'tests': [ + { 'id': 'TR3-1_SO1', + 'desc': 'Delete first table row', + 'pad': '<table><tbody>{<tr><td>A</td></tr>}<tr><td>B</td></tr><tr><td>C</td></tr></tbody></table>', + 'expected': ['<table><tbody>|<tr><td>B</td></tr><tr><td>C</td></tr></tbody></table>', + '<table><tbody><tr><td>^B</td></tr><tr><td>C</td></tr></tbody></table>'] }, + + { 'id': 'TR3-1_SO2', + 'desc': 'Delete middle table row', + 'pad': '<table><tbody><tr><td>A</td></tr>{<tr><td>B</td></tr>}<tr><td>C</td></tr></tbody></table>', + 'expected': ['<table><tbody><tr><td>A</td></tr>|<tr><td>C</td></tr></tbody></table>', + '<table><tbody><tr><td>A</td></tr><tr><td>^C</td></tr></tbody></table>'] }, + + { 'id': 'TR3-1_SO3', + 'desc': 'Delete last table row', + 'pad': '<table><tbody><tr><td>A</td></tr><tr><td>B</td></tr>{<tr><td>C</td></tr>}</tbody></table>', + 'expected': ['<table><tbody><tr><td>A</td></tr><tr><td>B</td></tr>|</tbody></table>', + '<table><tbody><tr><td>A</td></tr><tr><td>B^</td></tr></tbody></table>'] }, + + { 'id': 'TR2rs:2-1_SO1', + 'desc': 'Delete first table row where a cell has rowspan 2', + 'pad': '<table><tbody>{<tr><td>A</td><td rowspan=2>R</td></tr>}<tr><td>B</td></tr></tbody></table>', + 'expected': ['<table><tbody>|<tr><td>B</td><td>R</td></tr></tbody></table>', + '<table><tbody><tr><td>^B</td><td>R</td></tr></tbody></table>'] }, + + { 'id': 'TR2rs:2-1_SO2', + 'desc': 'Delete second table row where a cell has rowspan 2', + 'pad': '<table><tbody><tr><td>A</td><td rowspan=2>R</td></tr>{<tr><td>B</td></tr>}</tbody></table>', + 'expected': ['<table><tbody><tr><td>A</td><td>R</td></tr>|</tbody></table>', + '<table><tbody><tr><td>A</td><td>R^</td></tr></tbody></table>'] }, + + { 'id': 'TR3rs:3-1_SO1', + 'desc': 'Delete first table row where a cell has rowspan 3', + 'pad': '<table><tbody>{<tr><td>A</td><td rowspan=3>R</td></tr>}<tr><td>B</td></tr><tr><td>C</td></tr></tbody></table>', + 'expected': ['<table><tbody>|<tr><td>A</td><td rowspan="2">R</td></tr><tr><td>C</td></tr></tbody></table>', + '<table><tbody><tr><td>^A</td><td rowspan="2">R</td></tr><tr><td>C</td></tr></tbody></table>'] }, + + { 'id': 'TR3rs:3-1_SO2', + 'desc': 'Delete middle table row where a cell has rowspan 3', + 'pad': '<table><tbody><tr><td>A</td><td rowspan=3>R</td></tr>{<tr><td>B</td></tr>}<tr><td>C</td></tr></tbody></table>', + 'expected': ['<table><tbody><tr><td>B</td><td rowspan="2">R</td></tr>|<tr><td>C</td></tr></tbody></table>', + '<table><tbody><tr><td>B</td><td rowspan="2">R</td></tr><tr><td>^C</td></tr></tbody></table>'] }, + + { 'id': 'TR3rs:3-1_SO3', + 'desc': 'Delete last table row where a cell has rowspan 3', + 'pad': '<table><tbody><tr><td>A</td><td rowspan=3>R</td></tr><tr><td>B</td></tr>{<tr><td>C</td></tr>}</tbody></table>', + 'expected': ['<table><tbody><tr><td>A</td><td rowspan="2">R</td></tr><tr><td>B</td></tr>|</tbody></table>', + '<table><tbody><tr><td>A</td><td rowspan="2">R</td></tr><tr><td>B^</td></tr></tbody></table>'] } + ] + }, + + { 'desc': 'delete with non-editable nested content', + 'tests': [ + { 'id': 'DIV:ce:false-1_SO', + 'desc': 'Delete nested non-editable <div>', + 'pad': 'foo[bar<div contenteditable="false">NESTED</div>baz]qoz', + 'expected': 'foo^qoz' }, + + { 'id': 'DIV:ce:false-1_SB', + 'desc': 'Delete from immediately after a nested non-editable <div>', + 'pad': 'foobar<div contenteditable="false">NESTED</div>^bazqoz', + 'expected': 'foobar^bazqoz' }, + + { 'id': 'DIV:ce:false-1_SL', + 'desc': 'Delete nested non-editable <div> with oblique selection', + 'pad': 'foo[bar<div contenteditable="false">NES]TED</div>bazqoz', + 'expected': [ 'foo^<div contenteditable="false">NESTED</div>bazqoz', + 'foo<div contenteditable="false">[NES]TED</div>bazqoz' ] }, + + { 'id': 'DIV:ce:false-1_SR', + 'desc': 'Delete nested non-editable <div> with oblique selection', + 'pad': 'foobar<div contenteditable="false">NES[TED</div>baz]qoz', + 'expected': [ 'foobar<div contenteditable="false">NESTED</div>^qoz', + 'foobar<div contenteditable="false">NES[TED]</div>qoz' ] }, + + { 'id': 'DIV:ce:false-1_SI', + 'desc': 'Delete inside nested non-editable <div> (should be no-op)', + 'pad': 'foobar<div contenteditable="false">NE[ST]ED</div>bazqoz', + 'expected': 'foobar<div contenteditable="false">NE[ST]ED</div>bazqoz' } + ] + }, + + { 'desc': 'Delete with display:inline-block', + 'checkStyle': True, + 'tests': [ + { 'id': 'SPAN:d:ib-1_SC', + 'desc': 'Delete inside an inline-block <span>', + 'pad': 'foo<span style="display: inline-block">bar^baz</span>qoz', + 'expected': 'foo<span style="display: inline-block">ba^baz</span>qoz' }, + + { 'id': 'SPAN:d:ib-1_SA', + 'desc': 'Delete from immediately after an inline-block <span>', + 'pad': 'foo<span style="display: inline-block">barbaz</span>^qoz', + 'expected': 'foo<span style="display: inline-block">barba^</span>qoz' }, + + { 'id': 'SPAN:d:ib-2_SL', + 'desc': 'Delete with nested inline-block <span>, oblique selection', + 'pad': 'foo[DEL<span style="display: inline-block">ETE]bar</span>baz', + 'expected': 'foo^<span style="display: inline-block">bar</span>baz' }, + + { 'id': 'SPAN:d:ib-3_SR', + 'desc': 'Delete with nested inline-block <span>, oblique selection', + 'pad': 'foo<span style="display: inline-block">bar[DEL</span>ETE]baz', + 'expected': 'foo<span style="display: inline-block">bar^</span>baz' }, + + { 'id': 'SPAN:d:ib-4i_SI', + 'desc': 'Delete with nested inline-block <span>, oblique selection', + 'pad': 'foo<span style="display: inline-block">bar[DELETE]baz</span>qoz', + 'expected': 'foo<span style="display: inline-block">bar^baz</span>qoz' }, + + { 'id': 'SPAN:d:ib-4l_SI', + 'desc': 'Delete with nested inline-block <span>, oblique selection', + 'pad': 'foo<span style="display: inline-block">[DELETE]barbaz</span>qoz', + 'expected': 'foo<span style="display: inline-block">^barbaz</span>qoz' }, + + { 'id': 'SPAN:d:ib-4r_SI', + 'desc': 'Delete with nested inline-block <span>, oblique selection', + 'pad': 'foo<span style="display: inline-block">barbaz[DELETE]</span>qoz', + 'expected': 'foo<span style="display: inline-block">barbaz^</span>qoz' } + ] + } + ] +} + + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/forwarddelete.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/forwarddelete.py new file mode 100644 index 0000000000..813b22914a --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/forwarddelete.py @@ -0,0 +1,315 @@ + +FORWARDDELETE_TESTS = { + 'id': 'FD', + 'caption': 'Forward-Delete Tests', + 'command': 'forwardDelete', + 'checkAttrs': True, + 'checkStyle': False, + + 'Proposed': [ + { 'desc': '', + 'tests': [ + ] + }, + + { 'desc': 'forward-delete single characters', + 'tests': [ + { 'id': 'CHAR-1_SC', + 'desc': 'Delete 1 character', + 'pad': 'foo^barbaz', + 'expected': 'foo^arbaz' }, + + { 'id': 'CHAR-2_SC', + 'desc': 'Delete 1 pre-composed character o with diaeresis', + 'pad': 'fo^öbarbaz', + 'expected': 'fo^barbaz' }, + + { 'id': 'CHAR-3_SC', + 'desc': 'Delete 1 character with combining diaeresis above', + 'pad': 'fo^öbarbaz', + 'expected': 'fo^barbaz' }, + + { 'id': 'CHAR-4_SC', + 'desc': 'Delete 1 character with combining diaeresis below', + 'pad': 'fo^o̤barbaz', + 'expected': 'fo^barbaz' }, + + { 'id': 'CHAR-5_SC', + 'desc': 'Delete 1 character with combining diaeresis above and below', + 'pad': 'fo^ö̤barbaz', + 'expected': 'fo^barbaz' }, + + { 'id': 'CHAR-6_SC', + 'desc': 'Delete 1 character with enclosing square', + 'pad': 'fo^o⃞barbaz', + 'expected': 'fo^barbaz' }, + + { 'id': 'CHAR-7_SC', + 'desc': 'Delete 1 character with combining long solidus overlay', + 'pad': 'fo^o̸barbaz', + 'expected': 'fo^barbaz' } + ] + }, + + { 'desc': 'forward-delete text selections', + 'tests': [ + { 'id': 'TEXT-1_SI', + 'desc': 'Delete text selection', + 'pad': 'foo[bar]baz', + 'expected': 'foo^baz' }, + + { 'id': 'B-1_SE', + 'desc': 'Forward-delete at end of span', + 'pad': 'foo<b>bar^</b>baz', + 'expected': 'foo<b>bar^</b>az' }, + + { 'id': 'B-1_SB', + 'desc': 'Forward-delete from position before span', + 'pad': 'foo^<b>bar</b>baz', + 'expected': 'foo^<b>ar</b>baz' }, + + { 'id': 'B-1_SW', + 'desc': 'Delete selection that wraps the whole span content', + 'pad': 'foo<b>[bar]</b>baz', + 'expected': 'foo^baz' }, + + { 'id': 'B-1_SO', + 'desc': 'Delete selection that wraps the whole span', + 'pad': 'foo[<b>bar</b>]baz', + 'expected': 'foo^baz' }, + + { 'id': 'B-1_SL', + 'desc': 'Delete oblique selection that starts before span', + 'pad': 'foo[bar<b>baz]quoz</b>quuz', + 'expected': 'foo^<b>quoz</b>quuz' }, + + { 'id': 'B-1_SR', + 'desc': 'Delete oblique selection that ends after span', + 'pad': 'foo<b>bar[baz</b>quoz]quuz', + 'expected': 'foo<b>bar^</b>quuz' }, + + { 'id': 'B.I-1_SM', + 'desc': 'Delete oblique selection that starts and ends in different spans', + 'pad': 'foo<b>bar[baz</b><i>qoz]quuz</i>quuuz', + 'expected': 'foo<b>bar^</b><i>quuz</i>quuuz' }, + + { 'id': 'GEN-1_SE', + 'desc': 'Delete at end of span with generated content', + 'pad': 'foo<gen>bar^</gen>baz', + 'expected': 'foo<gen>bar^</gen>az' }, + + { 'id': 'GEN-1_SB', + 'desc': 'Delete from position before span with generated content', + 'pad': 'foo^<gen>bar</gen>baz', + 'expected': 'foo^<gen>ar</gen>baz' } + ] + }, + + { 'desc': 'forward-delete paragraphs', + 'tests': [ + { 'id': 'P2-1_SE1', + 'desc': 'Delete from collapsed selection at end of paragraph - should merge with next', + 'pad': '<p>foobar^</p><p>bazqoz</p>', + 'expected': '<p>foobar^bazqoz</p>' }, + + { 'id': 'P2-1_SI1', + 'desc': 'Delete non-collapsed selection at end of paragraph - should not merge with next', + 'pad': '<p>foo[bar]</p><p>bazqoz</p>', + 'expected': '<p>foo^</p><p>bazqoz</p>' }, + + { 'id': 'P2-1_SM', + 'desc': 'Delete non-collapsed selection spanning 2 paragraphs - should merge them', + 'pad': '<p>foo[bar</p><p>baz]qoz</p>', + 'expected': '<p>foo^qoz</p>' } + ] + }, + + { 'desc': 'forward-delete lists and list items', + 'tests': [ + { 'id': 'OL-LI2-1_SO1', + 'desc': 'Delete fully wrapped list item', + 'pad': 'foo<ol>{<li>bar</li>}<li>baz</li></ol>qoz', + 'expected': ['foo<ol>|<li>baz</li></ol>qoz', + 'foo<ol><li>^baz</li></ol>qoz'] }, + + { 'id': 'OL-LI2-1_SM', + 'desc': 'Delete oblique range between list items within same list', + 'pad': 'foo<ol><li>ba[r</li><li>b]az</li></ol>qoz', + 'expected': 'foo<ol><li>ba^az</li></ol>qoz' }, + + { 'id': 'OL-LI-1_SW', + 'desc': 'Delete contents of last list item (list should remain)', + 'pad': 'foo<ol><li>[foo]</li></ol>qoz', + 'expected': ['foo<ol><li>|</li></ol>qoz', + 'foo<ol><li>^</li></ol>qoz'] }, + + { 'id': 'OL-LI-1_SO', + 'desc': 'Delete last list item of list (should remove entire list)', + 'pad': 'foo<ol>{<li>foo</li>}</ol>qoz', + 'expected': 'foo^qoz' } + ] + }, + + { 'desc': 'forward-delete with strange selections', + 'tests': [ + { 'id': 'HR.BR-1_SM', + 'desc': 'Delete selection that starts and ends within nodes that don\'t have children', + 'pad': 'foo<hr {>bar<br }>baz', + 'expected': 'foo<hr>|<br>baz' } + ] + }, + + { 'desc': 'forward-delete from immediately before a table', + 'tests': [ + { 'id': 'TABLE-1_SB', + 'desc': 'Delete from position immediately before table (should have no effect)', + 'pad': 'foo^<table><tbody><tr><td>bar</td></tr></tbody></table>baz', + 'expected': 'foo^<table><tbody><tr><td>bar</td></tr></tbody></table>baz' } + ] + }, + + { 'desc': 'forward-delete within table cells', + 'tests': [ + { 'id': 'TD-1_SE', + 'desc': 'Delete from end of last cell (should have no effect)', + 'pad': 'foo<table><tbody><tr><td>bar^</td></tr></tbody></table>baz', + 'expected': 'foo<table><tbody><tr><td>bar^</td></tr></tbody></table>baz' }, + + { 'id': 'TD2-1_SE1', + 'desc': 'Delete from end of inner cell (should have no effect)', + 'pad': 'foo<table><tbody><tr><td>bar^</td><td>baz</td></tr></tbody></table>quoz', + 'expected': 'foo<table><tbody><tr><td>bar^</td><td>baz</td></tr></tbody></table>quoz' }, + + { 'id': 'TD2-1_SM', + 'desc': 'Delete with selection spanning 2 cells', + 'pad': 'foo<table><tbody><tr><td>ba[r</td><td>b]az</td></tr></tbody></table>quoz', + 'expected': 'foo<table><tbody><tr><td>ba^</td><td>az</td></tr></tbody></table>quoz' } + ] + }, + + { 'desc': 'forward-delete table rows', + 'tests': [ + { 'id': 'TR3-1_SO1', + 'desc': 'Delete first table row', + 'pad': '<table><tbody>{<tr><td>A</td></tr>}<tr><td>B</td></tr><tr><td>C</td></tr></tbody></table>', + 'expected': ['<table><tbody>|<tr><td>B</td></tr><tr><td>C</td></tr></tbody></table>', + '<table><tbody><tr><td>^B</td></tr><tr><td>C</td></tr></tbody></table>'] }, + + { 'id': 'TR3-1_SO2', + 'desc': 'Delete middle table row', + 'pad': '<table><tbody><tr><td>A</td></tr>{<tr><td>B</td></tr>}<tr><td>C</td></tr></tbody></table>', + 'expected': ['<table><tbody><tr><td>A</td></tr>|<tr><td>C</td></tr></tbody></table>', + '<table><tbody><tr><td>A</td></tr><tr><td>^C</td></tr></tbody></table>'] }, + + { 'id': 'TR3-1_SO3', + 'desc': 'Delete last table row', + 'pad': '<table><tbody><tr><td>A</td></tr><tr><td>B</td></tr>{<tr><td>C</td></tr>}</tbody></table>', + 'expected': ['<table><tbody><tr><td>A</td></tr><tr><td>B</td></tr>|</tbody></table>', + '<table><tbody><tr><td>A</td></tr><tr><td>B^</td></tr></tbody></table>'] }, + + { 'id': 'TR2rs:2-1_SO1', + 'desc': 'Delete first table row where a cell has rowspan 2', + 'pad': '<table><tbody>{<tr><td>A</td><td rowspan=2>R</td></tr>}<tr><td>B</td></tr></tbody></table>', + 'expected': ['<table><tbody>|<tr><td>B</td><td>R</td></tr></tbody></table>', + '<table><tbody><tr><td>^B</td><td>R</td></tr></tbody></table>'] }, + + { 'id': 'TR2rs:2-1_SO2', + 'desc': 'Delete second table row where a cell has rowspan 2', + 'pad': '<table><tbody><tr><td>A</td><td rowspan=2>R</td></tr>{<tr><td>B</td></tr>}</tbody></table>', + 'expected': ['<table><tbody><tr><td>A</td><td>R</td></tr>|</tbody></table>', + '<table><tbody><tr><td>A</td><td>R^</td></tr></tbody></table>'] }, + + { 'id': 'TR3rs:3-1_SO1', + 'desc': 'Delete first table row where a cell has rowspan 3', + 'pad': '<table><tbody>{<tr><td>A</td><td rowspan=3>R</td></tr>}<tr><td>B</td></tr><tr><td>C</td></tr></tbody></table>', + 'expected': ['<table><tbody>|<tr><td>A</td><td rowspan="2">R</td></tr><tr><td>C</td></tr></tbody></table>', + '<table><tbody><tr><td>^A</td><td rowspan="2">R</td></tr><tr><td>C</td></tr></tbody></table>'] }, + + { 'id': 'TR3rs:3-1_SO2', + 'desc': 'Delete middle table row where a cell has rowspan 3', + 'pad': '<table><tbody><tr><td>A</td><td rowspan=3>R</td></tr>{<tr><td>B</td></tr>}<tr><td>C</td></tr></tbody></table>', + 'expected': ['<table><tbody><tr><td>B</td><td rowspan="2">R</td></tr>|<tr><td>C</td></tr></tbody></table>', + '<table><tbody><tr><td>B</td><td rowspan="2">R</td></tr><tr><td>^C</td></tr></tbody></table>'] }, + + { 'id': 'TR3rs:3-1_SO3', + 'desc': 'Delete last table row where a cell has rowspan 3', + 'pad': '<table><tbody><tr><td>A</td><td rowspan=3>R</td></tr><tr><td>B</td></tr>{<tr><td>C</td></tr>}</tbody></table>', + 'expected': ['<table><tbody><tr><td>A</td><td rowspan="2">R</td></tr><tr><td>B</td></tr>|</tbody></table>', + '<table><tbody><tr><td>A</td><td rowspan="2">R</td></tr><tr><td>B^</td></tr></tbody></table>'] } + ] + }, + + { 'desc': 'delete with non-editable nested content', + 'tests': [ + { 'id': 'DIV:ce:false-1_SO', + 'desc': 'Delete nested non-editable <div>', + 'pad': 'foo[bar<div contenteditable="false">NESTED</div>baz]qoz', + 'expected': 'foo^qoz' }, + + { 'id': 'DIV:ce:false-1_SB', + 'desc': 'Delete from immediately before a nested non-editable <div>', + 'pad': 'foobar^<div contenteditable="false">NESTED</div>bazqoz', + 'expected': 'foobar^bazqoz' }, + + { 'id': 'DIV:ce:false-1_SL', + 'desc': 'Delete nested non-editable <div> with oblique selection', + 'pad': 'foo[bar<div contenteditable="false">NES]TED</div>bazqoz', + 'expected': [ 'foo^<div contenteditable="false">NESTED</div>bazqoz', + 'foo<div contenteditable="false">[NES]TED</div>bazqoz' ] }, + + { 'id': 'DIV:ce:false-1_SR', + 'desc': 'Delete nested non-editable <div> with oblique selection', + 'pad': 'foobar<div contenteditable="false">NES[TED</div>baz]qoz', + 'expected': [ 'foobar<div contenteditable="false">NESTED</div>^qoz', + 'foobar<div contenteditable="false">NES[TED]</div>qoz' ] }, + + { 'id': 'DIV:ce:false-1_SI', + 'desc': 'Delete inside nested non-editable <div> (should be no-op)', + 'pad': 'foobar<div contenteditable="false">NE[ST]ED</div>bazqoz', + 'expected': 'foobar<div contenteditable="false">NE[ST]ED</div>bazqoz' } + ] + }, + + { 'desc': 'Delete with display:inline-block', + 'checkStyle': True, + 'tests': [ + { 'id': 'SPAN:d:ib-1_SC', + 'desc': 'Delete inside an inline-block <span>', + 'pad': 'foo<span style="display: inline-block">bar^baz</span>qoz', + 'expected': 'foo<span style="display: inline-block">bar^az</span>qoz' }, + + { 'id': 'SPAN:d:ib-1_SA', + 'desc': 'Delete from immediately before an inline-block <span>', + 'pad': 'foo^<span style="display: inline-block">barbaz</span>qoz', + 'expected': 'foo^<span style="display: inline-block">arbaz</span>qoz' }, + + { 'id': 'SPAN:d:ib-2_SL', + 'desc': 'Delete with nested inline-block <span>, oblique selection', + 'pad': 'foo[DEL<span style="display: inline-block">ETE]bar</span>baz', + 'expected': 'foo^<span style="display: inline-block">bar</span>baz' }, + + { 'id': 'SPAN:d:ib-3_SR', + 'desc': 'Delete with nested inline-block <span>, oblique selection', + 'pad': 'foo<span style="display: inline-block">bar[DEL</span>ETE]baz', + 'expected': 'foo<span style="display: inline-block">bar^</span>baz' }, + + { 'id': 'SPAN:d:ib-4i_SI', + 'desc': 'Delete with nested inline-block <span>, oblique selection', + 'pad': 'foo<span style="display: inline-block">bar[DELETE]baz</span>qoz', + 'expected': 'foo<span style="display: inline-block">bar^baz</span>qoz' }, + + { 'id': 'SPAN:d:ib-4l_SI', + 'desc': 'Delete with nested inline-block <span>, oblique selection', + 'pad': 'foo<span style="display: inline-block">[DELETE]barbaz</span>qoz', + 'expected': 'foo<span style="display: inline-block">^barbaz</span>qoz' }, + + { 'id': 'SPAN:d:ib-4r_SI', + 'desc': 'Delete with nested inline-block <span>, oblique selection', + 'pad': 'foo<span style="display: inline-block">barbaz[DELETE]</span>qoz', + 'expected': 'foo<span style="display: inline-block">barbaz^</span>qoz' } + ] + } + ] +} + + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/insert.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/insert.py new file mode 100644 index 0000000000..a2e79c27c8 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/insert.py @@ -0,0 +1,285 @@ + +INSERT_TESTS = { + 'id': 'I', + 'caption': 'Insert Tests', + 'checkAttrs': False, + 'checkStyle': False, + + 'Proposed': [ + { 'desc': '', + 'command': '', + 'tests': [ + ] + }, + + { 'desc': 'insert <hr>', + 'command': 'inserthorizontalrule', + 'tests': [ + { 'id': 'IHR_TEXT-1_SC', + 'rte1-id': 'a-inserthorizontalrule-0', + 'desc': 'Insert <hr> into text', + 'pad': 'foo^bar', + 'expected': 'foo<hr>^bar', + 'accept': 'foo<hr>|bar' }, + + { 'id': 'IHR_TEXT-1_SI', + 'desc': 'Insert <hr>, replacing selected text', + 'pad': 'foo[bar]baz', + 'expected': 'foo<hr>^baz', + 'accept': 'foo<hr>|baz' }, + + { 'id': 'IHR_DIV-B-1_SX', + 'desc': 'Insert <hr> between elements', + 'pad': '<div><b>foo</b>|<b>bar</b></div>', + 'expected': '<div><b>foo</b><hr>|<b>bar</b></div>' }, + + { 'id': 'IHR_DIV-B-2_SO', + 'desc': 'Insert <hr>, replacing a fully wrapped element', + 'pad': '<div><b>foo</b>{<b>bar</b>}<b>baz</b></div>', + 'expected': '<div><b>foo</b><hr>|<b>baz</b></div>' }, + + { 'id': 'IHR_B-1_SC', + 'desc': 'Insert <hr> into a span, splitting it', + 'pad': '<b>foo^bar</b>', + 'expected': '<b>foo</b><hr><b>^bar</b>' }, + + { 'id': 'IHR_B-1_SS', + 'desc': 'Insert <hr> into a span at the start (should not create an empty span)', + 'pad': '<b>^foobar</b>', + 'expected': '<hr><b>^foobar</b>' }, + + { 'id': 'IHR_B-1_SE', + 'desc': 'Insert <hr> into a span at the end', + 'pad': '<b>foobar^</b>', + 'expected': [ '<b>foobar</b><hr>|', + '<b>foobar</b><hr><b>^</b>' ] }, + + { 'id': 'IHR_B-2_SL', + 'desc': 'Insert <hr> with oblique selection starting outside of span', + 'pad': 'foo[bar<b>baz]qoz</b>', + 'expected': 'foo<hr>|<b>qoz</b>' }, + + { 'id': 'IHR_B-2_SLR', + 'desc': 'Insert <hr> with oblique reversed selection starting outside of span', + 'pad': 'foo]bar<b>baz[qoz</b>', + 'expected': [ 'foo<hr>|<b>qoz</b>', + 'foo<hr><b>^qoz</b>' ] }, + + { 'id': 'IHR_B-3_SR', + 'desc': 'Insert <hr> with oblique selection ending outside of span', + 'pad': '<b>foo[bar</b>baz]quoz', + 'expected': [ '<b>foo</b><hr>|quoz', + '<b>foo</b><hr><b>^</b>quoz' ] }, + + { 'id': 'IHR_B-3_SRR', + 'desc': 'Insert <hr> with oblique reversed selection starting outside of span', + 'pad': '<b>foo]bar</b>baz[quoz', + 'expected': '<b>foo</b><hr>|quoz' }, + + { 'id': 'IHR_B-I-1_SM', + 'desc': 'Insert <hr> with oblique selection between different spans', + 'pad': '<b>foo[bar</b><i>baz]quoz</i>', + 'expected': [ '<b>foo</b><hr>|<i>quoz</i>', + '<b>foo</b><hr><b>^</b><i>quoz</i>' ] }, + + { 'id': 'IHR_B-I-1_SMR', + 'desc': 'Insert <hr> with reversed oblique selection between different spans', + 'pad': '<b>foo]bar</b><i>baz[quoz</i>', + 'expected': '<b>foo</b><hr><i>^quoz</i>' }, + + { 'id': 'IHR_P-1_SC', + 'desc': 'Insert <hr> into a paragraph, splitting it', + 'pad': '<p>foo^bar</p>', + 'expected': [ '<p>foo</p><hr>|<p>bar</p>', + '<p>foo</p><hr><p>^bar</p>' ] }, + + { 'id': 'IHR_P-1_SS', + 'desc': 'Insert <hr> into a paragraph at the start (should not create an empty span)', + 'pad': '<p>^foobar</p>', + 'expected': [ '<hr>|<p>foobar</p>', + '<hr><p>^foobar</p>' ] }, + + { 'id': 'IHR_P-1_SE', + 'desc': 'Insert <hr> into a paragraph at the end (should not create an empty span)', + 'pad': '<p>foobar^</p>', + 'expected': '<p>foobar</p><hr>|' } + ] + }, + + { 'desc': 'insert <p>', + 'command': 'insertparagraph', + 'tests': [ + { 'id': 'IP_P-1_SC', + 'desc': 'Split paragraph', + 'pad': '<p>foo^bar</p>', + 'expected': '<p>foo</p><p>^bar</p>' }, + + { 'id': 'IP_UL-LI-1_SC', + 'desc': 'Split list item', + 'pad': '<ul><li>foo^bar</li></ul>', + 'expected': '<ul><li>foo</li><li>^bar</li></ul>' } + ] + }, + + { 'desc': 'insert text', + 'command': 'inserttext', + 'tests': [ + { 'id': 'ITEXT:text_TEXT-1_SC', + 'desc': 'Insert text', + 'value': 'text', + 'pad': 'foo^bar', + 'expected': 'footext^bar' }, + + { 'id': 'ITEXT:text_TEXT-1_SI', + 'desc': 'Insert text, replacing selected text', + 'value': 'text', + 'pad': 'foo[bar]baz', + 'expected': 'footext^baz' } + ] + }, + + { 'desc': 'insert <br>', + 'command': 'insertlinebreak', + 'tests': [ + { 'id': 'IBR_TEXT-1_SC', + 'desc': 'Insert <br> into text', + 'pad': 'foo^bar', + 'expected': [ 'foo<br>|bar', + 'foo<br>^bar' ] }, + + { 'id': 'IBR_TEXT-1_SI', + 'desc': 'Insert <br>, replacing selected text', + 'pad': 'foo[bar]baz', + 'expected': [ 'foo<br>|baz', + 'foo<br>^baz' ] }, + + { 'id': 'IBR_LI-1_SC', + 'desc': 'Insert <br> within list item', + 'pad': '<ul><li>foo^bar</li></ul>', + 'expected': '<ul><li>foo<br>^bar</li></ul>' } + ] + }, + + { 'desc': 'insert <img>', + 'command': 'insertimage', + 'tests': [ + { 'id': 'IIMG:url_TEXT-1_SC', + 'rte1-id': 'a-insertimage-0', + 'desc': 'Insert image with URL "bar.png"', + 'value': 'bar.png', + 'checkAttrs': True, + 'pad': 'foo^bar', + 'expected': [ 'foo<img src="bar.png">|bar', + 'foo<img src="bar.png">^bar' ] }, + + { 'id': 'IIMG:url_IMG-1_SO', + 'desc': 'Change existing image to new URL, selection on <img>', + 'value': 'quz.png', + 'checkAttrs': True, + 'pad': '<span>foo{<img src="bar.png">}bar</span>', + 'expected': [ '<span>foo<img src="quz.png"/>|bar</span>', + '<span>foo<img src="quz.png"/>^bar</span>' ] }, + + { 'id': 'IIMG:url_SPAN-IMG-1_SO', + 'desc': 'Change existing image to new URL, selection in text surrounding <img>', + 'value': 'quz.png', + 'checkAttrs': True, + 'pad': 'foo[<img src="bar.png">]bar', + 'expected': [ 'foo<img src="quz.png"/>|bar', + 'foo<img src="quz.png"/>^bar' ] }, + + { 'id': 'IIMG:._SPAN-IMG-1_SO', + 'desc': 'Remove existing image or URL, selection on <img>', + 'value': '', + 'checkAttrs': True, + 'pad': '<span>foo{<img src="bar.png">}bar</span>', + 'expected': [ '<span>foo^bar</span>', + '<span>foo<img>|bar</span>', + '<span>foo<img>^bar</span>', + '<span>foo<img src="">|bar</span>', + '<span>foo<img src="">^bar</span>' ] }, + + { 'id': 'IIMG:._IMG-1_SO', + 'desc': 'Remove existing image or URL, selection in text surrounding <img>', + 'value': '', + 'checkAttrs': True, + 'pad': 'foo[<img src="bar.png">]bar', + 'expected': [ 'foo^bar', + 'foo<img>|bar', + 'foo<img>^bar', + 'foo<img src="">|bar', + 'foo<img src="">^bar' ] } + ] + }, + + { 'desc': 'insert <ol>', + 'command': 'insertorderedlist', + 'tests': [ + { 'id': 'IOL_TEXT-1_SC', + 'rte1-id': 'a-insertorderedlist-0', + 'desc': 'Insert ordered list on collapsed selection', + 'pad': 'foo^bar', + 'expected': '<ol><li>foo^bar</li></ol>' }, + + { 'id': 'IOL_TEXT-1_SI', + 'desc': 'Insert ordered list on selected text', + 'pad': 'foo[bar]baz', + 'expected': '<ol><li>foo[bar]baz</li></ol>' } + ] + }, + + { 'desc': 'insert <ul>', + 'command': 'insertunorderedlist', + 'tests': [ + { 'id': 'IUL_TEXT-1_SC', + 'desc': 'Insert unordered list on collapsed selection', + 'pad': 'foo^bar', + 'expected': '<ul><li>foo^bar</li></ul>' }, + + { 'id': 'IUL_TEXT-1_SI', + 'rte1-id': 'a-insertunorderedlist-0', + 'desc': 'Insert unordered list on selected text', + 'pad': 'foo[bar]baz', + 'expected': '<ul><li>foo[bar]baz</li></ul>' } + ] + }, + + { 'desc': 'insert arbitrary HTML', + 'command': 'inserthtml', + 'tests': [ + { 'id': 'IHTML:BR_TEXT-1_SC', + 'rte1-id': 'a-inserthtml-0', + 'desc': 'InsertHTML: <br>', + 'value': '<br>', + 'pad': 'foo^barbaz', + 'expected': 'foo<br>^barbaz' }, + + { 'id': 'IHTML:text_TEXT-1_SI', + 'desc': 'InsertHTML: "NEW"', + 'value': 'NEW', + 'pad': 'foo[bar]baz', + 'expected': 'fooNEW^baz' }, + + { 'id': 'IHTML:S_TEXT-1_SI', + 'desc': 'InsertHTML: "<span>NEW<span>"', + 'value': '<span>NEW</span>', + 'pad': 'foo[bar]baz', + 'expected': 'foo<span>NEW</span>^baz' }, + + { 'id': 'IHTML:H1.H2_TEXT-1_SI', + 'desc': 'InsertHTML: "<h1>NEW</h1><h2>HTML</h2>"', + 'value': '<h1>NEW</h1><h2>HTML</h2>', + 'pad': 'foo[bar]baz', + 'expected': 'foo<h1>NEW</h1><h2>HTML</h2>^baz' }, + + { 'id': 'IHTML:P-B_TEXT-1_SI', + 'desc': 'InsertHTML: "<p>NEW<b>HTML</b>!</p>"', + 'value': '<p>NEW<b>HTML</b>!</p>', + 'pad': 'foo[bar]baz', + 'expected': 'foo<p>NEW<b>HTML</b>!</p>^baz' } + ] + } + ] +} + + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryEnabled.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryEnabled.py new file mode 100644 index 0000000000..eb721923b6 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryEnabled.py @@ -0,0 +1,215 @@ + +QUERYENABLED_TESTS = { + 'id': 'QE', + 'caption': 'queryCommandEnabled Tests', + 'pad': 'foo[bar]baz', + 'checkAttrs': False, + 'checkStyle': False, + 'styleWithCSS': False, + 'expected': True, + + 'Proposed': [ + { 'desc': '', + 'tests': [ + ] + }, + + { 'desc': 'HTML5 commands', + 'tests': [ + { 'id': 'SELECTALL_TEXT-1', + 'desc': 'check whether the "selectall" command is enabled', + 'qcenabled': 'selectall' }, + + { 'id': 'UNSELECT_TEXT-1', + 'desc': 'check whether the "unselect" command is enabled', + 'qcenabled': 'unselect' }, + + { 'id': 'UNDO_TEXT-1', + 'desc': 'check whether the "undo" command is enabled', + 'qcenabled': 'undo' }, + + { 'id': 'REDO_TEXT-1', + 'desc': 'check whether the "redo" command is enabled', + 'qcenabled': 'redo' }, + + { 'id': 'BOLD_TEXT-1', + 'desc': 'check whether the "bold" command is enabled', + 'qcenabled': 'bold' }, + + { 'id': 'ITALIC_TEXT-1', + 'desc': 'check whether the "italic" command is enabled', + 'qcenabled': 'italic' }, + + { 'id': 'UNDERLINE_TEXT-1', + 'desc': 'check whether the "underline" command is enabled', + 'qcenabled': 'underline' }, + + { 'id': 'STRIKETHROUGH_TEXT-1', + 'desc': 'check whether the "strikethrough" command is enabled', + 'qcenabled': 'strikethrough' }, + + { 'id': 'SUBSCRIPT_TEXT-1', + 'desc': 'check whether the "subscript" command is enabled', + 'qcenabled': 'subscript' }, + + { 'id': 'SUPERSCRIPT_TEXT-1', + 'desc': 'check whether the "superscript" command is enabled', + 'qcenabled': 'superscript' }, + + { 'id': 'FORMATBLOCK_TEXT-1', + 'desc': 'check whether the "formatblock" command is enabled', + 'qcenabled': 'formatblock' }, + + { 'id': 'CREATELINK_TEXT-1', + 'desc': 'check whether the "createlink" command is enabled', + 'qcenabled': 'createlink' }, + + { 'id': 'UNLINK_TEXT-1', + 'desc': 'check whether the "unlink" command is enabled', + 'qcenabled': 'unlink' }, + + { 'id': 'INSERTHTML_TEXT-1', + 'desc': 'check whether the "inserthtml" command is enabled', + 'qcenabled': 'inserthtml' }, + + { 'id': 'INSERTHORIZONTALRULE_TEXT-1', + 'desc': 'check whether the "inserthorizontalrule" command is enabled', + 'qcenabled': 'inserthorizontalrule' }, + + { 'id': 'INSERTIMAGE_TEXT-1', + 'desc': 'check whether the "insertimage" command is enabled', + 'qcenabled': 'insertimage' }, + + { 'id': 'INSERTLINEBREAK_TEXT-1', + 'desc': 'check whether the "insertlinebreak" command is enabled', + 'qcenabled': 'insertlinebreak' }, + + { 'id': 'INSERTPARAGRAPH_TEXT-1', + 'desc': 'check whether the "insertparagraph" command is enabled', + 'qcenabled': 'insertparagraph' }, + + { 'id': 'INSERTORDEREDLIST_TEXT-1', + 'desc': 'check whether the "insertorderedlist" command is enabled', + 'qcenabled': 'insertorderedlist' }, + + { 'id': 'INSERTUNORDEREDLIST_TEXT-1', + 'desc': 'check whether the "insertunorderedlist" command is enabled', + 'qcenabled': 'insertunorderedlist' }, + + { 'id': 'INSERTTEXT_TEXT-1', + 'desc': 'check whether the "inserttext" command is enabled', + 'qcenabled': 'inserttext' }, + + { 'id': 'DELETE_TEXT-1', + 'desc': 'check whether the "delete" command is enabled', + 'qcenabled': 'delete' }, + + { 'id': 'FORWARDDELETE_TEXT-1', + 'desc': 'check whether the "forwarddelete" command is enabled', + 'qcenabled': 'forwarddelete' } + ] + }, + + { 'desc': 'MIDAS commands', + 'tests': [ + { 'id': 'STYLEWITHCSS_TEXT-1', + 'desc': 'check whether the "styleWithCSS" command is enabled', + 'qcenabled': 'styleWithCSS' }, + + { 'id': 'CONTENTREADONLY_TEXT-1', + 'desc': 'check whether the "contentreadonly" command is enabled', + 'qcenabled': 'contentreadonly' }, + + { 'id': 'BACKCOLOR_TEXT-1', + 'desc': 'check whether the "backcolor" command is enabled', + 'qcenabled': 'backcolor' }, + + { 'id': 'FORECOLOR_TEXT-1', + 'desc': 'check whether the "forecolor" command is enabled', + 'qcenabled': 'forecolor' }, + + { 'id': 'HILITECOLOR_TEXT-1', + 'desc': 'check whether the "hilitecolor" command is enabled', + 'qcenabled': 'hilitecolor' }, + + { 'id': 'FONTNAME_TEXT-1', + 'desc': 'check whether the "fontname" command is enabled', + 'qcenabled': 'fontname' }, + + { 'id': 'FONTSIZE_TEXT-1', + 'desc': 'check whether the "fontsize" command is enabled', + 'qcenabled': 'fontsize' }, + + { 'id': 'INCREASEFONTSIZE_TEXT-1', + 'desc': 'check whether the "increasefontsize" command is enabled', + 'qcenabled': 'increasefontsize' }, + + { 'id': 'DECREASEFONTSIZE_TEXT-1', + 'desc': 'check whether the "decreasefontsize" command is enabled', + 'qcenabled': 'decreasefontsize' }, + + { 'id': 'HEADING_TEXT-1', + 'desc': 'check whether the "heading" command is enabled', + 'qcenabled': 'heading' }, + + { 'id': 'INDENT_TEXT-1', + 'desc': 'check whether the "indent" command is enabled', + 'qcenabled': 'indent' }, + + { 'id': 'OUTDENT_TEXT-1', + 'desc': 'check whether the "outdent" command is enabled', + 'qcenabled': 'outdent' }, + + { 'id': 'CREATEBOOKMARK_TEXT-1', + 'desc': 'check whether the "createbookmark" command is enabled', + 'qcenabled': 'createbookmark' }, + + { 'id': 'UNBOOKMARK_TEXT-1', + 'desc': 'check whether the "unbookmark" command is enabled', + 'qcenabled': 'unbookmark' }, + + { 'id': 'JUSTIFYCENTER_TEXT-1', + 'desc': 'check whether the "justifycenter" command is enabled', + 'qcenabled': 'justifycenter' }, + + { 'id': 'JUSTIFYFULL_TEXT-1', + 'desc': 'check whether the "justifyfull" command is enabled', + 'qcenabled': 'justifyfull' }, + + { 'id': 'JUSTIFYLEFT_TEXT-1', + 'desc': 'check whether the "justifyleft" command is enabled', + 'qcenabled': 'justifyleft' }, + + { 'id': 'JUSTIFYRIGHT_TEXT-1', + 'desc': 'check whether the "justifyright" command is enabled', + 'qcenabled': 'justifyright' }, + + { 'id': 'REMOVEFORMAT_TEXT-1', + 'desc': 'check whether the "removeformat" command is enabled', + 'qcenabled': 'removeformat' }, + + { 'id': 'COPY_TEXT-1', + 'desc': 'check whether the "copy" command is enabled', + 'qcenabled': 'copy' }, + + { 'id': 'CUT_TEXT-1', + 'desc': 'check whether the "cut" command is enabled', + 'qcenabled': 'cut' }, + + { 'id': 'PASTE_TEXT-1', + 'desc': 'check whether the "paste" command is enabled', + 'qcenabled': 'paste' } + ] + }, + + { 'desc': 'Other tests', + 'tests': [ + { 'id': 'garbage-1_TEXT-1', + 'desc': 'check correct return value with garbage input', + 'qcenabled': '#!#@7', + 'expected': False } + ] + } + ] +} + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryIndeterm.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryIndeterm.py new file mode 100644 index 0000000000..d1ad8debdb --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryIndeterm.py @@ -0,0 +1,214 @@ + +QUERYINDETERM_TESTS = { + 'id': 'QI', + 'caption': 'queryCommandIndeterm Tests', + 'pad': 'foo[bar]baz', + 'checkAttrs': False, + 'checkStyle': False, + 'styleWithCSS': False, + 'expected': False, + + 'Proposed': [ + { 'desc': '', + 'tests': [ + ] + }, + + { 'desc': 'HTML5 commands', + 'tests': [ + { 'id': 'SELECTALL_TEXT-1', + 'desc': 'check whether the "selectall" command is indeterminate', + 'qcindeterm': 'selectall' }, + + { 'id': 'UNSELECT_TEXT-1', + 'desc': 'check whether the "unselect" command is indeterminate', + 'qcindeterm': 'unselect' }, + + { 'id': 'UNDO_TEXT-1', + 'desc': 'check whether the "undo" command is indeterminate', + 'qcindeterm': 'undo' }, + + { 'id': 'REDO_TEXT-1', + 'desc': 'check whether the "redo" command is indeterminate', + 'qcindeterm': 'redo' }, + + { 'id': 'BOLD_TEXT-1', + 'desc': 'check whether the "bold" command is indeterminate', + 'qcindeterm': 'bold' }, + + { 'id': 'ITALIC_TEXT-1', + 'desc': 'check whether the "italic" command is indeterminate', + 'qcindeterm': 'italic' }, + + { 'id': 'UNDERLINE_TEXT-1', + 'desc': 'check whether the "underline" command is indeterminate', + 'qcindeterm': 'underline' }, + + { 'id': 'STRIKETHROUGH_TEXT-1', + 'desc': 'check whether the "strikethrough" command is indeterminate', + 'qcindeterm': 'strikethrough' }, + + { 'id': 'SUBSCRIPT_TEXT-1', + 'desc': 'check whether the "subscript" command is indeterminate', + 'qcindeterm': 'subscript' }, + + { 'id': 'SUPERSCRIPT_TEXT-1', + 'desc': 'check whether the "superscript" command is indeterminate', + 'qcindeterm': 'superscript' }, + + { 'id': 'FORMATBLOCK_TEXT-1', + 'desc': 'check whether the "formatblock" command is indeterminate', + 'qcindeterm': 'formatblock' }, + + { 'id': 'CREATELINK_TEXT-1', + 'desc': 'check whether the "createlink" command is indeterminate', + 'qcindeterm': 'createlink' }, + + { 'id': 'UNLINK_TEXT-1', + 'desc': 'check whether the "unlink" command is indeterminate', + 'qcindeterm': 'unlink' }, + + { 'id': 'INSERTHTML_TEXT-1', + 'desc': 'check whether the "inserthtml" command is indeterminate', + 'qcindeterm': 'inserthtml' }, + + { 'id': 'INSERTHORIZONTALRULE_TEXT-1', + 'desc': 'check whether the "inserthorizontalrule" command is indeterminate', + 'qcindeterm': 'inserthorizontalrule' }, + + { 'id': 'INSERTIMAGE_TEXT-1', + 'desc': 'check whether the "insertimage" command is indeterminate', + 'qcindeterm': 'insertimage' }, + + { 'id': 'INSERTLINEBREAK_TEXT-1', + 'desc': 'check whether the "insertlinebreak" command is indeterminate', + 'qcindeterm': 'insertlinebreak' }, + + { 'id': 'INSERTPARAGRAPH_TEXT-1', + 'desc': 'check whether the "insertparagraph" command is indeterminate', + 'qcindeterm': 'insertparagraph' }, + + { 'id': 'INSERTORDEREDLIST_TEXT-1', + 'desc': 'check whether the "insertorderedlist" command is indeterminate', + 'qcindeterm': 'insertorderedlist' }, + + { 'id': 'INSERTUNORDEREDLIST_TEXT-1', + 'desc': 'check whether the "insertunorderedlist" command is indeterminate', + 'qcindeterm': 'insertunorderedlist' }, + + { 'id': 'INSERTTEXT_TEXT-1', + 'desc': 'check whether the "inserttext" command is indeterminate', + 'qcindeterm': 'inserttext' }, + + { 'id': 'DELETE_TEXT-1', + 'desc': 'check whether the "delete" command is indeterminate', + 'qcindeterm': 'delete' }, + + { 'id': 'FORWARDDELETE_TEXT-1', + 'desc': 'check whether the "forwarddelete" command is indeterminate', + 'qcindeterm': 'forwarddelete' } + ] + }, + + { 'desc': 'MIDAS commands', + 'tests': [ + { 'id': 'STYLEWITHCSS_TEXT-1', + 'desc': 'check whether the "styleWithCSS" command is indeterminate', + 'qcindeterm': 'styleWithCSS' }, + + { 'id': 'CONTENTREADONLY_TEXT-1', + 'desc': 'check whether the "contentreadonly" command is indeterminate', + 'qcindeterm': 'contentreadonly' }, + + { 'id': 'BACKCOLOR_TEXT-1', + 'desc': 'check whether the "backcolor" command is indeterminate', + 'qcindeterm': 'backcolor' }, + + { 'id': 'FORECOLOR_TEXT-1', + 'desc': 'check whether the "forecolor" command is indeterminate', + 'qcindeterm': 'forecolor' }, + + { 'id': 'HILITECOLOR_TEXT-1', + 'desc': 'check whether the "hilitecolor" command is indeterminate', + 'qcindeterm': 'hilitecolor' }, + + { 'id': 'FONTNAME_TEXT-1', + 'desc': 'check whether the "fontname" command is indeterminate', + 'qcindeterm': 'fontname' }, + + { 'id': 'FONTSIZE_TEXT-1', + 'desc': 'check whether the "fontsize" command is indeterminate', + 'qcindeterm': 'fontsize' }, + + { 'id': 'INCREASEFONTSIZE_TEXT-1', + 'desc': 'check whether the "increasefontsize" command is indeterminate', + 'qcindeterm': 'increasefontsize' }, + + { 'id': 'DECREASEFONTSIZE_TEXT-1', + 'desc': 'check whether the "decreasefontsize" command is indeterminate', + 'qcindeterm': 'decreasefontsize' }, + + { 'id': 'HEADING_TEXT-1', + 'desc': 'check whether the "heading" command is indeterminate', + 'qcindeterm': 'heading' }, + + { 'id': 'INDENT_TEXT-1', + 'desc': 'check whether the "indent" command is indeterminate', + 'qcindeterm': 'indent' }, + + { 'id': 'OUTDENT_TEXT-1', + 'desc': 'check whether the "outdent" command is indeterminate', + 'qcindeterm': 'outdent' }, + + { 'id': 'CREATEBOOKMARK_TEXT-1', + 'desc': 'check whether the "createbookmark" command is indeterminate', + 'qcindeterm': 'createbookmark' }, + + { 'id': 'UNBOOKMARK_TEXT-1', + 'desc': 'check whether the "unbookmark" command is indeterminate', + 'qcindeterm': 'unbookmark' }, + + { 'id': 'JUSTIFYCENTER_TEXT-1', + 'desc': 'check whether the "justifycenter" command is indeterminate', + 'qcindeterm': 'justifycenter' }, + + { 'id': 'JUSTIFYFULL_TEXT-1', + 'desc': 'check whether the "justifyfull" command is indeterminate', + 'qcindeterm': 'justifyfull' }, + + { 'id': 'JUSTIFYLEFT_TEXT-1', + 'desc': 'check whether the "justifyleft" command is indeterminate', + 'qcindeterm': 'justifyleft' }, + + { 'id': 'JUSTIFYRIGHT_TEXT-1', + 'desc': 'check whether the "justifyright" command is indeterminate', + 'qcindeterm': 'justifyright' }, + + { 'id': 'REMOVEFORMAT_TEXT-1', + 'desc': 'check whether the "removeformat" command is indeterminate', + 'qcindeterm': 'removeformat' }, + + { 'id': 'COPY_TEXT-1', + 'desc': 'check whether the "copy" command is indeterminate', + 'qcindeterm': 'copy' }, + + { 'id': 'CUT_TEXT-1', + 'desc': 'check whether the "cut" command is indeterminate', + 'qcindeterm': 'cut' }, + + { 'id': 'PASTE_TEXT-1', + 'desc': 'check whether the "paste" command is indeterminate', + 'qcindeterm': 'paste' } + ] + }, + + { 'desc': 'Other tests', + 'tests': [ + { 'id': 'garbage-1_TEXT-1', + 'desc': 'check correct return value with garbage input', + 'qcindeterm': '#!#@7' } + ] + } + ] +} + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryState.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryState.py new file mode 100644 index 0000000000..297559d625 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryState.py @@ -0,0 +1,575 @@ + +QUERYSTATE_TESTS = { + 'id': 'QS', + 'caption': 'queryCommandState Tests', + 'checkAttrs': False, + 'checkStyle': False, + 'styleWithCSS': False, + + 'Proposed': [ + { 'desc': '', + 'qcstate': '', + 'tests': [ + ] + }, + + { 'desc': 'query bold state', + 'qcstate': 'bold', + 'tests': [ + { 'id': 'B_TEXT_SI', + 'rte1-id': 'q-bold-0', + 'desc': 'query the "bold" state', + 'pad': 'foo[bar]baz', + 'expected': False }, + + { 'id': 'B_B-1_SI', + 'rte1-id': 'q-bold-1', + 'desc': 'query the "bold" state', + 'pad': '<b>foo[bar]baz</b>', + 'expected': True }, + + { 'id': 'B_STRONG-1_SI', + 'rte1-id': 'q-bold-2', + 'desc': 'query the "bold" state', + 'pad': '<strong>foo[bar]baz</strong>', + 'expected': True }, + + { 'id': 'B_SPANs:fw:b-1_SI', + 'rte1-id': 'q-bold-3', + 'desc': 'query the "bold" state', + 'pad': '<span style="font-weight: bold">foo[bar]baz</span>', + 'expected': True }, + + { 'id': 'B_SPANs:fw:n-1_SI', + 'desc': 'query the "bold" state', + 'pad': '<span style="font-weight: normal">foo[bar]baz</span>', + 'expected': False }, + + { 'id': 'B_Bs:fw:n-1_SI', + 'rte1-id': 'q-bold-4', + 'desc': 'query the "bold" state', + 'pad': '<span style="font-weight: normal">foo[bar]baz</span>', + 'expected': False }, + + { 'id': 'B_B-SPANs:fw:n-1_SI', + 'rte1-id': 'q-bold-5', + 'desc': 'query the "bold" state', + 'pad': '<b><span style="font-weight: normal">foo[bar]baz</span></b>', + 'expected': False }, + + { 'id': 'B_SPAN.b-1-SI', + 'desc': 'query the "bold" state', + 'pad': '<span class="b">foo[bar]baz</span>', + 'expected': True }, + + { 'id': 'B_MYB-1-SI', + 'desc': 'query the "bold" state', + 'pad': '<myb>foo[bar]baz</myb>', + 'expected': True }, + + { 'id': 'B_B-I-1_SC', + 'desc': 'query the "bold" state, bold tag not immediate parent', + 'pad': '<b>foo<i>ba^r</i>baz</b>', + 'expected': True }, + + { 'id': 'B_B-I-1_SL', + 'desc': 'query the "bold" state, selection partially in child element', + 'pad': '<b>fo[o<i>b]ar</i>baz</b>', + 'expected': True }, + + { 'id': 'B_B-I-1_SR', + 'desc': 'query the "bold" state, selection partially in child element', + 'pad': '<b>foo<i>ba[r</i>b]az</b>', + 'expected': True }, + + { 'id': 'B_STRONG-I-1_SC', + 'desc': 'query the "bold" state, bold tag not immediate parent', + 'pad': '<strong>foo<i>ba^r</i>baz</strong>', + 'expected': True }, + + { 'id': 'B_B-I-U-1_SC', + 'desc': 'query the "bold" state, bold tag not immediate parent', + 'pad': '<b>foo<i>bar<u>b^az</u></i></strong>', + 'expected': True }, + + { 'id': 'B_B-I-U-1_SM', + 'desc': 'query the "bold" state, bold tag not immediate parent', + 'pad': '<b>foo<i>ba[r<u>b]az</u></i></strong>', + 'expected': True }, + + { 'id': 'B_TEXT-B-1_SO-1', + 'desc': 'query the "bold" state, selection wrapping the bold tag', + 'pad': 'foo[<b>bar</b>]baz', + 'expected': True }, + + { 'id': 'B_TEXT-B-1_SO-2', + 'desc': 'query the "bold" state, selection wrapping the bold tag', + 'pad': 'foo{<b>bar</b>}baz', + 'expected': True }, + + { 'id': 'B_TEXT-B-1_SL', + 'desc': 'query the "bold" state, selection containing non-bold text', + 'pad': 'fo[o<b>ba]r</b>baz', + 'expected': False }, + + { 'id': 'B_TEXT-B-1_SR', + 'desc': 'query the "bold" state, selection containing non-bold text', + 'pad': 'foo<b>b[ar</b>b]az', + 'expected': False }, + + { 'id': 'B_TEXT-B-1_SO-3', + 'desc': 'query the "bold" state, selection containing non-bold text', + 'pad': 'fo[o<b>bar</b>b]az', + 'expected': False }, + + { 'id': 'B_B.TEXT.B-1_SM', + 'desc': 'query the "bold" state, selection including non-bold text', + 'pad': '<b>fo[o</b>bar<b>b]az</b>', + 'expected': False }, + + { 'id': 'B_B.B.B-1_SM', + 'desc': 'query the "bold" state, selection mixed, but all bold', + 'pad': '<b>fo[o</b><b>bar</b><b>b]az</b>', + 'expected': True }, + + { 'id': 'B_B.STRONG.B-1_SM', + 'desc': 'query the "bold" state, selection mixed, but all bold', + 'pad': '<b>fo[o</b><strong>bar</strong><b>b]az</b>', + 'expected': True }, + + { 'id': 'B_SPAN.b.MYB.SPANs:fw:b-1_SM', + 'desc': 'query the "bold" state, selection mixed, but all bold', + 'pad': '<span class="b">fo[o</span><myb>bar</myb><span style="font-weight: bold">b]az</span>', + 'expected': True } + ] + }, + + { 'desc': 'query italic state', + 'qcstate': 'italic', + 'tests': [ + { 'id': 'I_TEXT_SI', + 'rte1-id': 'q-italic-0', + 'desc': 'query the "italic" state', + 'pad': 'foo[bar]baz', + 'expected': False }, + + { 'id': 'I_I-1_SI', + 'rte1-id': 'q-italic-1', + 'desc': 'query the "italic" state', + 'pad': '<i>foo[bar]baz</i>', + 'expected': True }, + + { 'id': 'I_EM-1_SI', + 'rte1-id': 'q-italic-2', + 'desc': 'query the "italic" state', + 'pad': '<em>foo[bar]baz</em>', + 'expected': True }, + + { 'id': 'I_SPANs:fs:i-1_SI', + 'rte1-id': 'q-italic-3', + 'desc': 'query the "italic" state', + 'pad': '<span style="font-style: italic">foo[bar]baz</span>', + 'expected': True }, + + { 'id': 'I_SPANs:fs:n-1_SI', + 'desc': 'query the "italic" state', + 'pad': '<span style="font-style: normal">foo[bar]baz</span>', + 'expected': False }, + + { 'id': 'I_I-SPANs:fs:n-1_SI', + 'rte1-id': 'q-italic-4', + 'desc': 'query the "italic" state', + 'pad': '<i><span style="font-style: normal">foo[bar]baz</span></i>', + 'expected': False }, + + { 'id': 'I_SPAN.i-1-SI', + 'desc': 'query the "italic" state', + 'pad': '<span class="i">foo[bar]baz</span>', + 'expected': True }, + + { 'id': 'I_MYI-1-SI', + 'desc': 'query the "italic" state', + 'pad': '<myi>foo[bar]baz</myi>', + 'expected': True } + ] + }, + + { 'desc': 'query underline state', + 'qcstate': 'underline', + 'tests': [ + { 'id': 'U_TEXT_SI', + 'rte1-id': 'q-underline-0', + 'desc': 'query the "underline" state', + 'pad': 'foo[bar]baz', + 'expected': False }, + + { 'id': 'U_U-1_SI', + 'rte1-id': 'q-underline-1', + 'desc': 'query the "underline" state', + 'pad': '<u>foo[bar]baz</u>', + 'expected': True }, + + { 'id': 'U_Us:td:n-1_SI', + 'rte1-id': 'q-underline-4', + 'desc': 'query the "underline" state', + 'pad': '<u style="text-decoration: none">foo[bar]baz</u>', + 'expected': False }, + + { 'id': 'U_Ah:url-1_SI', + 'rte1-id': 'q-underline-2', + 'desc': 'query the "underline" state', + 'pad': '<a href="http://www.goo.gl">foo[bar]baz</a>', + 'expected': True }, + + { 'id': 'U_Ah:url.s:td:n-1_SI', + 'rte1-id': 'q-underline-5', + 'desc': 'query the "underline" state', + 'pad': '<a href="http://www.goo.gl" style="text-decoration: none">foo[bar]baz</a>', + 'expected': False }, + + { 'id': 'U_SPANs:td:u-1_SI', + 'rte1-id': 'q-underline-3', + 'desc': 'query the "underline" state', + 'pad': '<span style="text-decoration: underline">foo[bar]baz</span>', + 'expected': True }, + + { 'id': 'U_SPAN.u-1-SI', + 'desc': 'query the "underline" state', + 'pad': '<span class="u">foo[bar]baz</span>', + 'expected': True }, + + { 'id': 'U_MYU-1-SI', + 'desc': 'query the "underline" state', + 'pad': '<myu>foo[bar]baz</myu>', + 'expected': True } + ] + }, + + { 'desc': 'query strike-through state', + 'qcstate': 'strikethrough', + 'tests': [ + { 'id': 'S_TEXT_SI', + 'rte1-id': 'q-strikethrough-0', + 'desc': 'query the "strikethrough" state', + 'pad': 'foo[bar]baz', + 'expected': False }, + + { 'id': 'S_S-1_SI', + 'rte1-id': 'q-strikethrough-3', + 'desc': 'query the "strikethrough" state', + 'pad': '<s>foo[bar]baz</s>', + 'expected': True }, + + { 'id': 'S_STRIKE-1_SI', + 'rte1-id': 'q-strikethrough-1', + 'desc': 'query the "strikethrough" state', + 'pad': '<strike>foo[bar]baz</strike>', + 'expected': True }, + + { 'id': 'S_STRIKEs:td:n-1_SI', + 'rte1-id': 'q-strikethrough-2', + 'desc': 'query the "strikethrough" state', + 'pad': '<strike style="text-decoration: none">foo[bar]baz</strike>', + 'expected': False }, + + { 'id': 'S_DEL-1_SI', + 'rte1-id': 'q-strikethrough-4', + 'desc': 'query the "strikethrough" state', + 'pad': '<del>foo[bar]baz</del>', + 'expected': True }, + + { 'id': 'S_SPANs:td:lt-1_SI', + 'rte1-id': 'q-strikethrough-5', + 'desc': 'query the "strikethrough" state', + 'pad': '<span style="text-decoration: line-through">foo[bar]baz</span>', + 'expected': True }, + + { 'id': 'S_SPAN.s-1-SI', + 'desc': 'query the "strikethrough" state', + 'pad': '<span class="s">foo[bar]baz</span>', + 'expected': True }, + + { 'id': 'S_MYS-1-SI', + 'desc': 'query the "strikethrough" state', + 'pad': '<mys>foo[bar]baz</mys>', + 'expected': True }, + + { 'id': 'S_S.STRIKE.DEL-1_SM', + 'desc': 'query the "strikethrough" state, selection mixed, but all struck', + 'pad': '<s>fo[o</s><strike>bar</strike><del>b]az</del>', + 'expected': True } + ] + }, + + { 'desc': 'query subscript state', + 'qcstate': 'subscript', + 'tests': [ + { 'id': 'SUB_TEXT_SI', + 'rte1-id': 'q-subscript-0', + 'desc': 'query the "subscript" state', + 'pad': 'foo[bar]baz', + 'expected': False }, + + { 'id': 'SUB_SUB-1_SI', + 'rte1-id': 'q-subscript-1', + 'desc': 'query the "subscript" state', + 'pad': '<sub>foo[bar]baz</sub>', + 'expected': True }, + + { 'id': 'SUB_SPAN.sub-1-SI', + 'desc': 'query the "subscript" state', + 'pad': '<span class="sub">foo[bar]baz</span>', + 'expected': True }, + + { 'id': 'SUB_MYSUB-1-SI', + 'desc': 'query the "subscript" state', + 'pad': '<mysub>foo[bar]baz</mysub>', + 'expected': True } + ] + }, + + { 'desc': 'query superscript state', + 'qcstate': 'superscript', + 'tests': [ + { 'id': 'SUP_TEXT_SI', + 'rte1-id': 'q-superscript-0', + 'desc': 'query the "superscript" state', + 'pad': 'foo[bar]baz', + 'expected': False }, + + { 'id': 'SUP_SUP-1_SI', + 'rte1-id': 'q-superscript-1', + 'desc': 'query the "superscript" state', + 'pad': '<sup>foo[bar]baz</sup>', + 'expected': True }, + + { 'id': 'IOL_TEXT_SI', + 'desc': 'query the "insertorderedlist" state', + 'pad': 'foo[bar]baz', + 'expected': False }, + + { 'id': 'SUP_SPAN.sup-1-SI', + 'desc': 'query the "superscript" state', + 'pad': '<span class="sup">foo[bar]baz</span>', + 'expected': True }, + + { 'id': 'SUP_MYSUP-1-SI', + 'desc': 'query the "superscript" state', + 'pad': '<mysup>foo[bar]baz</mysup>', + 'expected': True } + ] + }, + + { 'desc': 'query whether the selection is in an ordered list', + 'qcstate': 'insertorderedlist', + 'tests': [ + { 'id': 'IOL_TEXT-1_SI', + 'rte1-id': 'q-insertorderedlist-0', + 'desc': 'query the "insertorderedlist" state', + 'pad': 'foo[bar]baz', + 'expected': False }, + + { 'id': 'IOL_OL-LI-1_SI', + 'rte1-id': 'q-insertorderedlist-1', + 'desc': 'query the "insertorderedlist" state', + 'pad': '<ol><li>foo[bar]baz</li></ol>', + 'expected': True }, + + { 'id': 'IOL_UL_LI-1_SI', + 'rte1-id': 'q-insertorderedlist-2', + 'desc': 'query the "insertorderedlist" state', + 'pad': '<ul><li>foo[bar]baz</li></ul>', + 'expected': False } + ] + }, + + { 'desc': 'query whether the selection is in an unordered list', + 'qcstate': 'insertunorderedlist', + 'tests': [ + { 'id': 'IUL_TEXT_SI', + 'rte1-id': 'q-insertunorderedlist-0', + 'desc': 'query the "insertunorderedlist" state', + 'pad': 'foo[bar]baz', + 'expected': False }, + + { 'id': 'IUL_OL-LI-1_SI', + 'rte1-id': 'q-insertunorderedlist-1', + 'desc': 'query the "insertunorderedlist" state', + 'pad': '<ol><li>foo[bar]baz</li></ol>', + 'expected': False }, + + { 'id': 'IUL_UL-LI-1_SI', + 'rte1-id': 'q-insertunorderedlist-2', + 'desc': 'query the "insertunorderedlist" state', + 'pad': '<ul><li>foo[bar]baz</li></ul>', + 'expected': True } + ] + }, + + { 'desc': 'query whether the paragraph is centered', + 'qcstate': 'justifycenter', + 'tests': [ + { 'id': 'JC_TEXT_SI', + 'rte1-id': 'q-justifycenter-0', + 'desc': 'query the "justifycenter" state', + 'pad': 'foo[bar]baz', + 'expected': False }, + + { 'id': 'JC_DIVa:c-1_SI', + 'rte1-id': 'q-justifycenter-1', + 'desc': 'query the "justifycenter" state', + 'pad': '<div align="center">foo[bar]baz</div>', + 'expected': True }, + + { 'id': 'JC_Pa:c-1_SI', + 'rte1-id': 'q-justifycenter-2', + 'desc': 'query the "justifycenter" state', + 'pad': '<p align="center">foo[bar]baz</p>', + 'expected': True }, + + { 'id': 'JC_SPANs:ta:c-1_SI', + 'rte1-id': 'q-justifycenter-3', + 'desc': 'query the "justifycenter" state', + 'pad': '<div style="text-align: center">foo[bar]baz</div>', + 'expected': True }, + + { 'id': 'JC_SPAN.jc-1-SI', + 'desc': 'query the "justifycenter" state', + 'pad': '<span class="jc">foo[bar]baz</span>', + 'expected': True }, + + { 'id': 'JC_MYJC-1-SI', + 'desc': 'query the "justifycenter" state', + 'pad': '<myjc>foo[bar]baz</myjc>', + 'expected': True } + ] + }, + + { 'desc': 'query whether the paragraph is justified', + 'qcstate': 'justifyfull', + 'tests': [ + { 'id': 'JF_TEXT_SI', + 'rte1-id': 'q-justifyfull-0', + 'desc': 'query the "justifyfull" state', + 'pad': 'foo[bar]baz', + 'expected': False }, + + { 'id': 'JF_DIVa:j-1_SI', + 'rte1-id': 'q-justifyfull-1', + 'desc': 'query the "justifyfull" state', + 'pad': '<div align="justify">foo[bar]baz</div>', + 'expected': True }, + + { 'id': 'JF_Pa:j-1_SI', + 'rte1-id': 'q-justifyfull-2', + 'desc': 'query the "justifyfull" state', + 'pad': '<p align="justify">foo[bar]baz</p>', + 'expected': True }, + + { 'id': 'JF_SPANs:ta:j-1_SI', + 'rte1-id': 'q-justifyfull-3', + 'desc': 'query the "justifyfull" state', + 'pad': '<span style="text-align: justify">foo[bar]baz</span>', + 'expected': True }, + + { 'id': 'JF_SPAN.jf-1-SI', + 'desc': 'query the "justifyfull" state', + 'pad': '<span class="jf">foo[bar]baz</span>', + 'expected': True }, + + { 'id': 'JF_MYJF-1-SI', + 'desc': 'query the "justifyfull" state', + 'pad': '<myjf>foo[bar]baz</myjf>', + 'expected': True } + ] + }, + + { 'desc': 'query whether the paragraph is aligned left', + 'qcstate': 'justifyleft', + 'tests': [ + { 'id': 'JL_TEXT_SI', + 'desc': 'query the "justifyleft" state', + 'pad': 'foo[bar]baz', + 'expected': False }, + + { 'id': 'JL_DIVa:l-1_SI', + 'rte1-id': 'q-justifyleft-0', + 'desc': 'query the "justifyleft" state', + 'pad': '<div align="left">foo[bar]baz</div>', + 'expected': True }, + + { 'id': 'JL_Pa:l-1_SI', + 'rte1-id': 'q-justifyleft-1', + 'desc': 'query the "justifyleft" state', + 'pad': '<p align="left">foo[bar]baz</p>', + 'expected': True }, + + { 'id': 'JL_SPANs:ta:l-1_SI', + 'rte1-id': 'q-justifyleft-2', + 'desc': 'query the "justifyleft" state', + 'pad': '<span style="text-align: left">foo[bar]baz</span>', + 'expected': True }, + + { 'id': 'JL_SPAN.jl-1-SI', + 'desc': 'query the "justifyleft" state', + 'pad': '<span class="jl">foo[bar]baz</span>', + 'expected': True }, + + { 'id': 'JL_MYJL-1-SI', + 'desc': 'query the "justifyleft" state', + 'pad': '<myjl>foo[bar]baz</myjl>', + 'expected': True } + ] + }, + + { 'desc': 'query whether the paragraph is aligned right', + 'qcstate': 'justifyright', + 'tests': [ + { 'id': 'JR_TEXT_SI', + 'rte1-id': 'q-justifyright-0', + 'desc': 'query the "justifyright" state', + 'pad': 'foo[bar]baz', + 'expected': False }, + + { 'id': 'JR_DIVa:r-1_SI', + 'rte1-id': 'q-justifyright-1', + 'desc': 'query the "justifyright" state', + 'pad': '<div align="right">foo[bar]baz</div>', + 'expected': True }, + + { 'id': 'JR_Pa:r-1_SI', + 'rte1-id': 'q-justifyright-2', + 'desc': 'query the "justifyright" state', + 'pad': '<p align="right">foo[bar]baz</p>', + 'expected': True }, + + { 'id': 'JR_SPANs:ta:r-1_SI', + 'rte1-id': 'q-justifyright-3', + 'desc': 'query the "justifyright" state', + 'pad': '<span style="text-align: right">foo[bar]baz</span>', + 'expected': True }, + + { 'id': 'JR_SPAN.jr-1-SI', + 'desc': 'query the "justifyright" state', + 'pad': '<span class="jr">foo[bar]baz</span>', + 'expected': True }, + + { 'id': 'JR_MYJR-1-SI', + 'desc': 'query the "justifyright" state', + 'pad': '<myjr>foo[bar]baz</myjr>', + 'expected': True } + ] + } + ] +} + +QUERYSTATE_TESTS_CSS = { + 'id': 'QSC', + 'caption': 'queryCommandState Tests, using styleWithCSS', + 'checkAttrs': False, + 'checkStyle': False, + 'styleWithCSS': True, + + 'Proposed': QUERYSTATE_TESTS['Proposed'] +} + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/querySupported.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/querySupported.py new file mode 100644 index 0000000000..af23a428ce --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/querySupported.py @@ -0,0 +1,226 @@ + +QUERYSUPPORTED_TESTS = { + 'id': 'Q', + 'caption': 'queryCommandSupported Tests', + 'pad': 'foo[bar]baz', + 'checkAttrs': False, + 'checkStyle': False, + 'styleWithCSS': False, + 'expected': True, + + 'Proposed': [ + { 'desc': '', + 'tests': [ + ] + }, + + { 'desc': 'HTML5 commands', + 'tests': [ + { 'id': 'SELECTALL_TEXT-1', + 'desc': 'check whether the "selectall" command is supported', + 'qcsupported': 'selectall' }, + + { 'id': 'UNSELECT_TEXT-1', + 'desc': 'check whether the "unselect" command is supported', + 'qcsupported': 'unselect' }, + + { 'id': 'UNDO_TEXT-1', + 'desc': 'check whether the "undo" command is supported', + 'qcsupported': 'undo' }, + + { 'id': 'REDO_TEXT-1', + 'desc': 'check whether the "redo" command is supported', + 'qcsupported': 'redo' }, + + { 'id': 'BOLD_TEXT-1', + 'desc': 'check whether the "bold" command is supported', + 'qcsupported': 'bold' }, + + { 'id': 'BOLD_B', + 'desc': 'check whether the "bold" command is supported', + 'qcsupported': 'bold', + 'pad': '<b>foo[bar]baz</b>' }, + + { 'id': 'ITALIC_TEXT-1', + 'desc': 'check whether the "italic" command is supported', + 'qcsupported': 'italic' }, + + { 'id': 'ITALIC_I', + 'desc': 'check whether the "italic" command is supported', + 'qcsupported': 'italic', + 'pad': '<i>foo[bar]baz</i>' }, + + { 'id': 'UNDERLINE_TEXT-1', + 'desc': 'check whether the "underline" command is supported', + 'qcsupported': 'underline' }, + + { 'id': 'STRIKETHROUGH_TEXT-1', + 'desc': 'check whether the "strikethrough" command is supported', + 'qcsupported': 'strikethrough' }, + + { 'id': 'SUBSCRIPT_TEXT-1', + 'desc': 'check whether the "subscript" command is supported', + 'qcsupported': 'subscript' }, + + { 'id': 'SUPERSCRIPT_TEXT-1', + 'desc': 'check whether the "superscript" command is supported', + 'qcsupported': 'superscript' }, + + { 'id': 'FORMATBLOCK_TEXT-1', + 'desc': 'check whether the "formatblock" command is supported', + 'qcsupported': 'formatblock' }, + + { 'id': 'CREATELINK_TEXT-1', + 'desc': 'check whether the "createlink" command is supported', + 'qcsupported': 'createlink' }, + + { 'id': 'UNLINK_TEXT-1', + 'desc': 'check whether the "unlink" command is supported', + 'qcsupported': 'unlink' }, + + { 'id': 'INSERTHTML_TEXT-1', + 'desc': 'check whether the "inserthtml" command is supported', + 'qcsupported': 'inserthtml' }, + + { 'id': 'INSERTHORIZONTALRULE_TEXT-1', + 'desc': 'check whether the "inserthorizontalrule" command is supported', + 'qcsupported': 'inserthorizontalrule' }, + + { 'id': 'INSERTIMAGE_TEXT-1', + 'desc': 'check whether the "insertimage" command is supported', + 'qcsupported': 'insertimage' }, + + { 'id': 'INSERTLINEBREAK_TEXT-1', + 'desc': 'check whether the "insertlinebreak" command is supported', + 'qcsupported': 'insertlinebreak' }, + + { 'id': 'INSERTPARAGRAPH_TEXT-1', + 'desc': 'check whether the "insertparagraph" command is supported', + 'qcsupported': 'insertparagraph' }, + + { 'id': 'INSERTORDEREDLIST_TEXT-1', + 'desc': 'check whether the "insertorderedlist" command is supported', + 'qcsupported': 'insertorderedlist' }, + + { 'id': 'INSERTUNORDEREDLIST_TEXT-1', + 'desc': 'check whether the "insertunorderedlist" command is supported', + 'qcsupported': 'insertunorderedlist' }, + + { 'id': 'INSERTTEXT_TEXT-1', + 'desc': 'check whether the "inserttext" command is supported', + 'qcsupported': 'inserttext' }, + + { 'id': 'DELETE_TEXT-1', + 'desc': 'check whether the "delete" command is supported', + 'qcsupported': 'delete' }, + + { 'id': 'FORWARDDELETE_TEXT-1', + 'desc': 'check whether the "forwarddelete" command is supported', + 'qcsupported': 'forwarddelete' } + ] + }, + + { 'desc': 'MIDAS commands', + 'tests': [ + { 'id': 'STYLEWITHCSS_TEXT-1', + 'desc': 'check whether the "styleWithCSS" command is supported', + 'qcsupported': 'styleWithCSS' }, + + { 'id': 'CONTENTREADONLY_TEXT-1', + 'desc': 'check whether the "contentreadonly" command is supported', + 'qcsupported': 'contentreadonly' }, + + { 'id': 'BACKCOLOR_TEXT-1', + 'desc': 'check whether the "backcolor" command is supported', + 'qcsupported': 'backcolor' }, + + { 'id': 'FORECOLOR_TEXT-1', + 'desc': 'check whether the "forecolor" command is supported', + 'qcsupported': 'forecolor' }, + + { 'id': 'HILITECOLOR_TEXT-1', + 'desc': 'check whether the "hilitecolor" command is supported', + 'qcsupported': 'hilitecolor' }, + + { 'id': 'FONTNAME_TEXT-1', + 'desc': 'check whether the "fontname" command is supported', + 'qcsupported': 'fontname' }, + + { 'id': 'FONTSIZE_TEXT-1', + 'desc': 'check whether the "fontsize" command is supported', + 'qcsupported': 'fontsize' }, + + { 'id': 'INCREASEFONTSIZE_TEXT-1', + 'desc': 'check whether the "increasefontsize" command is supported', + 'qcsupported': 'increasefontsize' }, + + { 'id': 'DECREASEFONTSIZE_TEXT-1', + 'desc': 'check whether the "decreasefontsize" command is supported', + 'qcsupported': 'decreasefontsize' }, + + { 'id': 'HEADING_TEXT-1', + 'desc': 'check whether the "heading" command is supported', + 'qcsupported': 'heading' }, + + { 'id': 'INDENT_TEXT-1', + 'desc': 'check whether the "indent" command is supported', + 'qcsupported': 'indent' }, + + { 'id': 'OUTDENT_TEXT-1', + 'desc': 'check whether the "outdent" command is supported', + 'qcsupported': 'outdent' }, + + { 'id': 'CREATEBOOKMARK_TEXT-1', + 'desc': 'check whether the "createbookmark" command is supported', + 'qcsupported': 'createbookmark' }, + + { 'id': 'UNBOOKMARK_TEXT-1', + 'desc': 'check whether the "unbookmark" command is supported', + 'qcsupported': 'unbookmark' }, + + { 'id': 'JUSTIFYCENTER_TEXT-1', + 'desc': 'check whether the "justifycenter" command is supported', + 'qcsupported': 'justifycenter' }, + + { 'id': 'JUSTIFYFULL_TEXT-1', + 'desc': 'check whether the "justifyfull" command is supported', + 'qcsupported': 'justifyfull' }, + + { 'id': 'JUSTIFYLEFT_TEXT-1', + 'desc': 'check whether the "justifyleft" command is supported', + 'qcsupported': 'justifyleft' }, + + { 'id': 'JUSTIFYRIGHT_TEXT-1', + 'desc': 'check whether the "justifyright" command is supported', + 'qcsupported': 'justifyright' }, + + { 'id': 'REMOVEFORMAT_TEXT-1', + 'desc': 'check whether the "removeformat" command is supported', + 'qcsupported': 'removeformat' }, + + { 'id': 'COPY_TEXT-1', + 'desc': 'check whether the "copy" command is supported', + 'qcsupported': 'copy' }, + + { 'id': 'CUT_TEXT-1', + 'desc': 'check whether the "cut" command is supported', + 'qcsupported': 'cut' }, + + { 'id': 'PASTE_TEXT-1', + 'desc': 'check whether the "paste" command is supported', + 'qcsupported': 'paste' } + ] + }, + + { 'desc': 'Other tests', + 'tests': [ + { 'id': 'garbage-1_TEXT-1', + 'desc': 'check correct return value with garbage input', + 'qcsupported': '#!#@7', + 'expected': False } + ] + } + ] +} + + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryValue.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryValue.py new file mode 100644 index 0000000000..793b7cb6cf --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/queryValue.py @@ -0,0 +1,429 @@ + +QUERYVALUE_TESTS = { + 'id': 'QV', + 'caption': 'queryCommandValue Tests', + 'checkAttrs': False, + 'checkStyle': False, + 'styleWithCSS': False, + + 'Proposed': [ + { 'desc': '', + 'tests': [ + ] + }, + + { 'desc': '[HTML5] query bold value', + 'qcvalue': 'bold', + 'tests': [ + { 'id': 'B_TEXT_SI', + 'desc': 'query the "bold" value', + 'pad': 'foo[bar]baz', + 'expected': 'false' }, + + { 'id': 'B_B-1_SI', + 'desc': 'query the "bold" value', + 'pad': '<b>foo[bar]baz</b>', + 'expected': 'true' }, + + { 'id': 'B_STRONG-1_SI', + 'desc': 'query the "bold" value', + 'pad': '<strong>foo[bar]baz</strong>', + 'expected': 'true' }, + + { 'id': 'B_SPANs:fw:b-1_SI', + 'desc': 'query the "bold" value', + 'pad': '<span style="font-weight: bold">foo[bar]baz</span>', + 'expected': 'true' }, + + { 'id': 'B_SPANs:fw:n-1_SI', + 'desc': 'query the "bold" value', + 'pad': '<span style="font-weight: normal">foo[bar]baz</span>', + 'expected': 'false' }, + + { 'id': 'B_Bs:fw:n-1_SI', + 'desc': 'query the "bold" value', + 'pad': '<b><span style="font-weight: normal">foo[bar]baz</span></b>', + 'expected': 'false' }, + + { 'id': 'B_SPAN.b-1_SI', + 'desc': 'query the "bold" value', + 'pad': '<span class="b">foo[bar]baz</span>', + 'expected': 'true' }, + + { 'id': 'B_MYB-1-SI', + 'desc': 'query the "bold" value', + 'pad': '<myb>foo[bar]baz</myb>', + 'expected': 'true' } + ] + }, + + { 'desc': '[HTML5] query italic value', + 'qcvalue': 'italic', + 'tests': [ + { 'id': 'I_TEXT_SI', + 'desc': 'query the "bold" value', + 'pad': 'foo[bar]baz', + 'expected': 'false' }, + + { 'id': 'I_I-1_SI', + 'desc': 'query the "bold" value', + 'pad': '<i>foo[bar]baz</i>', + 'expected': 'true' }, + + { 'id': 'I_EM-1_SI', + 'desc': 'query the "bold" value', + 'pad': '<em>foo[bar]baz</em>', + 'expected': 'true' }, + + { 'id': 'I_SPANs:fs:i-1_SI', + 'desc': 'query the "bold" value', + 'pad': '<span style="font-style: italic">foo[bar]baz</span>', + 'expected': 'true' }, + + { 'id': 'I_SPANs:fs:n-1_SI', + 'desc': 'query the "bold" value', + 'pad': '<span style="font-style: normal">foo[bar]baz</span>', + 'expected': 'false' }, + + { 'id': 'I_I-SPANs:fs:n-1_SI', + 'desc': 'query the "bold" value', + 'pad': '<i><span style="font-style: normal">foo[bar]baz</span></i>', + 'expected': 'false' }, + + { 'id': 'I_SPAN.i-1_SI', + 'desc': 'query the "italic" value', + 'pad': '<span class="i">foo[bar]baz</span>', + 'expected': 'true' }, + + { 'id': 'I_MYI-1-SI', + 'desc': 'query the "italic" value', + 'pad': '<myi>foo[bar]baz</myi>', + 'expected': 'true' } + ] + }, + + { 'desc': '[HTML5] query block formatting value', + 'qcvalue': 'formatblock', + 'tests': [ + { 'id': 'FB_TEXT-1_SC', + 'desc': 'query the "formatBlock" value', + 'pad': 'foobar^baz', + 'expected': '', + 'accept': 'normal' }, + + { 'id': 'FB_H1-1_SC', + 'desc': 'query the "formatBlock" value', + 'pad': '<h1>foobar^baz</h1>', + 'expected': 'h1' }, + + { 'id': 'FB_PRE-1_SC', + 'desc': 'query the "formatBlock" value', + 'pad': '<pre>foobar^baz</pre>', + 'expected': 'pre' }, + + { 'id': 'FB_BQ-1_SC', + 'desc': 'query the "formatBlock" value', + 'pad': '<blockquote>foobar^baz</blockquote>', + 'expected': 'blockquote' }, + + { 'id': 'FB_ADDRESS-1_SC', + 'desc': 'query the "formatBlock" value', + 'pad': '<address>foobar^baz</address>', + 'expected': 'address' }, + + { 'id': 'FB_H1-H2-1_SC', + 'desc': 'query the "formatBlock" value', + 'pad': '<h1>foo<h2>ba^r</h2>baz</h1>', + 'expected': 'h2' }, + + { 'id': 'FB_H1-H2-1_SL', + 'desc': 'query the "formatBlock" value on oblique selection (outermost formatting expected)', + 'pad': '<h1>fo[o<h2>ba]r</h2>baz</h1>', + 'expected': 'h1' }, + + { 'id': 'FB_H1-H2-1_SR', + 'desc': 'query the "formatBlock" value on oblique selection (outermost formatting expected)', + 'pad': '<h1>foo<h2>b[ar</h2>ba]z</h1>', + 'expected': 'h1' }, + + { 'id': 'FB_TEXT-ADDRESS-1_SL', + 'desc': 'query the "formatBlock" value on oblique selection (outermost formatting expected)', + 'pad': 'fo[o<ADDRESS>ba]r</ADDRESS>baz', + 'expected': '', + 'accept': 'normal' }, + + { 'id': 'FB_TEXT-ADDRESS-1_SR', + 'desc': 'query the "formatBlock" value on oblique selection (outermost formatting expected)', + 'pad': 'foo<ADDRESS>b[ar</ADDRESS>ba]z', + 'expected': '', + 'accept': 'normal' }, + + { 'id': 'FB_H1-H2.TEXT.H2-1_SM', + 'desc': 'query the "formatBlock" value on oblique selection (outermost formatting expected)', + 'pad': '<h1><h2>fo[o</h2>bar<h2>b]az</h2></h1>', + 'expected': 'h1' } + ] + }, + + { 'desc': '[MIDAS] query heading type', + 'qcvalue': 'heading', + 'tests': [ + { 'id': 'H_H1-1_SC', + 'desc': 'query the "heading" type', + 'pad': '<h1>foobar^baz</h1>', + 'expected': 'h1', + 'accept': '<h1>' }, + + { 'id': 'H_H3-1_SC', + 'desc': 'query the "heading" type', + 'pad': '<h3>foobar^baz</h3>', + 'expected': 'h3', + 'accept': '<h3>' }, + + { 'id': 'H_H1-H2-H3-H4-1_SC', + 'desc': 'query the "heading" type within nested heading tags', + 'pad': '<h1><h2><h3><h4>foobar^baz</h4></h3></h2></h1>', + 'expected': 'h4', + 'accept': '<h4>' }, + + { 'id': 'H_P-1_SC', + 'desc': 'query the "heading" type outside of a heading', + 'pad': '<p>foobar^baz</p>', + 'expected': '' } + ] + }, + + { 'desc': '[MIDAS] query font name', + 'qcvalue': 'fontname', + 'tests': [ + { 'id': 'FN_FONTf:a-1_SI', + 'rte1-id': 'q-fontname-0', + 'desc': 'query the "fontname" value', + 'pad': '<font face="arial">foo[bar]baz</font>', + 'expected': 'arial' }, + + { 'id': 'FN_SPANs:ff:a-1_SI', + 'rte1-id': 'q-fontname-1', + 'desc': 'query the "fontname" value', + 'pad': '<span style="font-family: arial">foo[bar]baz</span>', + 'expected': 'arial' }, + + { 'id': 'FN_FONTf:a.s:ff:c-1_SI', + 'rte1-id': 'q-fontname-2', + 'desc': 'query the "fontname" value', + 'pad': '<font face="arial" style="font-family: courier">foo[bar]baz</font>', + 'expected': 'courier' }, + + { 'id': 'FN_FONTf:a-FONTf:c-1_SI', + 'rte1-id': 'q-fontname-3', + 'desc': 'query the "fontname" value', + 'pad': '<font face="arial"><font face="courier">foo[bar]baz</font></font>', + 'expected': 'courier' }, + + { 'id': 'FN_SPANs:ff:c-FONTf:a-1_SI', + 'rte1-id': 'q-fontname-4', + 'desc': 'query the "fontname" value', + 'pad': '<span style="font-family: courier"><font face="arial">foo[bar]baz</font></span>', + 'expected': 'arial' }, + + { 'id': 'FN_SPAN.fs18px-1_SI', + 'desc': 'query the "fontname" value', + 'pad': '<span class="courier">foo[bar]baz</span>', + 'expected': 'courier' }, + + { 'id': 'FN_MYCOURIER-1-SI', + 'desc': 'query the "fontname" value', + 'pad': '<mycourier>foo[bar]baz</mycourier>', + 'expected': 'courier' } + ] + }, + + { 'desc': '[MIDAS] query font size', + 'qcvalue': 'fontsize', + 'tests': [ + { 'id': 'FS_FONTsz:4-1_SI', + 'rte1-id': 'q-fontsize-0', + 'desc': 'query the "fontsize" value', + 'pad': '<font size=4>foo[bar]baz</font>', + 'expected': '18px' }, + + { 'id': 'FS_FONTs:fs:l-1_SI', + 'desc': 'query the "fontsize" value', + 'pad': '<font style="font-size: large">foo[bar]baz</font>', + 'expected': '18px' }, + + { 'id': 'FS_FONT.ass.s:fs:l-1_SI', + 'rte1-id': 'q-fontsize-1', + 'desc': 'query the "fontsize" value', + 'pad': '<font class="Apple-style-span" style="font-size: large">foo[bar]baz</font>', + 'expected': '18px' }, + + { 'id': 'FS_FONTsz:1.s:fs:xl-1_SI', + 'rte1-id': 'q-fontsize-2', + 'desc': 'query the "fontsize" value', + 'pad': '<font size=1 style="font-size: x-large">foo[bar]baz</font>', + 'expected': '24px' }, + + { 'id': 'FS_SPAN.large-1_SI', + 'desc': 'query the "fontsize" value', + 'pad': '<span class="large">foo[bar]baz</span>', + 'expected': 'large' }, + + { 'id': 'FS_SPAN.fs18px-1_SI', + 'desc': 'query the "fontsize" value', + 'pad': '<span class="fs18px">foo[bar]baz</span>', + 'expected': '18px' }, + + { 'id': 'FA_MYLARGE-1-SI', + 'desc': 'query the "fontsize" value', + 'pad': '<mylarge>foo[bar]baz</mylarge>', + 'expected': 'large' }, + + { 'id': 'FA_MYFS18PX-1-SI', + 'desc': 'query the "fontsize" value', + 'pad': '<myfs18px>foo[bar]baz</myfs18px>', + 'expected': '18px' } + ] + }, + + { 'desc': '[MIDAS] query background color', + 'qcvalue': 'backcolor', + 'tests': [ + { 'id': 'BC_FONTs:bc:fca-1_SI', + 'rte1-id': 'q-backcolor-0', + 'desc': 'query the "backcolor" value', + 'pad': '<font style="background-color: #ffccaa">foo[bar]baz</font>', + 'expected': '#ffccaa' }, + + { 'id': 'BC_SPANs:bc:abc-1_SI', + 'rte1-id': 'q-backcolor-2', + 'desc': 'query the "backcolor" value', + 'pad': '<span style="background-color: #aabbcc">foo[bar]baz</span>', + 'expected': '#aabbcc' }, + + { 'id': 'BC_FONTs:bc:084-SPAN-1_SI', + 'desc': 'query the "backcolor" value, where the color was set on an ancestor', + 'pad': '<font style="background-color: #008844"><span>foo[bar]baz</span></font>', + 'expected': '#008844' }, + + { 'id': 'BC_SPANs:bc:cde-SPAN-1_SI', + 'desc': 'query the "backcolor" value, where the color was set on an ancestor', + 'pad': '<span style="background-color: #ccddee"><span>foo[bar]baz</span></span>', + 'expected': '#ccddee' }, + + { 'id': 'BC_SPAN.ass.s:bc:rgb-1_SI', + 'rte1-id': 'q-backcolor-1', + 'desc': 'query the "backcolor" value', + 'pad': '<span class="Apple-style-span" style="background-color: rgb(255, 0, 0)">foo[bar]baz</span>', + 'expected': '#ff0000' }, + + { 'id': 'BC_SPAN.bcred-1_SI', + 'desc': 'query the "backcolor" value', + 'pad': '<span class="bcred">foo[bar]baz</span>', + 'expected': 'red' }, + + { 'id': 'BC_MYBCRED-1-SI', + 'desc': 'query the "backcolor" value', + 'pad': '<mybcred>foo[bar]baz</mybcred>', + 'expected': 'red' } + ] + }, + + { 'desc': '[MIDAS] query text color', + 'qcvalue': 'forecolor', + 'tests': [ + { 'id': 'FC_FONTc:f00-1_SI', + 'rte1-id': 'q-forecolor-0', + 'desc': 'query the "forecolor" value', + 'pad': '<font color="#ff0000">foo[bar]baz</font>', + 'expected': '#ff0000' }, + + { 'id': 'FC_SPANs:c:0f0-1_SI', + 'rte1-id': 'q-forecolor-1', + 'desc': 'query the "forecolor" value', + 'pad': '<span style="color: #00ff00">foo[bar]baz</span>', + 'expected': '#00ff00' }, + + { 'id': 'FC_FONTc:333.s:c:999-1_SI', + 'rte1-id': 'q-forecolor-2', + 'desc': 'query the "forecolor" value', + 'pad': '<font color="#333333" style="color: #999999">foo[bar]baz</font>', + 'expected': '#999999' }, + + { 'id': 'FC_FONTc:641-SPAN-1_SI', + 'desc': 'query the "forecolor" value, where the color was set on an ancestor', + 'pad': '<font color="#664411"><span>foo[bar]baz</span></font>', + 'expected': '#664411' }, + + { 'id': 'FC_SPANs:c:d95-SPAN-1_SI', + 'desc': 'query the "forecolor" value, where the color was set on an ancestor', + 'pad': '<span style="color: #dd9955"><span>foo[bar]baz</span></span>', + 'expected': '#dd9955' }, + + { 'id': 'FC_SPAN.red-1_SI', + 'desc': 'query the "forecolor" value', + 'pad': '<span class="red">foo[bar]baz</span>', + 'expected': 'red' }, + + { 'id': 'FC_MYRED-1-SI', + 'desc': 'query the "forecolor" value', + 'pad': '<myred>foo[bar]baz</myred>', + 'expected': 'red' } + ] + }, + + { 'desc': '[MIDAS] query hilight color (same as background color)', + 'qcvalue': 'hilitecolor', + 'tests': [ + { 'id': 'HC_FONTs:bc:fc0-1_SI', + 'rte1-id': 'q-hilitecolor-0', + 'desc': 'query the "hilitecolor" value', + 'pad': '<font style="background-color: #ffcc00">foo[bar]baz</font>', + 'expected': '#ffcc00' }, + + { 'id': 'HC_SPANs:bc:a0c-1_SI', + 'rte1-id': 'q-hilitecolor-2', + 'desc': 'query the "hilitecolor" value', + 'pad': '<span style="background-color: #aa00cc">foo[bar]baz</span>', + 'expected': '#aa00cc' }, + + { 'id': 'HC_SPAN.ass.s:bc:rgb-1_SI', + 'rte1-id': 'q-hilitecolor-1', + 'desc': 'query the "hilitecolor" value', + 'pad': '<span class="Apple-style-span" style="background-color: rgb(255, 0, 0)">foo[bar]baz</span>', + 'expected': '#ff0000' }, + + { 'id': 'HC_FONTs:bc:83e-SPAN-1_SI', + 'desc': 'query the "hilitecolor" value, where the color was set on an ancestor', + 'pad': '<font style="background-color: #8833ee"><span>foo[bar]baz</span></font>', + 'expected': '#8833ee' }, + + { 'id': 'HC_SPANs:bc:b12-SPAN-1_SI', + 'desc': 'query the "hilitecolor" value, where the color was set on an ancestor', + 'pad': '<span style="background-color: #bb1122"><span>foo[bar]baz</span></span>', + 'expected': '#bb1122' }, + + { 'id': 'HC_SPAN.bcred-1_SI', + 'desc': 'query the "hilitecolor" value', + 'pad': '<span class="bcred">foo[bar]baz</span>', + 'expected': 'red' }, + + { 'id': 'HC_MYBCRED-1-SI', + 'desc': 'query the "hilitecolor" value', + 'pad': '<mybcred>foo[bar]baz</mybcred>', + 'expected': 'red' } + ] + } + ] +} + +QUERYVALUE_TESTS_CSS = { + 'id': 'QVC', + 'caption': 'queryCommandValue Tests, using styleWithCSS', + 'checkAttrs': False, + 'checkStyle': False, + 'styleWithCSS': True, + + 'Proposed': QUERYVALUE_TESTS['Proposed'] +} + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/selection.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/selection.py new file mode 100644 index 0000000000..6fa7e69a66 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/selection.py @@ -0,0 +1,801 @@ + +SELECTION_TESTS = { + 'id': 'S', + 'caption': 'Selection Tests', + 'checkAttrs': True, + 'checkStyle': True, + 'styleWithCSS': False, + + 'Proposed': [ + { 'desc': '', + 'command': '', + 'tests': [ + ] + }, + + { 'desc': 'selectall', + 'command': 'selectall', + 'tests': [ + { 'id': 'SELALL_TEXT-1_SI', + 'desc': 'select all, text only', + 'pad': 'foo [bar] baz', + 'expected': [ '[foo bar baz]', + '{foo bar baz}' ] }, + + { 'id': 'SELALL_I-1_SI', + 'desc': 'select all, with outer tags', + 'pad': '<i>foo [bar] baz</i>', + 'expected': '{<i>foo bar baz</i>}' } + ] + }, + + { 'desc': 'unselect', + 'command': 'unselect', + 'tests': [ + { 'id': 'UNSEL_TEXT-1_SI', + 'desc': 'unselect', + 'pad': 'foo [bar] baz', + 'expected': 'foo bar baz' } + ] + }, + + { 'desc': 'sel.modify (generic)', + 'tests': [ + { 'id': 'SM:m.f.c_TEXT-1_SC-1', + 'desc': 'move caret 1 character forward', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': 'foo b^ar baz', + 'expected': 'foo ba^r baz' }, + + { 'id': 'SM:m.b.c_TEXT-1_SC-1', + 'desc': 'move caret 1 character backward', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': 'foo b^ar baz', + 'expected': 'foo ^bar baz' }, + + { 'id': 'SM:m.f.c_TEXT-1_SI-1', + 'desc': 'move caret forward (sollapse selection)', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': 'foo [bar] baz', + 'expected': 'foo bar^ baz' }, + + { 'id': 'SM:m.b.c_TEXT-1_SI-1', + 'desc': 'move caret backward (collapse selection)', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': 'foo [bar] baz', + 'expected': 'foo ^bar baz' }, + + { 'id': 'SM:m.f.w_TEXT-1_SC-1', + 'desc': 'move caret 1 word forward', + 'function': 'sel.modify("move", "forward", "word");', + 'pad': 'foo b^ar baz', + 'expected': 'foo bar^ baz' }, + + { 'id': 'SM:m.f.w_TEXT-1_SC-2', + 'desc': 'move caret 1 word forward', + 'function': 'sel.modify("move", "forward", "word");', + 'pad': 'foo^ bar baz', + 'expected': 'foo bar^ baz' }, + + { 'id': 'SM:m.f.w_TEXT-1_SI-1', + 'desc': 'move caret 1 word forward from non-collapsed selection', + 'function': 'sel.modify("move", "forward", "word");', + 'pad': 'foo [bar] baz', + 'expected': 'foo bar baz^' }, + + { 'id': 'SM:m.b.w_TEXT-1_SC-1', + 'desc': 'move caret 1 word backward', + 'function': 'sel.modify("move", "backward", "word");', + 'pad': 'foo b^ar baz', + 'expected': 'foo ^bar baz' }, + + { 'id': 'SM:m.b.w_TEXT-1_SC-3', + 'desc': 'move caret 1 word backward', + 'function': 'sel.modify("move", "backward", "word");', + 'pad': 'foo bar ^baz', + 'expected': 'foo ^bar baz' }, + + { 'id': 'SM:m.b.w_TEXT-1_SI-1', + 'desc': 'move caret 1 word backward from non-collapsed selection', + 'function': 'sel.modify("move", "backward", "word");', + 'pad': 'foo [bar] baz', + 'expected': '^foo bar baz' } + ] + }, + + { 'desc': 'sel.modify: move forward over combining diacritics, etc.', + 'tests': [ + { 'id': 'SM:m.f.c_CHAR-2_SC-1', + 'desc': 'move 1 character forward over combined o with diaeresis', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': 'fo^öbarbaz', + 'expected': 'foö^barbaz' }, + + { 'id': 'SM:m.f.c_CHAR-3_SC-1', + 'desc': 'move 1 character forward over character with combining diaeresis above', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': 'fo^öbarbaz', + 'expected': 'foö^barbaz' }, + + { 'id': 'SM:m.f.c_CHAR-4_SC-1', + 'desc': 'move 1 character forward over character with combining diaeresis below', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': 'fo^o̤barbaz', + 'expected': 'foo̤^barbaz' }, + + { 'id': 'SM:m.f.c_CHAR-5_SC-1', + 'desc': 'move 1 character forward over character with combining diaeresis above and below', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': 'fo^ö̤barbaz', + 'expected': 'foö̤^barbaz' }, + + { 'id': 'SM:m.f.c_CHAR-5_SI-1', + 'desc': 'move 1 character forward over character with combining diaeresis above and below, selection on diaeresis above', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': 'foo[̈]̤barbaz', + 'expected': 'foö̤^barbaz' }, + + { 'id': 'SM:m.f.c_CHAR-5_SI-2', + 'desc': 'move 1 character forward over character with combining diaeresis above and below, selection on diaeresis below', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': 'foö[̤]barbaz', + 'expected': 'foö̤^barbaz' }, + + { 'id': 'SM:m.f.c_CHAR-5_SL', + 'desc': 'move 1 character forward over character with combining diaeresis above and below, selection oblique on diaeresis and preceding text', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': 'fo[ö]̤barbaz', + 'expected': 'foö̤^barbaz' }, + + { 'id': 'SM:m.f.c_CHAR-5_SR', + 'desc': 'move 1 character forward over character with combining diaeresis above and below, selection oblique on diaeresis and following text', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': 'foö[̤bar]baz', + 'expected': 'foö̤bar^baz' }, + + { 'id': 'SM:m.f.c_CHAR-6_SC-1', + 'desc': 'move 1 character forward over character with enclosing square', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': 'fo^o⃞barbaz', + 'expected': 'foo⃞^barbaz' }, + + { 'id': 'SM:m.f.c_CHAR-7_SC-1', + 'desc': 'move 1 character forward over character with combining long solidus overlay', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': 'fo^o̸barbaz', + 'expected': 'foo̸^barbaz' } + ] + }, + + { 'desc': 'sel.modify: move backward over combining diacritics, etc.', + 'tests': [ + { 'id': 'SM:m.b.c_CHAR-2_SC-1', + 'desc': 'move 1 character backward over combined o with diaeresis', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': 'foö^barbaz', + 'expected': 'fo^öbarbaz' }, + + { 'id': 'SM:m.b.c_CHAR-3_SC-1', + 'desc': 'move 1 character backward over character with combining diaeresis above', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': 'foö^barbaz', + 'expected': 'fo^öbarbaz' }, + + { 'id': 'SM:m.b.c_CHAR-4_SC-1', + 'desc': 'move 1 character backward over character with combining diaeresis below', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': 'foo̤^barbaz', + 'expected': 'fo^o̤barbaz' }, + + { 'id': 'SM:m.b.c_CHAR-5_SC-1', + 'desc': 'move 1 character backward over character with combining diaeresis above and below', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': 'foö̤^barbaz', + 'expected': 'fo^ö̤barbaz' }, + + { 'id': 'SM:m.b.c_CHAR-5_SI-1', + 'desc': 'move 1 character backward over character with combining diaeresis above and below, selection on diaeresis above', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': 'foo[̈]̤barbaz', + 'expected': 'fo^ö̤barbaz' }, + + { 'id': 'SM:m.b.c_CHAR-5_SI-2', + 'desc': 'move 1 character backward over character with combining diaeresis above and below, selection on diaeresis below', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': 'foö[̤]barbaz', + 'expected': 'fo^ö̤barbaz' }, + + { 'id': 'SM:m.b.c_CHAR-5_SL', + 'desc': 'move 1 character backward over character with combining diaeresis above and below, selection oblique on diaeresis and preceding text', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': 'fo[ö]̤barbaz', + 'expected': 'fo^ö̤barbaz' }, + + { 'id': 'SM:m.b.c_CHAR-5_SR', + 'desc': 'move 1 character backward over character with combining diaeresis above and below, selection oblique on diaeresis and following text', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': 'foö[̤bar]baz', + 'expected': 'fo^ö̤barbaz' }, + + { 'id': 'SM:m.b.c_CHAR-6_SC-1', + 'desc': 'move 1 character backward over character with enclosing square', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': 'foo⃞^barbaz', + 'expected': 'fo^o⃞barbaz' }, + + { 'id': 'SM:m.b.c_CHAR-7_SC-1', + 'desc': 'move 1 character backward over character with combining long solidus overlay', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': 'foo̸^barbaz', + 'expected': 'fo^o̸barbaz' } + ] + }, + + { 'desc': 'sel.modify: move forward/backward/left/right in RTL text', + 'tests': [ + { 'id': 'SM:m.f.c_Pdir:rtl-1_SC-1', + 'desc': 'move caret forward 1 character in right-to-left text', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': '<p dir="rtl">foo b^ar baz</p>', + 'expected': '<p dir="rtl">foo ba^r baz</p>' }, + + { 'id': 'SM:m.b.c_Pdir:rtl-1_SC-1', + 'desc': 'move caret backward 1 character in right-to-left text', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': '<p dir="rtl">foo ba^r baz</p>', + 'expected': '<p dir="rtl">foo b^ar baz</p>' }, + + { 'id': 'SM:m.r.c_Pdir:rtl-1_SC-1', + 'desc': 'move caret 1 character to the right in LTR text within RTL context', + 'function': 'sel.modify("move", "right", "character");', + 'pad': '<p dir="rtl">foo b^ar baz</p>', + 'expected': '<p dir="rtl">foo ba^r baz</p>' }, + + { 'id': 'SM:m.l.c_Pdir:rtl-1_SC-1', + 'desc': 'move caret 1 character to the left in LTR text within RTL context', + 'function': 'sel.modify("move", "left", "character");', + 'pad': '<p dir="rtl">foo ba^r baz</p>', + 'expected': '<p dir="rtl">foo b^ar baz</p>' }, + + + { 'id': 'SM:m.f.c_TEXT:ar-1_SC-1', + 'desc': 'move caret forward 1 character in Arabic text', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': 'مرح^با العالم', + 'expected': 'مرحب^ا العالم' }, + + { 'id': 'SM:m.b.c_TEXT:ar-1_SC-1', + 'desc': 'move caret backward 1 character in Arabic text', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': 'مرح^با العالم', + 'expected': 'مر^حبا العالم' }, + + { 'id': 'SM:m.f.c_TEXT:he-1_SC-1', + 'desc': 'move caret forward 1 character in Hebrew text', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': 'של^ום עולם', + 'expected': 'שלו^ם עולם' }, + + { 'id': 'SM:m.b.c_TEXT:he-1_SC-1', + 'desc': 'move caret backward 1 character in Hebrew text', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': 'של^ום עולם', + 'expected': 'ש^לום עולם' }, + + + { 'id': 'SM:m.f.c_BDOdir:rtl-1_SC-1', + 'desc': 'move caret forward 1 character inside <bdo>', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': 'foo <bdo dir="rtl">b^ar</bdo> baz', + 'expected': 'foo <bdo dir="rtl">ba^r</bdo> baz' }, + + { 'id': 'SM:m.b.c_BDOdir:rtl-1_SC-1', + 'desc': 'move caret backward 1 character inside <bdo>', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': 'foo <bdo dir="rtl">ba^r</bdo> baz', + 'expected': 'foo <bdo dir="rtl">b^ar</bdo> baz' }, + + { 'id': 'SM:m.r.c_BDOdir:rtl-1_SC-1', + 'desc': 'move caret 1 character to the right inside <bdo>', + 'function': 'sel.modify("move", "right", "character");', + 'pad': 'foo <bdo dir="rtl">ba^r</bdo> baz', + 'expected': 'foo <bdo dir="rtl">b^ar</bdo> baz' }, + + { 'id': 'SM:m.l.c_BDOdir:rtl-1_SC-1', + 'desc': 'move caret 1 character to the left inside <bdo>', + 'function': 'sel.modify("move", "left", "character");', + 'pad': 'foo <bdo dir="rtl">b^ar</bdo> baz', + 'expected': 'foo <bdo dir="rtl">ba^r</bdo> baz' }, + + + { 'id': 'SM:m.f.c_TEXTrle-1_SC-rtl-1', + 'desc': 'move caret forward in RTL text within RLE-PDF marks', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': 'I said, "(RLE)‫car يعني سي^ارة‬(PDF)".', + 'expected': 'I said, "(RLE)‫car يعني سيا^رة‬(PDF)".' }, + + { 'id': 'SM:m.b.c_TEXTrle-1_SC-rtl-1', + 'desc': 'move caret backward in RTL text within RLE-PDF marks', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': 'I said, "(RLE)‫car يعني سي^ارة‬(PDF)".', + 'expected': 'I said, "(RLE)‫car يعني س^يارة‬(PDF)".' }, + + { 'id': 'SM:m.r.c_TEXTrle-1_SC-rtl-1', + 'desc': 'move caret 1 character to the right in RTL text within RLE-PDF marks', + 'function': 'sel.modify("move", "right", "character");', + 'pad': 'I said, "(RLE)‫car يعني سي^ارة‬(PDF)".', + 'expected': 'I said, "(RLE)‫car يعني س^يارة‬(PDF)".' }, + + { 'id': 'SM:m.l.c_TEXTrle-1_SC-rtl-1', + 'desc': 'move caret 1 character to the left in RTL text within RLE-PDF marks', + 'function': 'sel.modify("move", "left", "character");', + 'pad': 'I said, "(RLE)‫car يعني سي^ارة‬(PDF)".', + 'expected': 'I said, "(RLE)‫car يعني سيا^رة‬(PDF)".' }, + + { 'id': 'SM:m.f.c_TEXTrle-1_SC-ltr-1', + 'desc': 'move caret forward in LTR text within RLE-PDF marks', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': 'I said, "(RLE)‫c^ar يعني سيارة‬(PDF)".', + 'expected': 'I said, "(RLE)‫ca^r يعني سيارة‬(PDF)".' }, + + { 'id': 'SM:m.b.c_TEXTrle-1_SC-ltr-1', + 'desc': 'move caret backward in LTR text within RLE-PDF marks', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': 'I said, "(RLE)‫ca^r يعني سيارة‬(PDF)".', + 'expected': 'I said, "(RLE)‫c^ar يعني سيارة‬(PDF)".' }, + + { 'id': 'SM:m.r.c_TEXTrle-1_SC-ltr-1', + 'desc': 'move caret 1 character to the right in LTR text within RLE-PDF marks', + 'function': 'sel.modify("move", "right", "character");', + 'pad': 'I said, "(RLE)‫c^ar يعني سيارة‬(PDF)".', + 'expected': 'I said, "(RLE)‫ca^r يعني سيارة‬(PDF)".' }, + + { 'id': 'SM:m.l.c_TEXTrle-1_SC-ltr-1', + 'desc': 'move caret 1 character to the left in LTR text within RLE-PDF marks', + 'function': 'sel.modify("move", "left", "character");', + 'pad': 'I said, "(RLE)‫ca^r يعني سيارة‬(PDF)".', + 'expected': 'I said, "(RLE)‫c^ar يعني سيارة‬(PDF)".' }, + + + { 'id': 'SM:m.f.c_TEXTrlo-1_SC-rtl-1', + 'desc': 'move caret forward in RTL text within RLO-PDF marks', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': 'I said, "(RLO)‮car يعني سي^ارة‬(PDF)".', + 'expected': 'I said, "(RLO)‮car يعني سيا^رة‬(PDF)".' }, + + { 'id': 'SM:m.b.c_TEXTrlo-1_SC-rtl-1', + 'desc': 'move caret backward in RTL text within RLO-PDF marks', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': 'I said, "(RLO)‮car يعني سي^ارة‬(PDF)".', + 'expected': 'I said, "(RLO)‮car يعني س^يارة‬(PDF)".' }, + + { 'id': 'SM:m.r.c_TEXTrlo-1_SC-rtl-1', + 'desc': 'move caret 1 character to the right in RTL text within RLO-PDF marks', + 'function': 'sel.modify("move", "right", "character");', + 'pad': 'I said, "(RLO)‮car يعني سي^ارة‬(PDF)".', + 'expected': 'I said, "(RLO)‮car يعني س^يارة‬(PDF)".' }, + + { 'id': 'SM:m.l.c_TEXTrlo-1_SC-rtl-1', + 'desc': 'move caret 1 character to the left in RTL text within RLO-PDF marks', + 'function': 'sel.modify("move", "left", "character");', + 'pad': 'I said, "(RLO)‮car يعني سي^ارة‬(PDF)".', + 'expected': 'I said, "(RLO)‮car يعني سيا^رة‬(PDF)".' }, + + { 'id': 'SM:m.f.c_TEXTrlo-1_SC-ltr-1', + 'desc': 'move caret forward in Latin text within RLO-PDF marks', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': 'I said, "(RLO)‮c^ar يعني سيارة‬(PDF)".', + 'expected': 'I said, "(RLO)‮ca^r يعني سيارة‬(PDF)".' }, + + { 'id': 'SM:m.b.c_TEXTrlo-1_SC-ltr-1', + 'desc': 'move caret backward in Latin text within RLO-PDF marks', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': 'I said, "(RLO)‮ca^r يعني سيارة‬(PDF)".', + 'expected': 'I said, "(RLO)‮c^ar يعني سيارة‬(PDF)".' }, + + { 'id': 'SM:m.r.c_TEXTrlo-1_SC-ltr-1', + 'desc': 'move caret 1 character to the right in Latin text within RLO-PDF marks', + 'function': 'sel.modify("move", "right", "character");', + 'pad': 'I said, "(RLO)‮ca^r يعني سيارة‬(PDF)".', + 'expected': 'I said, "(RLO)‮c^ar يعني سيارة‬(PDF)".' }, + + { 'id': 'SM:m.l.c_TEXTrlo-1_SC-ltr-1', + 'desc': 'move caret 1 character to the left in Latin text within RLO-PDF marks', + 'function': 'sel.modify("move", "left", "character");', + 'pad': 'I said, "(RLO)‮c^ar يعني سيارة‬(PDF)".', + 'expected': 'I said, "(RLO)‮ca^r يعني سيارة‬(PDF)".' }, + + + { 'id': 'SM:m.f.c_TEXTrlm-1_SC-1', + 'desc': 'move caret forward in RTL text within neutral characters followed by RLM', + 'function': 'sel.modify("move", "forward", "character");', + 'pad': 'I said, "يعني سيارة!^?!‏(RLM)".', + 'expected': 'I said, "يعني سيارة!?^!‏(RLM)".' }, + + { 'id': 'SM:m.b.c_TEXTrlm-1_SC-1', + 'desc': 'move caret backward in RTL text within neutral characters followed by RLM', + 'function': 'sel.modify("move", "backward", "character");', + 'pad': 'I said, "يعني سيارة!?^!‏(RLM)".', + 'expected': 'I said, "يعني سيارة!^?!‏(RLM)".' }, + + { 'id': 'SM:m.r.c_TEXTrlm-1_SC-1', + 'desc': 'move caret 1 character to the right in RTL text within neutral characters followed by RLM', + 'function': 'sel.modify("move", "right", "character");', + 'pad': 'I said, "يعني سيارة!?^!‏(RLM)".', + 'expected': 'I said, "يعني سيارة!^?!‏(RLM)".' }, + + { 'id': 'SM:m.l.c_TEXTrlm-1_SC-1', + 'desc': 'move caret 1 character to the left in RTL text within neutral characters followed by RLM', + 'function': 'sel.modify("move", "left", "character");', + 'pad': 'I said, "يعني سيارة!^?!‏(RLM)".', + 'expected': 'I said, "يعني سيارة!?^!‏(RLM)".' } + ] + }, + + { 'desc': 'sel.modify: move forward/backward over words in Japanese text', + 'tests': [ + { 'id': 'SM:m.f.w_TEXT-jp_SC-1', + 'desc': 'move caret forward 1 word in Japanese text (adjective)', + 'function': 'sel.modify("move", "forward", "word");', + 'pad': '^面白い例文をテストしましょう。', + 'expected': '面白い^例文をテストしましょう。' }, + + { 'id': 'SM:m.f.w_TEXT-jp_SC-2', + 'desc': 'move caret forward 1 word in Japanese text (in the middle of a word)', + 'function': 'sel.modify("move", "forward", "word");', + 'pad': '面^白い例文をテストしましょう。', + 'expected': '面白い^例文をテストしましょう。' }, + + { 'id': 'SM:m.f.w_TEXT-jp_SC-3', + 'desc': 'move caret forward 1 word in Japanese text (noun)', + 'function': 'sel.modify("move", "forward", "word");', + 'pad': '面白い^例文をテストしましょう。', + 'expected': [ '面白い例文^をテストしましょう。', + '面白い例文を^テストしましょう。' ] }, + + { 'id': 'SM:m.f.w_TEXT-jp_SC-4', + 'desc': 'move caret forward 1 word in Japanese text (Katakana)', + 'function': 'sel.modify("move", "forward", "word");', + 'pad': '面白い例文を^テストしましょう。', + 'expected': '面白い例文をテスト^しましょう。' }, + + { 'id': 'SM:m.f.w_TEXT-jp_SC-5', + 'desc': 'move caret forward 1 word in Japanese text (verb)', + 'function': 'sel.modify("move", "forward", "word");', + 'pad': '面白い例文をテスト^しましょう。', + 'expected': '面白い例文をテストしましょう^。' } + ] + }, + + { 'desc': 'sel.modify: move forward/backward over words in Thai text', + 'tests': [ + { 'id': 'SM:m.f.w_TEXT-th_SC-1', + 'desc': 'move caret forward 1 word in Thai text', + 'function': 'sel.modify("move", "forward", "word");', + 'pad': '^ทดสอบการทำงาน', + 'expected': 'ทดสอบ^การทำงาน' }, + + { 'id': 'SM:m.f.w_TEXT-th_SC-2', + 'desc': 'move caret forward 1 word in Thai text (mid-word)', + 'function': 'sel.modify("move", "forward", "word");', + 'pad': 'ทดสอบก^ารทำงาน', + 'expected': 'ทดสอบการ^ทำงาน' }, + + { 'id': 'SM:m.b.w_TEXT-th_SC-1', + 'desc': 'move caret backward 1 word in Thai text', + 'function': 'sel.modify("move", "backward", "word");', + 'pad': 'ทดสอบการ^ทำงาน', + 'expected': 'ทดสอบ^การทำงาน' }, + + { 'id': 'SM:m.b.w_TEXT-th_SC-2', + 'desc': 'move caret backward 1 word in Thai text (mid-word)', + 'function': 'sel.modify("move", "backward", "word");', + 'pad': 'ทดสอบการทำงา^น', + 'expected': [ 'ทดสอบการ^ทำงาน', + 'ทดสอบการทำ^งาน' ] }, + ] + }, + + { 'desc': 'sel.modify: extend selection forward', + 'tests': [ + { 'id': 'SM:e.f.c_TEXT-1_SC-1', + 'desc': 'extend selection 1 character forward', + 'function': 'sel.modify("extend", "forward", "character");', + 'pad': 'foo ^bar baz', + 'expected': 'foo [b]ar baz' }, + + { 'id': 'SM:e.f.c_TEXT-1_SI-1', + 'desc': 'extend selection 1 character forward', + 'function': 'sel.modify("extend", "forward", "character");', + 'pad': 'foo [b]ar baz', + 'expected': 'foo [ba]r baz' }, + + { 'id': 'SM:e.f.w_TEXT-1_SC-1', + 'desc': 'extend selection 1 word forward', + 'function': 'sel.modify("extend", "forward", "word");', + 'pad': 'foo ^bar baz', + 'expected': 'foo [bar] baz' }, + + { 'id': 'SM:e.f.w_TEXT-1_SI-1', + 'desc': 'extend selection 1 word forward', + 'function': 'sel.modify("extend", "forward", "word");', + 'pad': 'foo [b]ar baz', + 'expected': 'foo [bar] baz' }, + + { 'id': 'SM:e.f.w_TEXT-1_SI-2', + 'desc': 'extend selection 1 word forward', + 'function': 'sel.modify("extend", "forward", "word");', + 'pad': 'foo [bar] baz', + 'expected': 'foo [bar baz]' } + ] + }, + + { 'desc': 'sel.modify: extend selection backward, shrinking it', + 'tests': [ + { 'id': 'SM:e.b.c_TEXT-1_SI-2', + 'desc': 'extend selection 1 character backward', + 'function': 'sel.modify("extend", "backward", "character");', + 'pad': 'foo [bar] baz', + 'expected': 'foo [ba]r baz' }, + + { 'id': 'SM:e.b.c_TEXT-1_SI-1', + 'desc': 'extend selection 1 character backward', + 'function': 'sel.modify("extend", "backward", "character");', + 'pad': 'foo [b]ar baz', + 'expected': 'foo ^bar baz' }, + + { 'id': 'SM:e.b.w_TEXT-1_SI-3', + 'desc': 'extend selection 1 word backward', + 'function': 'sel.modify("extend", "backward", "word");', + 'pad': 'foo [bar baz]', + 'expected': 'foo [bar] baz' }, + + { 'id': 'SM:e.b.w_TEXT-1_SI-2', + 'desc': 'extend selection 1 word backward', + 'function': 'sel.modify("extend", "backward", "word");', + 'pad': 'foo [bar] baz', + 'expected': 'foo ^bar baz' }, + + { 'id': 'SM:e.b.w_TEXT-1_SI-4', + 'desc': 'extend selection 1 word backward', + 'function': 'sel.modify("extend", "backward", "word");', + 'pad': 'foo b[ar baz]', + 'expected': 'foo b[ar] baz' }, + + { 'id': 'SM:e.b.w_TEXT-1_SI-5', + 'desc': 'extend selection 1 word backward', + 'function': 'sel.modify("extend", "backward", "word");', + 'pad': 'foo b[ar] baz', + 'expected': 'foo b^ar baz' } + ] + }, + + { 'desc': 'sel.modify: extend selection backward, creating or extending a reverse selections', + 'tests': [ + { 'id': 'SM:e.b.c_TEXT-1_SC-1', + 'desc': 'extend selection 1 character backward', + 'function': 'sel.modify("extend", "backward", "character");', + 'pad': 'foo b^ar baz', + 'expected': 'foo ]b[ar baz' }, + + { 'id': 'SM:e.b.c_TEXT-1_SIR-1', + 'desc': 'extend selection 1 character backward', + 'function': 'sel.modify("extend", "backward", "character");', + 'pad': 'foo b]a[r baz', + 'expected': 'foo ]ba[r baz' }, + + { 'id': 'SM:e.b.w_TEXT-1_SIR-1', + 'desc': 'extend selection 1 word backward', + 'function': 'sel.modify("extend", "backward", "word");', + 'pad': 'foo b]a[r baz', + 'expected': 'foo ]ba[r baz' }, + + { 'id': 'SM:e.b.w_TEXT-1_SIR-2', + 'desc': 'extend selection 1 word backward', + 'function': 'sel.modify("extend", "backward", "word");', + 'pad': 'foo ]ba[r baz', + 'expected': ']foo ba[r baz' } + ] + }, + + { 'desc': 'sel.modify: extend selection forward, shrinking a reverse selections', + 'tests': [ + { 'id': 'SM:e.f.c_TEXT-1_SIR-1', + 'desc': 'extend selection 1 character forward', + 'function': 'sel.modify("extend", "forward", "character");', + 'pad': 'foo b]a[r baz', + 'expected': 'foo ba^r baz' }, + + { 'id': 'SM:e.f.c_TEXT-1_SIR-2', + 'desc': 'extend selection 1 character forward', + 'function': 'sel.modify("extend", "forward", "character");', + 'pad': 'foo ]ba[r baz', + 'expected': 'foo b]a[r baz' }, + + { 'id': 'SM:e.f.w_TEXT-1_SIR-1', + 'desc': 'extend selection 1 word forward', + 'function': 'sel.modify("extend", "forward", "word");', + 'pad': 'foo ]ba[r baz', + 'expected': 'foo ba^r baz' }, + + { 'id': 'SM:e.f.w_TEXT-1_SIR-3', + 'desc': 'extend selection 1 word forward', + 'function': 'sel.modify("extend", "forward", "word");', + 'pad': ']foo ba[r baz', + 'expected': 'foo ]ba[r baz' } + ] + }, + + { 'desc': 'sel.modify: extend selection forward to line boundary', + 'tests': [ + { 'id': 'SM:e.f.lb_BR.BR-1_SC-1', + 'desc': 'extend selection forward to line boundary', + 'function': 'sel.modify("extend", "forward", "lineboundary");', + 'pad': 'fo^o<br>bar<br>baz', + 'expected': 'fo[o]<br>bar<br>baz' }, + + { 'id': 'SM:e.f.lb_BR.BR-1_SI-1', + 'desc': 'extend selection forward to next line boundary', + 'function': 'sel.modify("extend", "forward", "lineboundary");', + 'pad': 'fo[o]<br>bar<br>baz', + 'expected': 'fo[o<br>bar]<br>baz' }, + + { 'id': 'SM:e.f.lb_BR.BR-1_SM-1', + 'desc': 'extend selection forward to line boundary', + 'function': 'sel.modify("extend", "forward", "lineboundary");', + 'pad': 'fo[o<br>b]ar<br>baz', + 'expected': 'fo[o<br>bar]<br>baz' }, + + { 'id': 'SM:e.f.lb_P.P.P-1_SC-1', + 'desc': 'extend selection forward to line boundary', + 'function': 'sel.modify("extend", "forward", "lineboundary");', + 'pad': '<p>fo^o</p><p>bar</p><p>baz</p>', + 'expected': '<p>fo[o]</p><p>bar</p><p>baz</p>' }, + + { 'id': 'SM:e.f.lb_P.P.P-1_SI-1', + 'desc': 'extend selection forward to next line boundary', + 'function': 'sel.modify("extend", "forward", "lineboundary");', + 'pad': '<p>fo[o]</p><p>bar</p><p>baz</p>', + 'expected': '<p>fo[o</p><p>bar]</p><p>baz</p>' }, + + { 'id': 'SM:e.f.lb_P.P.P-1_SM-1', + 'desc': 'extend selection forward to line boundary', + 'function': 'sel.modify("extend", "forward", "lineboundary");', + 'pad': '<p>fo[o</p><p>b]ar</p><p>baz</p>', + 'expected': '<p>fo[o</p><p>bar]</p><p>baz</p>' }, + + { 'id': 'SM:e.f.lb_P.P.P-1_SMR-1', + 'desc': 'extend selection forward to line boundary', + 'function': 'sel.modify("extend", "forward", "lineboundary");', + 'pad': '<p>foo</p><p>b]a[r</p><p>baz</p>', + 'expected': '<p>foo</p><p>ba[r]</p><p>baz</p>' } + ] + }, + + { 'desc': 'sel.modify: extend selection backward to line boundary', + 'tests': [ + { 'id': 'SM:e.b.lb_BR.BR-1_SC-2', + 'desc': 'extend selection backward to line boundary', + 'function': 'sel.modify("extend", "backward", "lineboundary");', + 'pad': 'foo<br>bar<br>b^az', + 'expected': 'foo<br>bar<br>]b[az' }, + + { 'id': 'SM:e.b.lb_BR.BR-1_SIR-2', + 'desc': 'extend selection backward to previous line boundary', + 'function': 'sel.modify("extend", "backward", "lineboundary");', + 'pad': 'foo<br>bar<br>]b[az', + 'expected': 'foo<br>]bar<br>b[az' }, + + { 'id': 'SM:e.b.lb_BR.BR-1_SMR-2', + 'desc': 'extend selection backward to line boundary', + 'function': 'sel.modify("extend", "backward", "lineboundary");', + 'pad': 'foo<br>ba]r<br>b[az', + 'expected': 'foo<br>]bar<br>b[az' }, + + { 'id': 'SM:e.b.lb_P.P.P-1_SC-2', + 'desc': 'extend selection backward to line boundary', + 'function': 'sel.modify("extend", "backward", "lineboundary");', + 'pad': '<p>foo</p><p>bar</p><p>b^az</p>', + 'expected': '<p>foo</p><p>bar</p><p>]b[az</p>' }, + + { 'id': 'SM:e.b.lb_P.P.P-1_SIR-2', + 'desc': 'extend selection backward to previous line boundary', + 'function': 'sel.modify("extend", "backward", "lineboundary");', + 'pad': '<p>foo</p><p>bar</p><p>]b[az</p>', + 'expected': '<p>foo</p><p>]bar</p><p>b[az</p>' }, + + { 'id': 'SM:e.b.lb_P.P.P-1_SMR-2', + 'desc': 'extend selection backward to line boundary', + 'function': 'sel.modify("extend", "backward", "lineboundary");', + 'pad': '<p>foo</p><p>ba]r</p><p>b[az</p>', + 'expected': '<p>foo</p><p>]bar</p><p>b[az</p>' }, + + { 'id': 'SM:e.b.lb_P.P.P-1_SM-2', + 'desc': 'extend selection backward to line boundary', + 'function': 'sel.modify("extend", "backward", "lineboundary");', + 'pad': '<p>foo</p><p>b[a]r</p><p>baz</p>', + 'expected': '<p>foo</p><p>]b[ar</p><p>baz</p>' } + ] + }, + + { 'desc': 'sel.modify: extend selection forward to next line (NOTE: use identical text in every line!)', + 'tests': [ + { 'id': 'SM:e.f.l_BR.BR-2_SC-1', + 'desc': 'extend selection forward to next line', + 'function': 'sel.modify("extend", "forward", "line");', + 'pad': 'fo^o<br>foo<br>foo', + 'expected': 'fo[o<br>fo]o<br>foo' }, + + { 'id': 'SM:e.f.l_BR.BR-2_SI-1', + 'desc': 'extend selection forward to next line', + 'function': 'sel.modify("extend", "forward", "line");', + 'pad': 'fo[o]<br>foo<br>foo', + 'expected': 'fo[o<br>foo]<br>foo' }, + + { 'id': 'SM:e.f.l_BR.BR-2_SM-1', + 'desc': 'extend selection forward to next line', + 'function': 'sel.modify("extend", "forward", "line");', + 'pad': 'fo[o<br>f]oo<br>foo', + 'expected': 'fo[o<br>foo<br>f]oo' }, + + { 'id': 'SM:e.f.l_P.P-1_SC-1', + 'desc': 'extend selection forward to next line over paragraph boundaries', + 'function': 'sel.modify("extend", "forward", "line");', + 'pad': '<p>foo^bar</p><p>foobar</p>', + 'expected': '<p>foo[bar</p><p>foo]bar</p>' }, + + { 'id': 'SM:e.f.l_P.P-1_SMR-1', + 'desc': 'extend selection forward to next line over paragraph boundaries', + 'function': 'sel.modify("extend", "forward", "line");', + 'pad': '<p>fo]obar</p><p>foob[ar</p>', + 'expected': '<p>foobar</p><p>fo]ob[ar</p>' } + ] + }, + + { 'desc': 'sel.modify: extend selection backward to previous line (NOTE: use identical text in every line!)', + 'tests': [ + { 'id': 'SM:e.b.l_BR.BR-2_SC-2', + 'desc': 'extend selection backward to previous line', + 'function': 'sel.modify("extend", "backward", "line");', + 'pad': 'foo<br>foo<br>f^oo', + 'expected': 'foo<br>f]oo<br>f[oo' }, + + { 'id': 'SM:e.b.l_BR.BR-2_SIR-2', + 'desc': 'extend selection backward to previous line', + 'function': 'sel.modify("extend", "backward", "line");', + 'pad': 'foo<br>foo<br>]f[oo', + 'expected': 'foo<br>]foo<br>f[oo' }, + + { 'id': 'SM:e.b.l_BR.BR-2_SMR-2', + 'desc': 'extend selection backward to previous line', + 'function': 'sel.modify("extend", "backward", "line");', + 'pad': 'foo<br>fo]o<br>f[oo', + 'expected': 'fo]o<br>foo<br>f[oo' }, + + { 'id': 'SM:e.b.l_P.P-1_SC-2', + 'desc': 'extend selection backward to next line over paragraph boundaries', + 'function': 'sel.modify("extend", "backward", "line");', + 'pad': '<p>foobar</p><p>foo^bar</p>', + 'expected': '<p>foo]bar</p><p>foo[bar</p>' }, + + { 'id': 'SM:e.b.l_P.P-1_SM-1', + 'desc': 'extend selection backward to next line over paragraph boundaries', + 'function': 'sel.modify("extend", "backward", "line");', + 'pad': '<p>fo[obar</p><p>foob]ar</p>', + 'expected': '<p>fo[ob]ar</p><p>foobar</p>' } + ] + }, + + { 'desc': 'sel.selectAllChildren(<element>)', + 'function': 'sel.selectAllChildren(doc.getElementById("div"));', + 'tests': [ + { 'id': 'SAC:div_DIV-1_SC-1', + 'desc': 'selectAllChildren(<div>)', + 'pad': 'foo<div id="div">bar <span>ba^z</span></div>qoz', + 'expected': [ 'foo<div id="div">[bar <span>baz</span>}</div>qoz', + 'foo<div id="div">{bar <span>baz</span>}</div>qoz' ] }, + ] + } + ] +} + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/unapply.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/unapply.py new file mode 100644 index 0000000000..adad65617e --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/unapply.py @@ -0,0 +1,462 @@ + +UNAPPLY_TESTS = { + 'id': 'U', + 'caption': 'Unapply Existing Formatting Tests', + 'checkAttrs': True, + 'checkStyle': True, + 'styleWithCSS': False, + 'expected': 'foo[bar]baz', + + 'RFC': [ + { 'desc': '', + 'command': '', + 'tests': [ + ] + }, + + { 'desc': 'remove link', + 'command': 'unlink', + 'tests': [ + { 'id': 'UNLINK_A-1_SO', + 'desc': 'unlink wrapped <a> element', + 'pad': 'foo[<a>bar</a>]baz' }, + + { 'id': 'UNLINK_A-1_SW', + 'desc': 'unlink <a> element where the selection wraps the full content', + 'pad': 'foo<a>[bar]</a>baz' }, + + { 'id': 'UNLINK_An:a.h:id-1_SO', + 'desc': 'unlink wrapped <a> element that has a name and href attribute', + 'pad': 'foo[<a name="A" href="#UNLINK:An:a.h:id-1_SO">bar</a>]baz' }, + + { 'id': 'UNLINK_A-2_SO', + 'desc': 'unlink contained <a> element', + 'pad': 'foo[b<a>a</a>r]baz' }, + + { 'id': 'UNLINK_A2-1_SO', + 'desc': 'unlink 2 contained <a> elements', + 'pad': 'foo[<a>b</a>a<a>r</a>]baz' } + ] + } + ], + + 'Proposed': [ + { 'desc': '', + 'command': '', + 'tests': [ + ] + }, + + { 'desc': 'remove bold', + 'command': 'bold', + 'tests': [ + { 'id': 'B_B-1_SW', + 'rte1-id': 'u-bold-0', + 'desc': 'Selection within tags; remove <b> tags', + 'pad': 'foo<b>[bar]</b>baz' }, + + { 'id': 'B_B-1_SO', + 'desc': 'Selection outside of tags; remove <b> tags', + 'pad': 'foo[<b>bar</b>]baz' }, + + { 'id': 'B_B-1_SL', + 'desc': 'Selection oblique left; remove <b> tags', + 'pad': 'foo[<b>bar]</b>baz' }, + + { 'id': 'B_B-1_SR', + 'desc': 'Selection oblique right; remove <b> tags', + 'pad': 'foo<b>[bar</b>]baz' }, + + { 'id': 'B_STRONG-1_SW', + 'rte1-id': 'u-bold-1', + 'desc': 'Selection within tags; remove <strong> tags', + 'pad': 'foo<strong>[bar]</strong>baz' }, + + { 'id': 'B_STRONG-1_SO', + 'desc': 'Selection outside of tags; remove <strong> tags', + 'pad': 'foo[<strong>bar</strong>]baz' }, + + { 'id': 'B_STRONG-1_SL', + 'desc': 'Selection oblique left; remove <strong> tags', + 'pad': 'foo[<strong>bar]</strong>baz' }, + + { 'id': 'B_STRONG-1_SR', + 'desc': 'Selection oblique right; remove <strong> tags', + 'pad': 'foo<strong>[bar</strong>]baz' }, + + { 'id': 'B_SPANs:fw:b-1_SW', + 'rte1-id': 'u-bold-2', + 'desc': 'Selection within tags; remove "font-weight: bold"', + 'pad': 'foo<span style="font-weight: bold">[bar]</span>baz' }, + + { 'id': 'B_SPANs:fw:b-1_SO', + 'desc': 'Selection outside of tags; remove "font-weight: bold"', + 'pad': 'foo[<span style="font-weight: bold">bar</span>]baz' }, + + { 'id': 'B_SPANs:fw:b-1_SL', + 'desc': 'Selection oblique left; remove "font-weight: bold"', + 'pad': 'foo[<span style="font-weight: bold">bar]</span>baz' }, + + { 'id': 'B_SPANs:fw:b-1_SR', + 'desc': 'Selection oblique right; remove "font-weight: bold"', + 'pad': 'foo<span style="font-weight: bold">[bar</span>]baz' }, + + { 'id': 'B_B-P3-1_SO12', + 'desc': 'Unbolding multiple paragraphs in inside bolded content with content-model violation', + 'pad': '<b>{<p>foo</p><p>bar</p>}<p>baz</p></b>', + 'expected': [ '<p>[foo</p><p>bar]</p><p><b>baz</b></p>', + '<p>[foo</p><p>bar]</p><b><p>baz</p></b>' ] }, + + { 'id': 'B_B-P-I..P-1_SO-I', + 'desc': 'Unbolding italicized content inside bolded content with content-model violation', + 'pad': '<b><p>foo[<i>bar</i>]</p><p>baz</p></b>', + 'expected': [ '<p><b>foo</b><i>[bar]</i></p><p><b>baz</b></p>', + '<b><p>foo</p></b><p><i>[bar]</i></p><b><p>baz</p></b>' ] }, + + { 'id': 'B_B-2_SL', + 'desc': 'Remove partially covered bold, selection extends left', + 'pad': 'foo [bar <b>baz] qoz</b> quz sic', + 'expected': 'foo [bar baz]<b> qoz</b> quz sic' }, + + { 'id': 'B_B-2_SR', + 'desc': 'Remove partially covered bold, selection extends right', + 'pad': 'foo bar <b>baz [qoz</b> quz] sic', + 'expected': 'foo bar <b>baz </b>[qoz quz] sic' } + ] + }, + + { 'desc': 'remove italic', + 'command': 'italic', + 'tests': [ + { 'id': 'I_I-1_SW', + 'rte1-id': 'u-italic-0', + 'desc': 'Selection within tags; remove <i> tags', + 'pad': 'foo<i>[bar]</i>baz' }, + + { 'id': 'I_I-1_SO', + 'desc': 'Selection outside of tags; remove <i> tags', + 'pad': 'foo[<i>bar</i>]baz' }, + + { 'id': 'I_I-1_SL', + 'desc': 'Selection oblique left; remove <i> tags', + 'pad': 'foo[<i>bar]</i>baz' }, + + { 'id': 'I_I-1_SR', + 'desc': 'Selection oblique right; remove <i> tags', + 'pad': 'foo<i>[bar</i>]baz' }, + + { 'id': 'I_EM-1_SW', + 'rte1-id': 'u-italic-1', + 'desc': 'Selection within tags; remove <em> tags', + 'pad': 'foo<em>[bar]</em>baz' }, + + { 'id': 'I_EM-1_SO', + 'desc': 'Selection outside of tags; remove <em> tags', + 'pad': 'foo[<em>bar</em>]baz' }, + + { 'id': 'I_EM-1_SL', + 'desc': 'Selection oblique left; remove <em> tags', + 'pad': 'foo[<em>bar]</em>baz' }, + + { 'id': 'I_EM-1_SR', + 'desc': 'Selection oblique right; remove <em> tags', + 'pad': 'foo<em>[bar</em>]baz' }, + + { 'id': 'I_SPANs:fs:i-1_SW', + 'rte1-id': 'u-italic-2', + 'desc': 'Selection within tags; remove "font-style: italic"', + 'pad': 'foo<span style="font-style: italic">[bar]</span>baz' }, + + { 'id': 'I_SPANs:fs:i-1_SO', + 'desc': 'Selection outside of tags; Italicize "font-style: italic"', + 'pad': 'foo[<span style="font-style: italic">bar</span>]baz' }, + + { 'id': 'I_SPANs:fs:i-1_SL', + 'desc': 'Selection oblique left; Italicize "font-style: italic"', + 'pad': 'foo[<span style="font-style: italic">bar]</span>baz' }, + + { 'id': 'I_SPANs:fs:i-1_SR', + 'desc': 'Selection oblique right; Italicize "font-style: italic"', + 'pad': 'foo<span style="font-style: italic">[bar</span>]baz' }, + + { 'id': 'I_I-P3-1_SO2', + 'desc': 'Unitalicize content with content-model violation', + 'pad': '<i><p>foo</p>{<p>bar</p>}<p>baz</p></i>', + 'expected': [ '<p><i>foo</i></p><p>[bar]</p><p><i>baz</i></p>', + '<i><p>foo</p></i><p>[bar]</p><i><p>baz</p></i>' ] } + ] + }, + + { 'desc': 'remove underline', + 'command': 'underline', + 'tests': [ + { 'id': 'U_U-1_SW', + 'rte1-id': 'u-underline-0', + 'desc': 'Selection within tags; remove <u> tags', + 'pad': 'foo<u>[bar]</u>baz' }, + + { 'id': 'U_U-1_SO', + 'desc': 'Selection outside of tags; remove <u> tags', + 'pad': 'foo[<u>bar</u>]baz' }, + + { 'id': 'U_U-1_SL', + 'desc': 'Selection oblique left; remove <u> tags', + 'pad': 'foo[<u>bar]</u>baz' }, + + { 'id': 'U_U-1_SR', + 'desc': 'Selection oblique right; remove <u> tags', + 'pad': 'foo<u>[bar</u>]baz' }, + + { 'id': 'U_SPANs:td:u-1_SW', + 'rte1-id': 'u-underline-1', + 'desc': 'Selection within tags; remove "text-decoration: underline"', + 'pad': 'foo<span style="text-decoration: underline">[bar]</span>baz' }, + + { 'id': 'U_SPANs:td:u-1_SO', + 'desc': 'Selection outside of tags; remove "text-decoration: underline"', + 'pad': 'foo[<span style="text-decoration: underline">bar</span>]baz' }, + + { 'id': 'U_SPANs:td:u-1_SL', + 'desc': 'Selection oblique left; remove "text-decoration: underline"', + 'pad': 'foo[<span style="text-decoration: underline">bar]</span>baz' }, + + { 'id': 'U_SPANs:td:u-1_SR', + 'desc': 'Selection oblique right; remove "text-decoration: underline"', + 'pad': 'foo<span style="text-decoration: underline">[bar</span>]baz' }, + + { 'id': 'U_U-S-1_SO', + 'desc': 'Removing underline from underlined content with striked content', + 'pad': '<u>foo[bar<s>baz</s>quoz]</u>', + 'expected': '<u>foo</u>[bar<s>baz</s>quoz]' }, + + { 'id': 'U_U-S-2_SI', + 'desc': 'Removing underline from striked content inside underlined content', + 'pad': '<u><s>foo[bar]baz</s>quoz</u>', + 'expected': '<s><u>foo</u>[bar]<u>baz</u>quoz</s>' }, + + { 'id': 'U_U-P3-1_SO', + 'desc': 'Removing underline from underlined content with content-model violation', + 'pad': '<u><p>foo</p>{<p>bar</p>}<p>baz</p></u>', + 'expected': [ '<p><u>foo</u></p><p>[bar]</p><p><u>baz</u></p>', + '<u><p>foo</p></u><p>[bar]</p><u><p>baz</p></u>' ] } + ] + }, + + { 'desc': 'remove strike through', + 'command': 'strikethrough', + 'tests': [ + { 'id': 'S_S-1_SW', + 'rte1-id': 'u-strikethrough-1', + 'desc': 'Selection within tags; remove <s> tags', + 'pad': 'foo<s>[bar]</s>baz' }, + + { 'id': 'S_S-1_SO', + 'desc': 'Selection outside of tags; remove <s> tags', + 'pad': 'foo[<s>bar</s>]baz' }, + + { 'id': 'S_S-1_SL', + 'desc': 'Selection oblique left; remove <s> tags', + 'pad': 'foo[<s>bar]</s>baz' }, + + { 'id': 'S_S-1_SR', + 'desc': 'Selection oblique right; remove <s> tags', + 'pad': 'foo<s>[bar</s>]baz' }, + + { 'id': 'S_STRIKE-1_SW', + 'rte1-id': 'u-strikethrough-0', + 'desc': 'Selection within tags; remove <strike> tags', + 'pad': 'foo<strike>[bar]</strike>baz' }, + + { 'id': 'S_STRIKE-1_SO', + 'desc': 'Selection outside of tags; remove <strike> tags', + 'pad': 'foo[<strike>bar</strike>]baz' }, + + { 'id': 'S_STRIKE-1_SL', + 'desc': 'Selection oblique left; remove <strike> tags', + 'pad': 'foo[<strike>bar]</strike>baz' }, + + { 'id': 'S_STRIKE-2_SR', + 'desc': 'Selection oblique right; remove <strike> tags', + 'pad': 'foo<strike>[bar</strike>]baz' }, + + { 'id': 'S_DEL-1_SW', + 'rte1-id': 'u-strikethrough-2', + 'desc': 'Selection within tags; remove <del> tags', + 'pad': 'foo<del>[bar]</del>baz' }, + + { 'id': 'S_SPANs:td:lt-1_SW', + 'rte1-id': 'u-strikethrough-3', + 'desc': 'Selection within tags; remove "text-decoration:line-through"', + 'pad': 'foo<span style="text-decoration:line-through">[bar]</span>baz' }, + + { 'id': 'S_SPANs:td:lt-1_SO', + 'desc': 'Selection outside of tags; Italicize "text-decoration:line-through"', + 'pad': 'foo[<span style="text-decoration:line-through">bar</span>]baz' }, + + { 'id': 'S_SPANs:td:lt-1_SL', + 'desc': 'Selection oblique left; Italicize "text-decoration:line-through"', + 'pad': 'foo[<span style="text-decoration:line-through">bar]</span>baz' }, + + { 'id': 'S_SPANs:td:lt-1_SR', + 'desc': 'Selection oblique right; Italicize "text-decoration:line-through"', + 'pad': 'foo<span style="text-decoration:line-through">[bar</span>]baz' }, + + { 'id': 'S_S-U-1_SI', + 'desc': 'Removing underline from underlined content inside striked content', + 'pad': '<s><u>foo[bar]baz</u>quoz</s>', + 'expected': '<s><u>foo</u></s><u>[bar]</u><s><u>baz</u>quoz</s>' }, + + { 'id': 'S_U-S-1_SI', + 'desc': 'Removing underline from striked content inside underlined content', + 'pad': '<u><s>foo[bar]baz</s>quoz</u>', + 'expected': '<u><s>foo</s>[bar]<s>baz</s>quoz</u>' } + ] + }, + + { 'desc': 'remove subscript', + 'command': 'subscript', + 'tests': [ + { 'id': 'SUB_SUB-1_SW', + 'rte1-id': 'u-subscript-0', + 'desc': 'remove subscript', + 'pad': 'foo<sub>[bar]</sub>baz' }, + + { 'id': 'SUB_SPANs:va:sub-1_SW', + 'rte1-id': 'u-subscript-1', + 'desc': 'remove subscript', + 'pad': 'foo<span style="vertical-align: sub">[bar]</span>baz' } + ] + }, + + { 'desc': 'remove superscript', + 'command': 'superscript', + 'tests': [ + { 'id': 'SUP_SUP-1_SW', + 'rte1-id': 'u-superscript-0', + 'desc': 'remove superscript', + 'pad': 'foo<sup>[bar]</sup>baz' }, + + { 'id': 'SUP_SPANs:va:super-1_SW', + 'rte1-id': 'u-superscript-1', + 'desc': 'remove superscript', + 'pad': 'foo<span style="vertical-align: super">[bar]</span>baz' } + ] + }, + + { 'desc': 'remove links', + 'command': 'unlink', + 'tests': [ + { 'id': 'UNLINK_Ahref:url-1_SW', + 'rte1-id': 'u-unlink-0', + 'desc': 'unlink an <a> element with href attribute where all children are selected', + 'pad': 'foo<a href="http://www.goo.gl">[bar]</a>baz' }, + + { 'id': 'UNLINK_A-1_SC', + 'desc': 'unlink an <a> element that contains the collapsed selection', + 'pad': 'foo<a>ba^r</a>baz', + 'expected': 'fooba^rbaz' }, + + { 'id': 'UNLINK_A-1_SI', + 'desc': 'unlink an <a> element that contains the whole selection', + 'pad': 'foo<a>b[a]r</a>baz', + 'expected': 'foob[a]rbaz' }, + + { 'id': 'UNLINK_A-2_SL', + 'desc': 'unlink a partially contained <a> element', + 'pad': 'foo[ba<a>r]ba</a>z' }, + + { 'id': 'UNLINK_A-3_SR', + 'desc': 'unlink a partially contained <a> element', + 'pad': 'fo<a>o[ba</a>r]baz' }, + + { 'id': 'UNLINK_As:d:b.fw:b-1_SW', + 'desc': 'unlink, preserving styles', + 'pad': 'foo<a href="#" style="display: block; font-weight: bold">[bar]</a>baz', + 'expected': 'foo<span style="display: block; font-weight: bold">[bar]</span>baz' }, + + { 'id': 'UNLINK_A-IMG-1_SO', + 'desc': 'unlink a linked image at the start of the content', + 'pad': '{<a href="#"><img src="pic.jpg" align="right" height="140" width="200"></a>abc]', + 'expected': '{<img src="pic.jpg" align="right" height="140" width="200">abc]' } + ] + }, + + { 'desc': 'outdent', + 'command': 'outdent', + 'tests': [ + { 'id': 'OUTDENT_BQ-1_SW', + 'rte1-id': 'u-outdent-0', + 'desc': 'outdent (remove) a <blockquote>', + 'pad': 'foo<blockquote>[bar]</blockquote>baz', + 'expected': [ 'foo<p>[bar]</p>baz', + 'foo<div>[bar]</div>baz' ], + 'accept': 'foo<br>[bar]<br>baz' }, + + { 'id': 'OUTDENT_BQ.wibq.s:m:00040.b:n.p:0-1_SW', + 'rte1-id': 'u-outdent-1', + 'desc': 'outdent (remove) a styled <blockquote>', + 'pad': 'foo<blockquote class="webkit-indent-blockquote" style="margin: 0 0 0 40px; border: none; padding: 0px">[bar]</blockquote>baz', + 'expected': [ 'foo<p>[bar]</p>baz', + 'foo<div>[bar]</div>baz' ], + 'accept': 'foo<br>[bar]<br>baz' }, + + { 'id': 'OUTDENT_OL-LI-1_SW', + 'rte1-id': 'u-outdent-3', + 'desc': 'outdent (remove) an ordered list', + 'pad': 'foo<ol><li>[bar]</li></ol>baz', + 'expected': [ 'foo<p>[bar]</p>baz', + 'foo<div>[bar]</div>baz' ], + 'accept': 'foo<br>[bar]<br>baz' }, + + { 'id': 'OUTDENT_UL-LI-1_SW', + 'rte1-id': 'u-outdent-2', + 'desc': 'outdent (remove) an unordered list', + 'pad': 'foo<ul><li>[bar]</li></ul>baz', + 'expected': [ 'foo<p>[bar]</p>baz', + 'foo<div>[bar]</div>baz' ], + 'accept': 'foo<br>[bar]<br>baz' }, + + { 'id': 'OUTDENT_DIV-1_SW', + 'rte1-id': 'u-outdent-4', + 'desc': 'outdent (remove) a styled <div> with margin', + 'pad': 'foo<div style="margin-left: 40px;">[bar]</div>baz', + 'expected': [ 'foo<p>[bar]</p>baz', + 'foo<div>[bar]</div>baz' ], + 'accept': 'foo<br>[bar]<br>baz' } + ] + }, + + { 'desc': 'remove all formatting', + 'command': 'removeformat', + 'tests': [ + { 'id': 'REMOVEFORMAT_B-1_SW', + 'rte1-id': 'u-removeformat-0', + 'desc': 'remove a <b> tag using "removeformat"', + 'pad': 'foo<b>[bar]</b>baz' }, + + { 'id': 'REMOVEFORMAT_Ahref:url-1_SW', + 'rte1-id': 'u-removeformat-0', + 'desc': 'remove a link using "removeformat"', + 'pad': 'foo<a href="http://www.goo.gl">[bar]</a>baz' }, + + { 'id': 'REMOVEFORMAT_TABLE-TBODY-TR-TD-1_SW', + 'rte1-id': 'u-removeformat-2', + 'desc': 'remove a table using "removeformat"', + 'pad': 'foo<table><tbody><tr><td>[bar]</td></tr></tbody></table>baz', + 'expected': [ 'foo<p>[bar]</p>baz', + 'foo<div>[bar]</div>baz' ], + 'accept': 'foo<br>[bar]<br>baz' } + ] + }, + + { 'desc': 'remove bookmark', + 'command': 'unbookmark', + 'tests': [ + { 'id': 'UNBOOKMARK_An:name-1_SW', + 'rte1-id': 'u-unbookmark-0', + 'desc': 'unlink a bookmark (a named <a> element) where all children are selected', + 'pad': 'foo<a name="bookmark">[bar]</a>baz' } + ] + } + ] +} diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/unapplyCSS.py b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/unapplyCSS.py new file mode 100644 index 0000000000..6f934a0f02 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/tests/unapplyCSS.py @@ -0,0 +1,226 @@ + +UNAPPLY_TESTS_CSS = { + 'id': 'UC', + 'caption': 'Unapply Existing Formatting Tests, using styleWithCSS', + 'checkAttrs': True, + 'checkStyle': True, + 'styleWithCSS': True, + 'expected': 'foo[bar]baz', + + 'Proposed': [ + { 'desc': '', + 'id': '', + 'command': '', + 'tests': [ + ] + }, + + { 'desc': 'remove bold', + 'command': 'bold', + 'tests': [ + { 'id': 'B_B-1_SW', + 'desc': 'Selection within tags; remove <b> tags', + 'pad': 'foo<b>[bar]</b>baz' }, + + { 'id': 'B_B-1_SO', + 'desc': 'Selection outside of tags; remove <b> tags', + 'pad': 'foo[<b>bar</b>]baz' }, + + { 'id': 'B_B-1_SL', + 'desc': 'Selection oblique left; remove <b> tags', + 'pad': 'foo[<b>bar]</b>baz' }, + + { 'id': 'B_B-1_SR', + 'desc': 'Selection oblique right; remove <b> tags', + 'pad': 'foo<b>[bar</b>]baz' }, + + { 'id': 'B_STRONG-1_SW', + 'desc': 'Selection within tags; remove <strong> tags', + 'pad': 'foo<strong>[bar]</strong>baz' }, + + { 'id': 'B_STRONG-1_SO', + 'desc': 'Selection outside of tags; remove <strong> tags', + 'pad': 'foo[<strong>bar</strong>]baz' }, + + { 'id': 'B_STRONG-1_SL', + 'desc': 'Selection oblique left; remove <strong> tags', + 'pad': 'foo[<strong>bar]</strong>baz' }, + + { 'id': 'B_STRONG-1_SR', + 'desc': 'Selection oblique right; remove <strong> tags', + 'pad': 'foo<strong>[bar</strong>]baz' }, + + { 'id': 'B_SPANs:fw:b-1_SW', + 'desc': 'Selection within tags; remove "font-weight: bold"', + 'pad': 'foo<span style="font-weight: bold">[bar]</span>baz' }, + + { 'id': 'B_SPANs:fw:b-1_SO', + 'desc': 'Selection outside of tags; remove "font-weight: bold"', + 'pad': 'foo[<span style="font-weight: bold">bar</span>]baz' }, + + { 'id': 'B_SPANs:fw:b-1_SL', + 'desc': 'Selection oblique left; remove "font-weight: bold"', + 'pad': 'foo[<span style="font-weight: bold">bar]</span>baz' }, + + { 'id': 'B_SPANs:fw:b-1_SR', + 'desc': 'Selection oblique right; remove "font-weight: bold"', + 'pad': 'foo<span style="font-weight: bold">[bar</span>]baz' } + ] + }, + + { 'desc': 'remove italic', + 'command': 'italic', + 'tests': [ + { 'id': 'I_I-1_SW', + 'desc': 'Selection within tags; remove <i> tags', + 'pad': 'foo<i>[bar]</i>baz' }, + + { 'id': 'I_I-1_SO', + 'desc': 'Selection outside of tags; remove <i> tags', + 'pad': 'foo[<i>bar</i>]baz' }, + + { 'id': 'I_I-1_SL', + 'desc': 'Selection oblique left; remove <i> tags', + 'pad': 'foo[<i>bar]</i>baz' }, + + { 'id': 'I_I-1_SR', + 'desc': 'Selection oblique right; remove <i> tags', + 'pad': 'foo<i>[bar</i>]baz' }, + + { 'id': 'I_EM-1_SW', + 'desc': 'Selection within tags; remove <em> tags', + 'pad': 'foo<em>[bar]</em>baz' }, + + { 'id': 'I_EM-1_SO', + 'desc': 'Selection outside of tags; remove <em> tags', + 'pad': 'foo[<em>bar</em>]baz' }, + + { 'id': 'I_EM-1_SL', + 'desc': 'Selection oblique left; remove <em> tags', + 'pad': 'foo[<em>bar]</em>baz' }, + + { 'id': 'I_EM-1_SR', + 'desc': 'Selection oblique right; remove <em> tags', + 'pad': 'foo<em>[bar</em>]baz' }, + + { 'id': 'I_SPANs:fs:i-1_SW', + 'desc': 'Selection within tags; remove "font-style: italic"', + 'pad': 'foo<span style="font-style: italic">[bar]</span>baz' }, + + { 'id': 'I_SPANs:fs:i-1_SO', + 'desc': 'Selection outside of tags; Italicize "font-style: italic"', + 'pad': 'foo[<span style="font-style: italic">bar</span>]baz' }, + + { 'id': 'I_SPANs:fs:i-1_SL', + 'desc': 'Selection oblique left; Italicize "font-style: italic"', + 'pad': 'foo[<span style="font-style: italic">bar]</span>baz' }, + + { 'id': 'I_SPANs:fs:i-1_SR', + 'desc': 'Selection oblique right; Italicize "font-style: italic"', + 'pad': 'foo<span style="font-style: italic">[bar</span>]baz' } + ] + }, + + { 'desc': 'remove underline', + 'command': 'underline', + 'tests': [ + { 'id': 'U_U-1_SW', + 'desc': 'Selection within tags; remove <u> tags', + 'pad': 'foo<u>[bar]</u>baz' }, + + { 'id': 'U_U-1_SO', + 'desc': 'Selection outside of tags; remove <u> tags', + 'pad': 'foo[<u>bar</u>]baz' }, + + { 'id': 'U_U-1_SL', + 'desc': 'Selection oblique left; remove <u> tags', + 'pad': 'foo[<u>bar]</u>baz' }, + + { 'id': 'U_U-1_SR', + 'desc': 'Selection oblique right; remove <u> tags', + 'pad': 'foo<u>[bar</u>]baz' }, + + { 'id': 'U_SPANs:td:u-1_SW', + 'desc': 'Selection within tags; remove "text-decoration: underline"', + 'pad': 'foo<span style="text-decoration: underline">[bar]</span>baz' }, + + { 'id': 'U_SPANs:td:u-1_SO', + 'desc': 'Selection outside of tags; remove "text-decoration: underline"', + 'pad': 'foo[<span style="text-decoration: underline">bar</span>]baz' }, + + { 'id': 'U_SPANs:td:u-1_SL', + 'desc': 'Selection oblique left; remove "text-decoration: underline"', + 'pad': 'foo[<span style="text-decoration: underline">bar]</span>baz' }, + + { 'id': 'U_SPANs:td:u-1_SR', + 'desc': 'Selection oblique right; remove "text-decoration: underline"', + 'pad': 'foo<span style="text-decoration: underline">[bar</span>]baz' } + ] + }, + + { 'desc': 'remove strike-through', + 'command': 'strikethrough', + 'tests': [ + { 'id': 'S_S-1_SW', + 'desc': 'Selection within tags; remove <s> tags', + 'pad': 'foo<s>[bar]</s>baz' }, + + { 'id': 'S_S-1_SO', + 'desc': 'Selection outside of tags; remove <s> tags', + 'pad': 'foo[<s>bar</s>]baz' }, + + { 'id': 'S_S-1_SL', + 'desc': 'Selection oblique left; remove <s> tags', + 'pad': 'foo[<s>bar]</s>baz' }, + + { 'id': 'S_S-1_SR', + 'desc': 'Selection oblique right; remove <s> tags', + 'pad': 'foo<s>[bar</s>]baz' }, + + { 'id': 'S_STRIKE-1_SW', + 'desc': 'Selection within tags; remove <strike> tags', + 'pad': 'foo<strike>[bar]</strike>baz' }, + + { 'id': 'S_STRIKE-1_SO', + 'desc': 'Selection outside of tags; remove <strike> tags', + 'pad': 'foo[<strike>bar</strike>]baz' }, + + { 'id': 'S_STRIKE-1_SL', + 'desc': 'Selection oblique left; remove <strike> tags', + 'pad': 'foo[<strike>bar]</strike>baz' }, + + { 'id': 'S_STRIKE-1_SR', + 'desc': 'Selection oblique right; remove <strike> tags', + 'pad': 'foo<strike>[bar</strike>]baz' }, + + { 'id': 'S_SPANs:td:lt-1_SW', + 'desc': 'Selection within tags; remove "text-decoration:line-through"', + 'pad': 'foo<span style="text-decoration:line-through">[bar]</span>baz' }, + + { 'id': 'S_SPANs:td:lt-1_SO', + 'desc': 'Selection outside of tags; Italicize "text-decoration:line-through"', + 'pad': 'foo[<span style="text-decoration:line-through">bar</span>]baz' }, + + { 'id': 'S_SPANs:td:lt-1_SL', + 'desc': 'Selection oblique left; Italicize "text-decoration:line-through"', + 'pad': 'foo[<span style="text-decoration:line-through">bar]</span>baz' }, + + { 'id': 'S_SPANs:td:lt-1_SR', + 'desc': 'Selection oblique right; Italicize "text-decoration:line-through"', + 'pad': 'foo<span style="text-decoration:line-through">[bar</span>]baz' }, + + { 'id': 'S_SPANc:s-1_SW', + 'desc': 'Unapply "strike-through" on interited CSS style', + 'checkClass': True, + 'pad': 'foo<span class="s">[bar]</span>baz' }, + + { 'id': 'S_SPANc:s-2_SI', + 'desc': 'Unapply "strike-through" on interited CSS style', + 'pad': '<span class="s">foo[bar]baz</span>', + 'checkClass': True, + 'expected': '<span class="s">foo</span>[bar]<span class="s">baz</span>' } + ] + } + ] +} + diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/unittestexample.html b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/unittestexample.html new file mode 100644 index 0000000000..4e27b05540 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/richtext2/unittestexample.html @@ -0,0 +1,103 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta http-equiv="content-type" content="text/html; charset=utf-8" /> + <meta http-equiv="X-UA-Compatible" content="IE=edge" /> + + <title>Rich Text 2 Unit Test Example</title> + + <!-- utility scripts --> + <script type="text/javascript" src="static/js/variables.js"></script> + <script type="text/javascript" src="static/js/canonicalize.js"></script> + <script type="text/javascript" src="static/js/compare.js"></script> + <script type="text/javascript" src="static/js/pad.js"></script> + <script type="text/javascript" src="static/js/range.js"></script> + <script type="text/javascript" src="static/js/units.js"></script> + <script type="text/javascript" src="static/js/run.js"></script> + <!-- you do not need static/js/output.js --> + + <!-- + Tests - note that those have the extensions .py, + but can be used as JS files directly. + --> + <script type="text/javascript" src="tests/selection.py"></script> + <script type="text/javascript" src="tests/apply.py"></script> + <script type="text/javascript" src="tests/applyCSS.py"></script> + <script type="text/javascript" src="tests/change.py"></script> + <script type="text/javascript" src="tests/changeCSS.py"></script> + <script type="text/javascript" src="tests/unapply.py"></script> + <script type="text/javascript" src="tests/unapplyCSS.py"></script> + <script type="text/javascript" src="tests/delete.py"></script> + <script type="text/javascript" src="tests/forwarddelete.py"></script> + <script type="text/javascript" src="tests/insert.py"></script> + <script type="text/javascript" src="tests/querySupported.py"></script> + <script type="text/javascript" src="tests/queryEnabled.py"></script> + <script type="text/javascript" src="tests/queryIndeterm.py"></script> + <script type="text/javascript" src="tests/queryState.py"></script> + <script type="text/javascript" src="tests/queryValue.py"></script> + + <!-- Do something --> + <script type="text/javascript"> + function runTest() { + initVariables(); + initEditorDocs(); + + runTestSuite(UNAPPLY_TESTS); + + // Below alert is just a simple demonstration on how to access the test results. + // Note that we only ran UNAPPLY tests above, so we have only results from that test set. + // + // The 'results' structure is as follows: + // + // results structure containing all results + // [<suite ID>] structure containing the results for the given suite *) + // .count number of tests in the given suite + // .valscore sum of all test value results (HTML or query value) + // .selscore sum of all selection results (HTML tests only) + // [<class ID>] structure containing the results for the given class **) + // .count number of tests in the given suite + // .valscore sum of all test value results (HTML or query value) + // .selscore sum of all selection results (HTML tests only) + // [<test ID>] structure containing the reults for a given test ***) + // .valscore value score (0 or 1), minimum over all containers + // .selscore selection score (0 or 1), minimum over all containers (HTML tests only) + // .valresult worst test value result (integer, see variables.js) + // .selresult worst selection result (integer, see variables.js) + // [<cont. ID>] structure containing the results of the test for a given container ****) + // .valscore value score (0 or 1) + // .selscore selection score (0 or 1) + // .valresult value result (integer, see variables.js) + // .selresult selection result (integer, see variables.js) + // .output output string (mainly for use by the online version) + // .innerHTML inner HTML of the testing container (<div> or <body>) after the test + // .outerHTML outer HTML of the testing container (<div> or <body>) after the test + // .bodyInnerHTML inner HTML of the <body> after the test + // .bodyOuterHTML outer HTML of the <body> after the test + // + // *) <suite ID>: a 1-3 character ID, e.g. UNAPPLY_TESTS.id, or 'U' (both referring the same suite) + // **) <class ID>: one of 'Proposed', 'RFC' or 'Finalized' + // ***) <test ID>: the ID of the test, without the leading 'RTE2-<suite ID>_' part + // ****) <container ID>: one of 'div' (test within a <div contenteditable="true">) + // 'dM' (test with designMode = 'on') + // 'body' (test within a <body contenteditable="true">) + + alert("Result of 'Apply' tests:\nOut of " + + results[UNAPPLY_TESTS.id].count + " tests\n" + + results[UNAPPLY_TESTS.id].valscore + " had correct HTML, and\n" + + results[UNAPPLY_TESTS.id].selscore + " had a correct result selection\n(in all testing containers)." + + "\n\n" + + "Test RTE2-U_B_B-1_SW results with a contenteditable <body>:\n" + + results['U']['Proposed']['B_B-1_SW']['body'].valscore + " points for the value result, and\n" + + results['U']['Proposed']['B_B-1_SW']['body'].selscore + " points for the selection" + + "" + ); + } + </script> +</head> + +<body onload="runTest()"> + <iframe name="iframe-dM" id="iframe-dM" src="static/editable-dM.html"></iframe> + <iframe name="iframe-body" id="iframe-body" src="static/editable-body.html"></iframe> + <iframe name="iframe-div" id="iframe-div" src="static/editable-div.html"></iframe> +</body> +</html> diff --git a/editor/libeditor/tests/browserscope/lib/richtext2/update_from_upstream b/editor/libeditor/tests/browserscope/lib/richtext2/update_from_upstream new file mode 100644 index 0000000000..baeb767454 --- /dev/null +++ b/editor/libeditor/tests/browserscope/lib/richtext2/update_from_upstream @@ -0,0 +1,19 @@ +#!/bin/sh + +set -x + +if test -d richtext2; then + rm -drf richtext2; +fi + +svn checkout http://browserscope.googlecode.com/svn/trunk/categories/richtext2 richtext2 | tail -1 | sed 's/[^0-9]//g' > current_revision + +find richtext2 -type d -name .svn -exec rm -drf \{\} \; 2> /dev/null + +# Remove test_set.py and other similarly named files because they confuse our mochitest runner +find richtext2 =type f -name test_\* -exec rm -rf \{\} \; 2> /dev/null + +hg add current_revision richtext2 + +hg stat . + diff --git a/editor/libeditor/tests/browserscope/mochitest.toml b/editor/libeditor/tests/browserscope/mochitest.toml new file mode 100644 index 0000000000..a2db4d4863 --- /dev/null +++ b/editor/libeditor/tests/browserscope/mochitest.toml @@ -0,0 +1,60 @@ +[DEFAULT] +support-files = [ + "lib/richtext2/current_revision", + "lib/richtext2/richtext2/common.py", + "lib/richtext2/richtext2/unittestexample.html", + "lib/richtext2/richtext2/static/editable-dM.html", + "lib/richtext2/richtext2/static/editable.css", + "lib/richtext2/richtext2/static/editable-body.html", + "lib/richtext2/richtext2/static/editable-div.html", + "lib/richtext2/richtext2/static/js/variables.js", + "lib/richtext2/richtext2/static/js/range-bootstrap.js", + "lib/richtext2/richtext2/static/js/range.js", + "lib/richtext2/richtext2/static/js/output.js", + "lib/richtext2/richtext2/static/js/compare.js", + "lib/richtext2/richtext2/static/js/canonicalize.js", + "lib/richtext2/richtext2/static/js/pad.js", + "lib/richtext2/richtext2/static/js/run.js", + "lib/richtext2/richtext2/static/js/units.js", + "lib/richtext2/richtext2/static/common.css", + "lib/richtext2/richtext2/__init__.py", + "lib/richtext2/richtext2/handlers.py", + "lib/richtext2/richtext2/templates/output.html", + "lib/richtext2/richtext2/templates/richtext2.html", + "lib/richtext2/richtext2/tests/forwarddelete.py", + "lib/richtext2/richtext2/tests/selection.py", + "lib/richtext2/richtext2/tests/queryIndeterm.py", + "lib/richtext2/richtext2/tests/unapplyCSS.py", + "lib/richtext2/richtext2/tests/apply.py", + "lib/richtext2/richtext2/tests/unapply.py", + "lib/richtext2/richtext2/tests/change.py", + "lib/richtext2/richtext2/tests/queryState.py", + "lib/richtext2/richtext2/tests/queryValue.py", + "lib/richtext2/richtext2/tests/__init__.py", + "lib/richtext2/richtext2/tests/insert.py", + "lib/richtext2/richtext2/tests/queryEnabled.py", + "lib/richtext2/richtext2/tests/applyCSS.py", + "lib/richtext2/richtext2/tests/changeCSS.py", + "lib/richtext2/richtext2/tests/delete.py", + "lib/richtext2/richtext2/tests/querySupported.py", + "lib/richtext2/README", + "lib/richtext2/update_from_upstream", + "lib/richtext2/LICENSE", + "lib/richtext2/README.Mozilla", + "lib/richtext2/currentStatus.js", + "lib/richtext2/platformFailures.js", + "lib/richtext/current_revision", + "lib/richtext/README", + "lib/richtext/update_from_upstream", + "lib/richtext/LICENSE", + "lib/richtext/README.Mozilla", + "lib/richtext/richtext/editable.html", + "lib/richtext/richtext/richtext.html", + "lib/richtext/richtext/js/range.js", + "lib/richtext/currentStatus.js", +] + +["test_richtext.html"] + +["test_richtext2.html"] +skip-if = ["os == 'android'"] # Bug 1202045 diff --git a/editor/libeditor/tests/browserscope/test_richtext.html b/editor/libeditor/tests/browserscope/test_richtext.html new file mode 100644 index 0000000000..c07f0a366a --- /dev/null +++ b/editor/libeditor/tests/browserscope/test_richtext.html @@ -0,0 +1,48 @@ +<!DOCTYPE HTML> +<html> +<!-- +BrowserScope richtext category tests +--> +<head> + <title>BrowserScope Richtext Tests</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="lib/richtext/currentStatus.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=550569">Mozilla Bug 550569</a> +<p id="display"></p> +<div id="content"> + <iframe src="lib/richtext/richtext/richtext.html"></iframe> +</div> +<pre id="test"> +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +// Running all of the tests can take a long time, try to account for it +SimpleTest.requestLongerTimeout(5); + +function sendScore(results, continueParams) { + ok(results.length > 1, "At least one test should have been run"); + for (var i = 1; i < results.length; ++i) { + var result = results[i]; + let [type, command, param, success] = result.split(/[\-=]/); + var comp = is; + if (isKnownFailure(type, command, param)) { + comp = todo_is; + } + comp(success, "1", "Browserscope richtext category=" + type + + " test=" + command + + " param=" + param); + } +} + +document.getElementsByTagName("iframe")[0].addEventListener("load", function() { + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/browserscope/test_richtext2.html b/editor/libeditor/tests/browserscope/test_richtext2.html new file mode 100644 index 0000000000..70aa74d802 --- /dev/null +++ b/editor/libeditor/tests/browserscope/test_richtext2.html @@ -0,0 +1,238 @@ +<!DOCTYPE html> +<html lang="en"> +<!-- +BrowserScope richtext2 category tests + +This test is originally based on the unit test example available as part of the +RichText2 suite: +http://code.google.com/p/browserscope/source/browse/trunk/categories/richtext2/unittestexample.html +--> +<head> + <meta http-equiv="content-type" content="text/html; charset=utf-8" /> + <meta http-equiv="X-UA-Compatible" content="IE=edge" /> + + <title>BrowserScope Richtext2 Tests</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> + + <!-- utility scripts --> + <script type="text/javascript" src="lib/richtext2/richtext2/static/js/variables.js"></script> + <script type="text/javascript" src="lib/richtext2/richtext2/static/js/canonicalize.js"></script> + <script type="text/javascript" src="lib/richtext2/richtext2/static/js/compare.js"></script> + <script type="text/javascript" src="lib/richtext2/richtext2/static/js/pad.js"></script> + <script type="text/javascript" src="lib/richtext2/richtext2/static/js/range.js"></script> + <script type="text/javascript" src="lib/richtext2/richtext2/static/js/units.js"></script> + <script type="text/javascript" src="lib/richtext2/richtext2/static/js/run.js"></script> + <!-- you do not need static/js/output.js --> + + <!-- + Tests - note that those have the extensions .py, + but can be used as JS files directly. + --> + <script type="text/javascript" src="lib/richtext2/richtext2/tests/selection.py"></script> + <script type="text/javascript" src="lib/richtext2/richtext2/tests/apply.py"></script> + <script type="text/javascript" src="lib/richtext2/richtext2/tests/applyCSS.py"></script> + <script type="text/javascript" src="lib/richtext2/richtext2/tests/change.py"></script> + <script type="text/javascript" src="lib/richtext2/richtext2/tests/changeCSS.py"></script> + <script type="text/javascript" src="lib/richtext2/richtext2/tests/unapply.py"></script> + <script type="text/javascript" src="lib/richtext2/richtext2/tests/unapplyCSS.py"></script> + <script type="text/javascript" src="lib/richtext2/richtext2/tests/delete.py"></script> + <script type="text/javascript" src="lib/richtext2/richtext2/tests/forwarddelete.py"></script> + <script type="text/javascript" src="lib/richtext2/richtext2/tests/insert.py"></script> + <script type="text/javascript" src="lib/richtext2/richtext2/tests/querySupported.py"></script> + <script type="text/javascript" src="lib/richtext2/richtext2/tests/queryEnabled.py"></script> + <script type="text/javascript" src="lib/richtext2/richtext2/tests/queryIndeterm.py"></script> + <script type="text/javascript" src="lib/richtext2/richtext2/tests/queryState.py"></script> + <script type="text/javascript" src="lib/richtext2/richtext2/tests/queryValue.py"></script> + + <script type="text/javascript" src="lib/richtext2/currentStatus.js"></script> + <script type="text/javascript" src="lib/richtext2/platformFailures.js"></script> + + <!-- Do something --> + <script type="text/javascript"> + // Set this constant to true in order to get the current status of the test suite. + // This is useful for updating the currentStatus.js file when an editor bug is fixed. + const UPDATE_TEST_RESULTS = false; + + // some tests (at least RTE2-QE_PASTE_TEXT-1) require clipboard data + function startTest() { + SimpleTest.waitForClipboard("foo", + function() { + SpecialPowers.clipboardCopyString("foo"); + }, + runTest, + function() { + ok(false, "Failed to copy a string to the clipboard"); + SimpleTest.finish(); + } + ); + } + + /* eslint-disable-next-line complexity */ + function runTest() { + initVariables(); + initEditorDocs(); + + // These are all globals in the js and py files above */ + /* eslint-disable no-undef */ + const tests = [ + SELECTION_TESTS, + APPLY_TESTS, + APPLY_TESTS_CSS, + CHANGE_TESTS, + CHANGE_TESTS_CSS, + UNAPPLY_TESTS, + UNAPPLY_TESTS_CSS, + DELETE_TESTS, + FORWARDDELETE_TESTS, + INSERT_TESTS, + QUERYSUPPORTED_TESTS, + QUERYENABLED_TESTS, + QUERYINDETERM_TESTS, + QUERYSTATE_TESTS, + QUERYVALUE_TESTS, + ]; + /* eslint-enable no-undef */ + + for (let i = 0; i < tests.length; ++i) { + runTestSuite(tests[i]); + } + + // Below alert is just a simple demonstration on how to access the test results. + // Note that we only ran UNAPPLY tests above, so we have only results from that test set. + // + // The 'results' structure is as follows: + // + // results structure containing all results + // [<suite ID>] structure containing the results for the given suite *) + // .count number of tests in the given suite + // .valscore sum of all test value results (HTML or query value) + // .selscore sum of all selection results (HTML tests only) + // [<class ID>] structure containing the results for the given class **) + // .count number of tests in the given suite + // .valscore sum of all test value results (HTML or query value) + // .selscore sum of all selection results (HTML tests only) + // [<test ID>] structure containing the reults for a given test ***) + // .valscore value score (0 or 1), minimum over all containers + // .selscore selection score (0 or 1), minimum over all containers (HTML tests only) + // .valresult worst test value result (integer, see variables.js) + // .selresult worst selection result (integer, see variables.js) + // [<cont. ID>] structure containing the results of the test for a given container ****) + // .valscore value score (0 or 1) + // .selscore selection score (0 or 1) + // .valresult value result (integer, see variables.js) + // .selresult selection result (integer, see variables.js) + // .output output string (mainly for use by the online version) + // .innerHTML inner HTML of the testing container (<div> or <body>) after the test + // .outerHTML outer HTML of the testing container (<div> or <body>) after the test + // .bodyInnerHTML inner HTML of the <body> after the test + // .bodyOuterHTML outer HTML of the <body> after the test + // + // *) <suite ID>: a 1-3 character ID, e.g. UNAPPLY_TESTS.id, or 'U' (both referring the same suite) + // **) <class ID>: one of 'Proposed', 'RFC' or 'Finalized' + // ***) <test ID>: the ID of the test, without the leading 'RTE2-<suite ID>_' part + // ****) <container ID>: one of 'div' (test within a <div contenteditable="true">) + // 'dM' (test with designMode = 'on') + // 'body' (test within a <body contenteditable="true">) + + if (UPDATE_TEST_RESULTS) { + let newKnownFailures = {value: {}, select: {}}; + for (let i = 0; i < tests.length; ++i) { + let category = tests[i]; + for (let group in results[category.id]) { + switch (group) { + // Skip the known properties + case "count": + case "valscore": + case "selscore": + case "time": + break; + default: + for (let test_id in results[category.id][group]) { + switch (test_id) { + // Skip the known properties + case "count": + case "valscore": + case "selscore": + break; + default: + for (let structure in results[category.id][group][test_id]) { + switch (structure) { + // Only look at each test structure + case "dM": + case "body": + case "div": + if (!results[category.id][group][test_id][structure].valscore) { + newKnownFailures.value[category.id + "-" + group + "-" + test_id + "-" + structure] = true; + } + if (!results[category.id][group][test_id][structure].selscore) { + newKnownFailures.select[category.id + "-" + group + "-" + test_id + "-" + structure] = true; + } + } + } + } + } + } + } + } + var resultContainer = document.getElementById("results"); + resultContainer.style.display = ""; + resultContainer.textContent = JSON.stringify(newKnownFailures); + } else { + for (let i = 0; i < tests.length; ++i) { + let category = tests[i]; + for (let group in results[category.id]) { + switch (group) { + // Skip the known properties + case "count": + case "valscore": + case "selscore": + case "time": + break; + default: + for (let test_id in results[category.id][group]) { + switch (test_id) { + // Skip the known properties + case "count": + case "valscore": + case "selscore": + break; + default: + for (let structure in results[category.id][group][test_id]) { + switch (structure) { + // Only look at each test structure + case "dM": + case "body": + case "div": + var row = results[category.id][group][test_id][structure]; + var testName = [category.id, group, test_id, structure].join("-"); + (knownFailures.value[testName] || platformFailures.value[testName] ? todo_is : is)( + row.valscore, 1, "Browserscope richtext2 value: " + testName); + (knownFailures.select[testName] || platformFailures.select[testName] ? todo_is : is)( + row.selscore, 1, "Browserscope richtext2 selection: " + testName); + } + } + } + } + } + } + } + } + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + // Running all of the tests can take a long time, try to account for it + SimpleTest.requestLongerTimeout(5); + </script> +</head> + +<body onload="startTest()"> + <iframe name="iframe-dM" id="iframe-dM" src="lib/richtext2/richtext2/static/editable-dM.html"></iframe> + <iframe name="iframe-body" id="iframe-body" src="lib/richtext2/richtext2/static/editable-body.html"></iframe> + <iframe name="iframe-div" id="iframe-div" src="lib/richtext2/richtext2/static/editable-div.html"></iframe> + <pre id="results" style="display: none"></pre> +</body> +</html> diff --git a/editor/libeditor/tests/bug527935.html b/editor/libeditor/tests/bug527935.html new file mode 100644 index 0000000000..1731734d29 --- /dev/null +++ b/editor/libeditor/tests/bug527935.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> +<html> +<body> +<div id="content"> + <iframe id="formTarget" name="formTarget"></iframe> + <form action="bug527935_2.html" target="formTarget"> + <input name="test" id="initValue"><input type="submit"> + </form> +</div> +</body> +</html diff --git a/editor/libeditor/tests/bug527935_2.html b/editor/libeditor/tests/bug527935_2.html new file mode 100644 index 0000000000..96af0721d4 --- /dev/null +++ b/editor/libeditor/tests/bug527935_2.html @@ -0,0 +1 @@ +<html><body>dummy page</body></html> diff --git a/editor/libeditor/tests/chrome.toml b/editor/libeditor/tests/chrome.toml new file mode 100644 index 0000000000..a12dd64a76 --- /dev/null +++ b/editor/libeditor/tests/chrome.toml @@ -0,0 +1,31 @@ +[DEFAULT] +skip-if = ["os == 'android'"] + +["test_bug489202.xhtml"] + +["test_bug599983.xhtml"] + +["test_bug607584.xhtml"] + +["test_bug616590.xhtml"] + +["test_bug780908.xhtml"] + +["test_bug1397412.xhtml"] + +["test_can_undo_after_setting_value.xhtml"] + +["test_contenteditable_text_input_handling.html"] + +["test_cut_copy_delete_command_enabled.xhtml"] + +["test_htmleditor_keyevent_handling.html"] + +["test_pasteImgTextarea.xhtml"] +support-files = ["green.png"] + +["test_texteditor_keyevent_handling.html"] +skip-if = [ + "debug && os == 'win'", # Bug 1116205, leaks on windows debug + "os == 'linux'", # Fails delete key on linux +] diff --git a/editor/libeditor/tests/data/cfhtml-chromium.txt b/editor/libeditor/tests/data/cfhtml-chromium.txt Binary files differnew file mode 100644 index 0000000000..7e02537157 --- /dev/null +++ b/editor/libeditor/tests/data/cfhtml-chromium.txt diff --git a/editor/libeditor/tests/data/cfhtml-firefox.txt b/editor/libeditor/tests/data/cfhtml-firefox.txt Binary files differnew file mode 100644 index 0000000000..cc686d8562 --- /dev/null +++ b/editor/libeditor/tests/data/cfhtml-firefox.txt diff --git a/editor/libeditor/tests/data/cfhtml-ie.txt b/editor/libeditor/tests/data/cfhtml-ie.txt Binary files differnew file mode 100644 index 0000000000..a30bc5295e --- /dev/null +++ b/editor/libeditor/tests/data/cfhtml-ie.txt diff --git a/editor/libeditor/tests/data/cfhtml-nocontext.txt b/editor/libeditor/tests/data/cfhtml-nocontext.txt new file mode 100644 index 0000000000..aa48822277 --- /dev/null +++ b/editor/libeditor/tests/data/cfhtml-nocontext.txt @@ -0,0 +1,18 @@ +Version:0.9
+StartHTML:-1
+EndHTML:-1
+StartFragment:0000000111
+EndFragment:0000000246
+<!--StartFragment-->
+<html>
+ <head>
+ <title>Test</title>
+
+ </head>
+ <body>
+ <p>
+ 3.<b>1415926535897932</b>
+ </p>
+ </body>
+</html>
+<!--EndFragment-->
diff --git a/editor/libeditor/tests/data/cfhtml-ooo.txt b/editor/libeditor/tests/data/cfhtml-ooo.txt Binary files differnew file mode 100644 index 0000000000..0bcf7616ef --- /dev/null +++ b/editor/libeditor/tests/data/cfhtml-ooo.txt diff --git a/editor/libeditor/tests/file_bug289384-1.html b/editor/libeditor/tests/file_bug289384-1.html new file mode 100644 index 0000000000..42b7a4da49 --- /dev/null +++ b/editor/libeditor/tests/file_bug289384-1.html @@ -0,0 +1 @@ +<a href="file_bug289384-2.html">link</a> diff --git a/editor/libeditor/tests/file_bug289384-2.html b/editor/libeditor/tests/file_bug289384-2.html new file mode 100644 index 0000000000..0356626279 --- /dev/null +++ b/editor/libeditor/tests/file_bug289384-2.html @@ -0,0 +1 @@ +<body contenteditable onload='opener.continueTest(window);'>foo bar</body> diff --git a/editor/libeditor/tests/file_bug549262.html b/editor/libeditor/tests/file_bug549262.html new file mode 100644 index 0000000000..36d2d26aae --- /dev/null +++ b/editor/libeditor/tests/file_bug549262.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> + <body> + <div style="height: 25vh"></div> + <a href="">test</a> + <div style="height: 25vh"></div> + <div id="editor" contenteditable="true">abc</div> + <div style="height: 20000px;"></div> + </body> +</html> diff --git a/editor/libeditor/tests/file_bug586662.html b/editor/libeditor/tests/file_bug586662.html new file mode 100644 index 0000000000..2989531975 --- /dev/null +++ b/editor/libeditor/tests/file_bug586662.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<html> + <body> + <div style="height: 20000px;"></div> + <textarea id="editor"></textarea> + </body> +</html> diff --git a/editor/libeditor/tests/file_bug611182.html b/editor/libeditor/tests/file_bug611182.html new file mode 100644 index 0000000000..cc9e3e3765 --- /dev/null +++ b/editor/libeditor/tests/file_bug611182.html @@ -0,0 +1 @@ +<html><body>foo bar</body></html> diff --git a/editor/libeditor/tests/file_bug611182.sjs b/editor/libeditor/tests/file_bug611182.sjs new file mode 100644 index 0000000000..772aedef74 --- /dev/null +++ b/editor/libeditor/tests/file_bug611182.sjs @@ -0,0 +1,254 @@ +// SJS file for test_bug611182.html +"use strict"; + +const TESTS = [ + { + ct: "text/html", + val: "<html contenteditable>fooz bar</html>", + }, + { + ct: "text/html", + val: "<html contenteditable><body>fooz bar</body></html>", + }, + { + ct: "text/html", + val: "<body contenteditable>fooz bar</body>", + }, + { + ct: "text/html", + val: "<body contenteditable><p>fooz bar</p></body>", + }, + { + ct: "text/html", + val: "<body contenteditable><div>fooz bar</div></body>", + }, + { + ct: "text/html", + val: "<body contenteditable><span>fooz bar</span></body>", + }, + { + ct: "text/html", + val: "<p contenteditable style='outline:none'>fooz bar</p>", + }, + { + ct: "text/html", + val: "<!DOCTYPE html><html><body contenteditable>fooz bar</body></html>", + }, + { + ct: "text/html", + val: "<!DOCTYPE html><html contenteditable><body>fooz bar</body></html>", + }, + { + ct: "application/xhtml+xml", + val: '<html xmlns="http://www.w3.org/1999/xhtml"><body contenteditable="true">fooz bar</body></html>', + }, + { + ct: "application/xhtml+xml", + val: '<html xmlns="http://www.w3.org/1999/xhtml" contenteditable="true"><body>fooz bar</body></html>', + }, + { + ct: "text/html", + val: "<body onload=\"document.designMode='on'\">fooz bar</body>", + }, + { + ct: "text/html", + val: + "<html><script>" + + "onload = function() {" + + "var old = document.body;" + + "old.parentNode.removeChild(old);" + + "var r = document.documentElement;" + + 'var b = document.createElement("body");' + + "r.appendChild(b);" + + 'b.appendChild(document.createTextNode("fooz bar"));' + + 'b.contentEditable = "true";' + + "};" + + "</script><body></body></html>", + }, + { + ct: "text/html", + val: + "<html><script>" + + "onload = function() {" + + "var old = document.body;" + + "old.parentNode.removeChild(old);" + + "var r = document.documentElement;" + + 'var b = document.createElement("body");' + + 'b.appendChild(document.createTextNode("fooz bar"));' + + 'b.contentEditable = "true";' + + "r.appendChild(b);" + + "};" + + "</script><body></body></html>", + }, + { + ct: "text/html", + val: + "<html><script>" + + "onload = function() {" + + "var old = document.body;" + + "old.parentNode.removeChild(old);" + + "var r = document.documentElement;" + + 'var b = document.createElement("body");' + + "r.appendChild(b);" + + 'b.appendChild(document.createTextNode("fooz bar"));' + + 'b.setAttribute("contenteditable", "true");' + + "};" + + "</script><body></body></html>", + }, + { + ct: "text/html", + val: + "<html><script>" + + "onload = function() {" + + "var old = document.body;" + + "old.parentNode.removeChild(old);" + + "var r = document.documentElement;" + + 'var b = document.createElement("body");' + + 'b.appendChild(document.createTextNode("fooz bar"));' + + 'b.setAttribute("contenteditable", "true");' + + "r.appendChild(b);" + + "};" + + "</script><body></body></html>", + }, + { + ct: "text/html", + val: + "<html><script>" + + "onload = function() {" + + "var old = document.body;" + + "old.parentNode.removeChild(old);" + + "var r = document.documentElement;" + + 'var b = document.createElement("body");' + + "r.appendChild(b);" + + 'b.contentEditable = "true";' + + 'b.appendChild(document.createTextNode("fooz bar"));' + + "};" + + "</script><body></body></html>", + }, + { + ct: "text/html", + val: + "<html><script>" + + "onload = function() {" + + "var old = document.body;" + + "old.parentNode.removeChild(old);" + + "var r = document.documentElement;" + + 'var b = document.createElement("body");' + + 'b.contentEditable = "true";' + + "r.appendChild(b);" + + 'b.appendChild(document.createTextNode("fooz bar"));' + + "};" + + "</script><body></body></html>", + }, + { + ct: "text/html", + val: + "<html><script>" + + "onload = function() {" + + "var old = document.body;" + + "old.parentNode.removeChild(old);" + + "var r = document.documentElement;" + + 'var b = document.createElement("body");' + + "r.appendChild(b);" + + 'b.setAttribute("contenteditable", "true");' + + 'b.appendChild(document.createTextNode("fooz bar"));' + + "};" + + "</script><body></body></html>", + }, + { + ct: "text/html", + val: + "<html><script>" + + "onload = function() {" + + "var old = document.body;" + + "old.parentNode.removeChild(old);" + + "var r = document.documentElement;" + + 'var b = document.createElement("body");' + + 'b.setAttribute("contenteditable", "true");' + + "r.appendChild(b);" + + 'b.appendChild(document.createTextNode("fooz bar"));' + + "};" + + "</script><body></body></html>", + }, + { + ct: "text/html", + val: + "<html><script>" + + "onload = function() {" + + "document.open();" + + 'document.write("<body contenteditable>fooz bar</body>");' + + "document.close();" + + "};" + + "</script><body></body></html>", + }, + { + ct: "text/html", + val: + "data:text/html,<html><script>" + + "onload = function() {" + + "document.open();" + + 'document.write("<body contenteditable><div>fooz bar</div></body>");' + + "document.close();" + + "};" + + "</script><body></body></html>", + }, + { + ct: "text/html", + val: + "<html><script>" + + "onload = function() {" + + "document.open();" + + 'document.write("<body contenteditable><span>fooz bar</span></body>");' + + "document.close();" + + "};" + + "</script><body></body></html>", + }, + { + ct: "text/html", + val: + "<html><script>" + + "onload = function() {" + + "document.open();" + + 'document.write("<p contenteditable style=\\"outline: none\\">fooz bar</p>");' + + "document.close();" + + "};" + + "</script><body></body></html>", + }, + { + ct: "text/html", + val: + "<html><script>" + + "onload = function() {" + + "document.open();" + + 'document.write("<html contenteditable>fooz bar</html>");' + + "document.close();" + + "};" + + "</script><body></body></html>", + }, + { + ct: "text/html", + val: + "<html><script>" + + "onload = function() {" + + "document.open();" + + 'document.write("<html contenteditable><body>fooz bar</body></html>");' + + "document.close();" + + "};" + + "</script><body></body></html>", + }, +]; + +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-cache", false); + + let query = request.queryString; + if (query === "queryTotalTests") { + response.setHeader("Content-Type", "text/html", false); + response.write(TESTS.length); + return; + } + + var curTest = TESTS[query]; + response.setHeader("Content-Type", curTest.ct, false); + response.write(curTest.val); +} diff --git a/editor/libeditor/tests/file_bug635636.xhtml b/editor/libeditor/tests/file_bug635636.xhtml new file mode 100644 index 0000000000..18a8c50aa6 --- /dev/null +++ b/editor/libeditor/tests/file_bug635636.xhtml @@ -0,0 +1,3 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<div>1</div> +</html> diff --git a/editor/libeditor/tests/file_bug635636_2.html b/editor/libeditor/tests/file_bug635636_2.html new file mode 100644 index 0000000000..bf0c8101f9 --- /dev/null +++ b/editor/libeditor/tests/file_bug635636_2.html @@ -0,0 +1 @@ +<html><body>2</body></html> diff --git a/editor/libeditor/tests/file_bug674770-1.html b/editor/libeditor/tests/file_bug674770-1.html new file mode 100644 index 0000000000..3460e3d6a7 --- /dev/null +++ b/editor/libeditor/tests/file_bug674770-1.html @@ -0,0 +1,5 @@ +<!DOCTYPE> +<script> + localStorage.clicked = "true"; + close(); +</script> diff --git a/editor/libeditor/tests/file_bug795418-2.sjs b/editor/libeditor/tests/file_bug795418-2.sjs new file mode 100644 index 0000000000..59aecd6f95 --- /dev/null +++ b/editor/libeditor/tests/file_bug795418-2.sjs @@ -0,0 +1,10 @@ +// SJS file for test_bug795418-2.html +"use strict"; + +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "application/xhtml+xml", false); + response.write( + "<html contenteditable='' xmlns='http://www.w3.org/1999/xhtml'><span>AB</span></html>" + ); +} diff --git a/editor/libeditor/tests/file_bug915962.html b/editor/libeditor/tests/file_bug915962.html new file mode 100644 index 0000000000..85c5139d3b --- /dev/null +++ b/editor/libeditor/tests/file_bug915962.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html> + <body> + <button>Button</button> + <img src="green.png" usemap="#map"> + <map name="map"> + <!-- This URL ensures that the link doesn't get clicked, since + mochitests cannot access the outside network. --> + <area shape="rect" coords="0,0,10,10" href="https://youtube.com/"> + </map> + <div style="height: 20000px;" tabindex="-1"><hr></div> + </body> +</html> diff --git a/editor/libeditor/tests/file_bug966155.html b/editor/libeditor/tests/file_bug966155.html new file mode 100644 index 0000000000..04f55a9188 --- /dev/null +++ b/editor/libeditor/tests/file_bug966155.html @@ -0,0 +1 @@ +<input><iframe onload="contentDocument.designMode = 'on';"> diff --git a/editor/libeditor/tests/file_bug966552.html b/editor/libeditor/tests/file_bug966552.html new file mode 100644 index 0000000000..5061c2e40d --- /dev/null +++ b/editor/libeditor/tests/file_bug966552.html @@ -0,0 +1 @@ +<body onload="document.designMode='on'">test</body> diff --git a/editor/libeditor/tests/file_sanitizer_on_paste.sjs b/editor/libeditor/tests/file_sanitizer_on_paste.sjs new file mode 100644 index 0000000000..6d83609216 --- /dev/null +++ b/editor/libeditor/tests/file_sanitizer_on_paste.sjs @@ -0,0 +1,19 @@ +function handleRequest(request, response) { + if (request.queryString.includes("report")) { + response.setHeader("Content-Type", "text/javascript", false); + if (getState("loaded") == "loaded") { + response.write( + "ok(false, 'There was an attempt to preload the image.');" + ); + } else { + response.write("ok(true, 'There was no attempt to preload the image.');"); + } + response.write("SimpleTest.finish();"); + } else { + setState("loaded", "loaded"); + response.setHeader("Content-Type", "image/svg", false); + response.write( + "<svg xmlns='http://www.w3.org/2000/svg'>Not supposed to load this</svg>" + ); + } +} diff --git a/editor/libeditor/tests/file_select_all_without_body.html b/editor/libeditor/tests/file_select_all_without_body.html new file mode 100644 index 0000000000..06b7e6685c --- /dev/null +++ b/editor/libeditor/tests/file_select_all_without_body.html @@ -0,0 +1,38 @@ +<html> +<head> +<script type="text/javascript"> + +function is(aLeft, aRight, aMessage) { + window.opener.SimpleTest.is(aLeft, aRight, aMessage); +} + +function unload() { + window.opener.SimpleTest.finish(); +} + +function boom() { + var root = document.documentElement; + while (root.firstChild) { + root.firstChild.remove(); + } + root.appendChild(document.createTextNode("Mozilla")); + root.focus(); + let cespan = document.createElementNS("http://www.w3.org/1999/xhtml", "span"); + cespan.setAttributeNS(null, "contenteditable", "true"); + root.appendChild(cespan); + try { + document.execCommand("selectAll", false, null); + } catch (e) { } + + is(window.getSelection().toString(), "Mozilla", + "The nodes are not selected"); + + window.close(); +} + +window.opener.SimpleTest.waitForFocus(boom, window); + +</script></head> + +<body onunload="unload();"></body> +</html> diff --git a/editor/libeditor/tests/green.png b/editor/libeditor/tests/green.png Binary files differnew file mode 100644 index 0000000000..0aaec20932 --- /dev/null +++ b/editor/libeditor/tests/green.png diff --git a/editor/libeditor/tests/mochitest.toml b/editor/libeditor/tests/mochitest.toml new file mode 100644 index 0000000000..5af13503f7 --- /dev/null +++ b/editor/libeditor/tests/mochitest.toml @@ -0,0 +1,628 @@ +[DEFAULT] +prefs = [ + "apz.zoom-to-focused-input.enabled=false", + "ui.dragThresholdX=4", # Bug 1873142 + "ui.dragThresholdY=4", # Bug 1873142 +] + +support-files = ["green.png"] + +["test_CF_HTML_clipboard.html"] +skip-if = ["os != 'mac'"] # bug 574005 +support-files = [ + "data/cfhtml-chromium.txt", + "data/cfhtml-firefox.txt", + "data/cfhtml-ie.txt", + "data/cfhtml-ooo.txt", + "data/cfhtml-nocontext.txt", +] + +["test_abs_positioner_appearance.html"] + +["test_abs_positioner_hidden_during_dragging.html"] +skip-if = ["os == 'android'"] # Sync with test_abs_positioner_positioning_elements.html + +["test_abs_positioner_positioning_elements.html"] +skip-if = [ + "os == 'android'", # Bug 1525959 + "xorigin", # Inconsistent pass/fail in opt and debug +] + +["test_backspace_vs.html"] + +["test_bug46555.html"] + +["test_bug200416.html"] + +["test_bug289384.html"] +skip-if = ["os != 'mac'"] +support-files = [ + "file_bug289384-1.html", + "file_bug289384-2.html", +] + +["test_bug290026.html"] + +["test_bug291780.html"] + +["test_bug309731.html"] + +["test_bug316447.html"] + +["test_bug318065.html"] + +["test_bug332636.html"] +support-files = ["test_bug332636.html^headers^"] + +["test_bug358033.html"] + +["test_bug372345.html"] + +["test_bug404320.html"] + +["test_bug408231.html"] +skip-if = ["os == 'android'"] + +["test_bug410986.html"] + +["test_bug414526.html"] + +["test_bug417418.html"] + +["test_bug426246.html"] + +["test_bug430392.html"] + +["test_bug439808.html"] + +["test_bug442186.html"] + +["test_bug455992.html"] + +["test_bug456244.html"] + +["test_bug460740.html"] + +["test_bug471319.html"] + +["test_bug471722.html"] + +["test_bug478725.html"] + +["test_bug480647.html"] + +["test_bug480972.html"] + +["test_bug483651.html"] + +["test_bug490879.html"] +skip-if = [ + "os == 'android'", # bug 1299578 + "headless", +] + +["test_bug502673.html"] + +["test_bug514156.html"] + +["test_bug520189.html"] + +["test_bug525389.html"] + +["test_bug537046.html"] + +["test_bug549262.html"] +support-files = [ + "file_bug549262.html", + "!/gfx/layers/apz/test/mochitest/apz_test_utils.js", +] + +["test_bug550434.html"] + +["test_bug551704.html"] + +["test_bug552782.html"] + +["test_bug567213.html"] + +["test_bug569988.html"] +skip-if = ["os == 'android'"] + +["test_bug570144.html"] + +["test_bug578771.html"] + +["test_bug586662.html"] +skip-if = ["true"] # bug 1376382 +support-files = ["file_bug586662.html"] + +["test_bug590554.html"] + +["test_bug592592.html"] + +["test_bug596001.html"] + +["test_bug596506.html"] + +["test_bug597331.html"] + +["test_bug597784.html"] + +["test_bug599322.html"] + +["test_bug599983.html"] + +["test_bug600570.html"] + +["test_bug603556.html"] + +["test_bug604532.html"] + +["test_bug607584.html"] + +["test_bug611182.html"] +support-files = [ + "file_bug611182.html", + "file_bug611182.sjs", +] + +["test_bug612128.html"] + +["test_bug612447.html"] + +["test_bug622371.html"] + +["test_bug625452.html"] + +["test_bug629172.html"] +skip-if = [ + "os == 'android'", + "verify && os == 'mac'", # Due to resizer rendring issue of macOS +] + +["test_bug629845.html"] + +["test_bug635636.html"] +support-files = [ + "file_bug635636.xhtml", + "file_bug635636_2.html", +] + +["test_bug638596.html"] + +["test_bug641466.html"] + +["test_bug645914.html"] + +["test_bug646194.html"] + +["test_bug668599.html"] + +["test_bug674770-1.html"] +skip-if = ["os == 'android'"] +support-files = ["file_bug674770-1.html"] + +["test_bug674770-2.html"] +skip-if = ["os == 'android'"] + +["test_bug674861.html"] + +["test_bug676401.html"] + +["test_bug677752.html"] + +["test_bug681229.html"] + +["test_bug686203.html"] + +["test_bug692520.html"] + +["test_bug697842.html"] + +["test_bug725069.html"] + +["test_bug735059.html"] + +["test_bug738366.html"] + +["test_bug740784.html"] + +["test_bug742261.html"] + +["test_bug757371.html"] + +["test_bug757771.html"] + +["test_bug772796.html"] + +["test_bug773262.html"] + +["test_bug780035.html"] + +["test_bug787432.html"] + +["test_bug790475.html"] + +["test_bug795418-2.html"] +support-files = ["file_bug795418-2.sjs"] + +["test_bug795418-3.html"] + +["test_bug795418-4.html"] + +["test_bug795418-5.html"] + +["test_bug795418-6.html"] + +["test_bug795418.html"] + +["test_bug795785.html"] +support-files = ["!/gfx/layers/apz/test/mochitest/apz_test_utils.js"] + +["test_bug796839.html"] + +["test_bug830600.html"] +skip-if = ["os == 'android'"] + +["test_bug832025.html"] + +["test_bug850043.html"] + +["test_bug857487.html"] + +["test_bug858918.html"] + +["test_bug915962.html"] +support-files = [ + "file_bug915962.html", + "!/gfx/layers/apz/test/mochitest/apz_test_utils.js", +] + +["test_bug966155.html"] +skip-if = ["os != 'win'"] +support-files = ["file_bug966155.html"] + +["test_bug966552.html"] +skip-if = ["os != 'win'"] +support-files = ["file_bug966552.html"] + +["test_bug974309.html"] + +["test_bug998188.html"] + +["test_bug1026397.html"] + +["test_bug1053048.html"] + +["test_bug1068979.html"] + +["test_bug1094000.html"] + +["test_bug1102906.html"] +skip-if = ["os == 'android'"] + +["test_bug1109465.html"] + +["test_bug1130651.html"] + +["test_bug1140105.html"] + +["test_bug1140617.html"] + +["test_bug1151186.html"] +skip-if = ["os == 'win' && ccov && xorigin"] # high frequency intermittent + +["test_bug1153237.html"] + +["test_bug1162952.html"] + +["test_bug1181130-1.html"] + +["test_bug1181130-2.html"] + +["test_bug1186799.html"] + +["test_bug1230473.html"] + +["test_bug1247483.html"] + +["test_bug1248128.html"] + +["test_bug1248185.html"] + +["test_bug1248186.html"] + +["test_bug1250010.html"] + +["test_bug1257363.html"] + +["test_bug1258085.html"] + +["test_bug1268736.html"] + +["test_bug1270235.html"] + +["test_bug1306532.html"] +skip-if = ["headless"] + +["test_bug1310912.html"] + +["test_bug1314790.html"] + +["test_bug1315065.html"] + +["test_bug1316302.html"] + +["test_bug1328023.html"] + +["test_bug1330796.html"] + +["test_bug1332876.html"] + +["test_bug1352799.html"] + +["test_bug1355792.html"] + +["test_bug1358025.html"] + +["test_bug1361008.html"] + +["test_bug1361052.html"] + +["test_bug1385905.html"] + +["test_bug1390562.html"] + +["test_bug1394758.html"] + +["test_bug1399722.html"] + +["test_bug1406726.html"] + +["test_bug1409520.html"] + +["test_bug1425997.html"] + +["test_bug1543312.html"] + +["test_bug1568996.html"] + +["test_bug1574596.html"] +skip-if = ["os == 'android'"] #Bug 1575739 + +["test_bug1581337.html"] + +["test_bug1619852.html"] + +["test_bug1620778.html"] + +["test_bug1649005.html"] + +["test_bug1659276.html"] + +["test_bug1704381.html"] + +["test_cannot_undo_after_reinitializing_editor.html"] + +["test_caret_move_in_vertical_content.html"] + +["test_cmd_absPos.html"] + +["test_cmd_backgroundColor.html"] + +["test_cmd_fontFace_with_empty_string.html"] + +["test_cmd_fontFace_with_tt.html"] + +["test_cmd_increaseFont.html"] + +["test_cmd_paragraphState.html"] + +["test_composition_event_created_in_chrome.html"] + +["test_composition_with_highlight_in_texteditor.html"] + +["test_contenteditable_copy_empty_selection.html"] + +["test_contenteditable_focus.html"] + +["test_cut_copy_delete_command_enabled.html"] + +["test_cut_copy_password.html"] + +["test_defaultParagraphSeparatorBR_between_blocks.html"] + +["test_doc_scrollbar_toggled_designMode_on_mousedown.html"] +skip-if = ["os == 'android'"] # Needs interaction with the scrollbar + +["test_dom_input_event_on_htmleditor.html"] + +["test_dom_input_event_on_texteditor.html"] + +["test_dragdrop.html"] + +["test_execCommandPaste_noTarget.html"] + +["test_focus_caret_navigation_between_nested_editors.html"] + +["test_focused_document_element_becoming_editable.html"] + +["test_handle_new_lines.html"] + +["test_htmleditor_tab_key_handling.html"] + +["test_htmleditor_toggle_text_direction.html"] + +["test_initial_selection_and_caret_of_designMode.html"] + +["test_inlineTableEditing.html"] + +["test_inline_style_cache.html"] + +["test_insertHTML_starting_with_multiple_comment_nodes.html"] + +["test_insertParagraph_in_h2_and_li.html"] + +["test_insertParagraph_in_inline_editing_host.html"] + +["test_insertText_around_text_node_in_plaintext_mode.html"] + +["test_join_split_node_direction_change_command.html"] + +["test_keypress_untrusted_event.html"] + +["test_label_contenteditable.html"] + +["test_middle_click_paste.html"] +skip-if = ["headless"] + +["test_native_key_bindings_in_shadow.html"] +skip-if = ["os != 'linux' && os != 'mac'"] # Depends on NativeKeyBindings used only on Linux and macOS + +["test_nested_editor.html"] + +["test_new_plaintext_mail_with_plaintext_signature.html"] + +["test_nsIEditorMailSupport_insertAsCitedQuotation.html"] + +["test_nsIEditorMailSupport_insertTextWithQuotations.html"] +skip-if = ["xorigin"] # Testing internal API for comm-central + +["test_nsIEditor_beginningOfDocument.html"] + +["test_nsIEditor_canUndo_canRedo.html"] + +["test_nsIEditor_clearUndoRedo.html"] + +["test_nsIEditor_deleteNode.html"] + +["test_nsIEditor_documentCharacterSet.html"] + +["test_nsIEditor_documentIsEmpty.html"] + +["test_nsIEditor_insertLineBreak.html"] + +["test_nsIEditor_insertNode.html"] + +["test_nsIEditor_isSelectionEditable.html"] + +["test_nsIEditor_outputToString.html"] + +["test_nsIEditor_undoAll.html"] + +["test_nsIEditor_undoRedoEnabled.html"] + +["test_nsIHTMLEditor_getElementOrParentByTagName.html"] + +["test_nsIHTMLEditor_getParagraphState.html"] + +["test_nsIHTMLEditor_getSelectedElement.html"] + +["test_nsIHTMLEditor_insertElementAtSelection.html"] + +["test_nsIHTMLEditor_removeInlineProperty.html"] + +["test_nsIHTMLEditor_selectElement.html"] + +["test_nsIHTMLEditor_setBackgroundColor.html"] + +["test_nsIHTMLObjectResizer_hideResizers.html"] + +["test_nsITableEditor_deleteTableCell.html"] + +["test_nsITableEditor_deleteTableCellContents.html"] + +["test_nsITableEditor_deleteTableColumn.html"] + +["test_nsITableEditor_deleteTableRow.html"] + +["test_nsITableEditor_getCellAt.html"] + +["test_nsITableEditor_getCellDataAt.html"] + +["test_nsITableEditor_getCellIndexes.html"] + +["test_nsITableEditor_getFirstRow.html"] + +["test_nsITableEditor_getFirstSelectedCellInTable.html"] + +["test_nsITableEditor_getSelectedCells.html"] + +["test_nsITableEditor_getSelectedCellsType.html"] + +["test_nsITableEditor_getSelectedOrParentTableElement.html"] + +["test_nsITableEditor_getTableSize.html"] + +["test_nsITableEditor_insertTableCell.html"] + +["test_nsITableEditor_insertTableColumn.html"] + +["test_nsITableEditor_insertTableRow.html"] + +["test_password_input_with_unmasked_range.html"] + +["test_password_paste.html"] + +["test_password_per_word_operation.html"] + +["test_password_unmask_API.html"] + +["test_pasteImgFromTransferable.html"] + +["test_pasteImgTextarea.html"] + +["test_paste_as_quote_in_text_control.html"] + +["test_paste_no_formatting.html"] + +["test_paste_redirect_focus_in_paste_event_listener.html"] + +["test_pasting_in_root_element.xhtml"] + +["test_pasting_in_temporarily_created_div_outside_body.html"] + +["test_pasting_table_rows.html"] +skip-if = ["headless"] # The test calls `synthesizeKey`, see bug 1669923. + +["test_pasting_text_longer_than_maxlength.html"] + +["test_resizers_appearance.html"] + +["test_resizers_resizing_elements.html"] +skip-if = ["verify && debug && os == 'win'"] # bug 1485293 + +["test_root_element_replacement.html"] + +["test_sanitizer_on_paste.html"] +support-files = ["file_sanitizer_on_paste.sjs"] + +["test_select_all_without_body.html"] +support-files = ["file_select_all_without_body.html"] + +["test_selection_move_commands.html"] +support-files = ["!/gfx/layers/apz/test/mochitest/apz_test_utils.js"] + +["test_setting_value_longer_than_maxlength_with_setUserInput.html"] + +["test_spellcheck_pref.html"] +skip-if = ["os == 'android'"] + +["test_state_change_on_reframe.html"] + +["test_textarea_value_not_include_cr.html"] + +["test_texteditor_textnode.html"] + +["test_texteditor_tripleclick_setvalue.html"] + +["test_texteditor_wrapping_long_line.html"] + +["test_typing_at_edge_of_anchor.html"] + +["test_undo_after_spellchecker_replaces_word.html"] +skip-if = ["os == 'android'"] + +["test_undo_redo_stack_after_setting_value.html"] + +["test_undo_with_editingui.html"] diff --git a/editor/libeditor/tests/test_CF_HTML_clipboard.html b/editor/libeditor/tests/test_CF_HTML_clipboard.html new file mode 100644 index 0000000000..51b0e03fed --- /dev/null +++ b/editor/libeditor/tests/test_CF_HTML_clipboard.html @@ -0,0 +1,149 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=572642 +--> +<head> + <title>Test for Bug 572642</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=572642">Mozilla Bug 572642</a> +<p id="display"></p> +<div id="content"> + <div id="editor1" contenteditable="true"></div> + <iframe id="editor2"></iframe> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 572642 **/ + +function copyCF_HTML(cfhtml, success, failure) { + const Cc = SpecialPowers.Cc; + const Ci = SpecialPowers.Ci; + const CF_HTML = "application/x-moz-nativehtml"; + + function getLoadContext() { + return SpecialPowers.wrap(window) + .docShell + .QueryInterface(Ci.nsILoadContext); + } + + var cb = SpecialPowers.Services.clipboard; + + var counter = 0; + function copyCF_HTML_worker(successFn, failureFn) { + if (++counter > 50) { + ok(false, "Timed out while polling clipboard for pasted data"); + failure(); + return; + } + + var flavors = [CF_HTML]; + if (!cb.hasDataMatchingFlavors(flavors, cb.kGlobalClipboard)) { + setTimeout(function() { copyCF_HTML_worker(successFn, failureFn); }, 100); + return; + } + + var trans = Cc["@mozilla.org/widget/transferable;1"]. + createInstance(Ci.nsITransferable); + trans.init(getLoadContext()); + trans.addDataFlavor(CF_HTML); + cb.getData(trans, cb.kGlobalClipboard, SpecialPowers.wrap(window).browsingContext.currentWindowContext); + var data = SpecialPowers.createBlankObject(); + try { + trans.getTransferData(CF_HTML, data); + data = SpecialPowers.wrap(data).value.QueryInterface(Ci.nsISupportsCString).data; + } catch (e) { + setTimeout(function() { copyCF_HTML_worker(successFn, failureFn); }, 100); + return; + } + success(); + } + + var trans = Cc["@mozilla.org/widget/transferable;1"]. + createInstance(Ci.nsITransferable); + trans.init(getLoadContext()); + trans.addDataFlavor(CF_HTML); + var data = Cc["@mozilla.org/supports-cstring;1"]. + createInstance(Ci.nsISupportsCString); + data.data = cfhtml; + trans.setTransferData(CF_HTML, data); + cb.setData(trans, null, cb.kGlobalClipboard); + copyCF_HTML_worker(success, failure); +} + +function loadCF_HTMLdata(filename) { + var req = new XMLHttpRequest(); + req.open("GET", filename, false); + req.overrideMimeType("text/plain; charset=x-user-defined"); + req.send(null); + is(req.status, 200, "Could not read the binary file " + filename); + return req.responseText; +} + +var gTests = [ + // Copied from Firefox + {fileName: "cfhtml-firefox.txt", expected: "Firefox"}, + // Copied from OpenOffice.org + {fileName: "cfhtml-ooo.txt", expected: "hello"}, + // Copied from IE + {fileName: "cfhtml-ie.txt", expected: "browser"}, + // Copied from Chromium + {fileName: "cfhtml-chromium.txt", expected: "Pacific"}, + // CF_HTML with no context specified (StartHTML and EndHTML set to -1) + {fileName: "cfhtml-nocontext.txt", expected: "3.1415926535897932"}, +]; +var gTestIndex = 0; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("It's a legacy test."); + +for (var i = 0; i < gTests.length; ++i) { + gTests[i].data = loadCF_HTMLdata("data/" + gTests[i].fileName); +} + +function runTest() { + var test = gTests[gTestIndex++]; + + copyCF_HTML(test.data, function() { + // contenteditable + var contentEditable = document.getElementById("editor1"); + contentEditable.innerHTML = ""; + contentEditable.focus(); + synthesizeKey("v", {accelKey: true}); + isnot(contentEditable.textContent.indexOf(test.expected), -1, + "Paste operation for " + test.fileName + " should be successful in contenteditable"); + + // designMode + var iframe = document.getElementById("editor2"); + iframe.addEventListener("load", function() { + var doc = iframe.contentDocument; + var win = doc.defaultView; + setTimeout(function() { + win.addEventListener("focus", function() { + doc.designMode = "on"; + synthesizeKey("v", {accelKey: true}, win); + isnot(doc.body.textContent.indexOf(test.expected), -1, + "Paste operation for " + test.fileName + " should be successful in designMode"); + + if (gTestIndex == gTests.length) + SimpleTest.finish(); + else + runTest(); + }, {once: true}); + win.focus(); + }, 0); + }, {once: true}); + iframe.srcdoc = "foo"; + }, SimpleTest.finish); +} + +SimpleTest.waitForFocus(runTest); +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_abs_positioner_appearance.html b/editor/libeditor/tests/test_abs_positioner_appearance.html new file mode 100644 index 0000000000..c516b9c511 --- /dev/null +++ b/editor/libeditor/tests/test_abs_positioner_appearance.html @@ -0,0 +1,177 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test for absolute positioner appearance</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none;"> + +</div> + +<div id="editor" contenteditable></div> +<div id="clickaway" style="width: 3px; height: 3px;"></div> +<img src="green.png"><!-- for ensuring to load the image at first test of <img> case --> +<pre id="test"> + +<script class="testbody" type="application/javascript"> +"use strict"; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(async function() { + async function waitForSelectionChange() { + return new Promise(resolve => { + document.addEventListener("selectionchange", () => { + resolve(); + }, {once: true}); + }); + } + + let editor = document.getElementById("editor"); + let outOfEditor = document.getElementById("clickaway"); + + async function testIfAppears() { + const kTests = [ + { description: "absolute positioned <div>", + innerHTML: "<div id=\"target\" style=\"position: absolute; top: 50px; left: 50px;\">positioned</div>", + movable: true, + }, + { description: "fixed positioned <div>", + innerHTML: "<div id=\"target\" style=\"position: fixed; top: 50px; left: 50px;\">positioned</div>", + movable: false, + }, + { description: "relative positioned <div>", + innerHTML: "<div id=\"target\" style=\"position: relative; top: 50px; left: 50px;\">positioned</div>", + movable: false, + }, + ]; + + for (const kTest of kTests) { + const kDescription = "testIfAppears, " + kTest.description + ": "; + editor.innerHTML = kTest.innerHTML; + let target = document.getElementById("target"); + + document.execCommand("enableAbsolutePositionEditing", false, false); + ok(!document.queryCommandState("enableAbsolutePositionEditing"), + kDescription + "Absolute positioned element editor should be disabled by the call of execCommand"); + + synthesizeMouseAtCenter(outOfEditor, {}); + let promiseSelectionChangeEvent1 = waitForSelectionChange(); + synthesizeMouseAtCenter(target, {}); + await promiseSelectionChangeEvent1; + + ok(!target.hasAttribute("_moz_abspos"), + kDescription + "While enableAbsolutePositioner is disabled, positioner shouldn't appear"); + + document.execCommand("enableAbsolutePositionEditing", false, true); + ok(document.queryCommandState("enableAbsolutePositionEditing"), + kDescription + "Absolute positioned element editor should be enabled by the call of execCommand"); + + synthesizeMouseAtCenter(outOfEditor, {}); + let promiseSelectionChangeEvent2 = waitForSelectionChange(); + synthesizeMouseAtCenter(target, {}); + await promiseSelectionChangeEvent2; + + is(target.hasAttribute("_moz_abspos"), kTest.movable, + kDescription + (kTest.movable ? "While enableAbsolutePositionEditing is enabled, positioner should appear" : + "Even while enableAbsolutePositionEditing is enabled, positioner shouldn't appear")); + + document.execCommand("enableAbsolutePositionEditing", false, false); + ok(!target.hasAttribute("_moz_abspos"), + kDescription + "When enableAbsolutePositionEditing is disabled even while positioner is visible, positioner should disappear"); + + document.execCommand("enableAbsolutePositionEditing", false, true); + is(target.hasAttribute("_moz_abspos"), kTest.movable, + kDescription + (kTest.movable ? + "When enableAbsolutePositionEditing is enabled when absolute positioned element is selected, positioner should appear" : + "Even if enableAbsolutePositionEditing is enabled when static positioned element is selected, positioner shouldn't appear")); + } + } + + async function testStyle() { + // See HTMLEditor::GetTemporaryStyleForFocusedPositionedElement(). + const kTests = [ + { description: "background-color: transparent; color: white;", + innerHTML: "<div id=\"target\" style=\"position: absolute; " + + "top: 50%; left: 50%; " + + "background-color: transparent; " + + "color: white;\">positioned</div>", + value: "black", + }, + { description: "background-color: transparent; color: black;", + innerHTML: "<div id=\"target\" style=\"position: absolute; " + + "top: 50%; left: 50%; " + + "background-color: transparent; " + + "color: black;\">positioned</div>", + value: "white", + }, + { description: "background-color: black; color: white;", + innerHTML: "<div id=\"target\" style=\"position: absolute; " + + "top: 50%; left: 50%; " + + "background-color: black; " + + "color: white;\">positioned</div>", + value: "", + }, + { description: "background-color: white; color: black;", + innerHTML: "<div id=\"target\" style=\"position: absolute; " + + "top: 50%; left: 50%; " + + "background-color: white; " + + "color: black;\">positioned</div>", + value: "", + }, + { description: "background-image: green.png; background-color: black; color: white;", + innerHTML: "<div id=\"target\" style=\"position: absolute; " + + "top: 50%; left: 50%; " + + "background-image: green.png; " + + "background-color: black; " + + "color: white;\">positioned</div>", + value: "", + }, + { description: "background-image: green.png; background-color: white; color: black;", + innerHTML: "<div id=\"target\" style=\"position: absolute; " + + "top: 50%; left: 50%; " + + "background-image: green.png; " + + "background-color: white; " + + "color: black;\">positioned</div>", + value: "", + }, + { description: "background-image: green.png;", + innerHTML: "<div id=\"target\" style=\"position: absolute; " + + "top: 50%; left: 50%; " + + "background-image: green.png;\">positioned</div>", + value: "white", // XXX Why? background-image is not "none"... + }, + ]; + + document.execCommand("enableAbsolutePositionEditing", false, true); + ok(document.queryCommandState("enableAbsolutePositionEditing"), + "testStyle, Absolute positioned element editor should be enabled by the call of execCommand"); + + for (const kTest of kTests) { + const kDescription = "testStyle, " + kTest.description + ": "; + + editor.innerHTML = kTest.innerHTML; + let target = document.getElementById("target"); + + synthesizeMouseAtCenter(outOfEditor, {}); + let promiseSelectionChangeEvent = waitForSelectionChange(); + synthesizeMouseAtCenter(target, {}); + await promiseSelectionChangeEvent; + + is(target.getAttribute("_moz_abspos"), kTest.value, + kDescription + "The value of _moz_abspos attribute is unexpected"); + } + } + + await testIfAppears(); + await testStyle(); + + SimpleTest.finish(); +}); +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_abs_positioner_hidden_during_dragging.html b/editor/libeditor/tests/test_abs_positioner_hidden_during_dragging.html new file mode 100644 index 0000000000..3badd4c3b3 --- /dev/null +++ b/editor/libeditor/tests/test_abs_positioner_hidden_during_dragging.html @@ -0,0 +1,102 @@ +<!DOCTYPE html> +<meta charset="utf-8" /> +<title>Drag absolutely positioned element to crash</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +<div + contenteditable + style=" + border: blue 1px solid; + margin: 20px; + " +> + <div + style=" + border: red 1px dashed; + background-color: rgba(255, 0, 0, 0.3); + position: absolute; + width: 100px; + height: 100px; + overflow: auto; + " + > + This is absolutely positioned element. + </div> + <p>This is static positioned paragraph #1</p> + <p>This is static positioned paragraph #2</p> + <p>This is static positioned paragraph #3</p> + <p>This is static positioned paragraph #4</p> + <p>This is static positioned paragraph #5</p> + <p>This is static positioned paragraph #6</p> + <p>This is static positioned paragraph #7</p> +</div> +<script> +"use strict"; + +document.execCommand("enableAbsolutePositionEditing", false, true); + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(async () => { + disableNonTestMouseEvents(true); + try { + document.querySelector("div[contenteditable").focus(); + + function promiseSelectionChange() { + return new Promise(resolve => { + document.addEventListener("selectionchange", () => { + resolve(); + }, {once: true}); + }); + } + + let absContainer = document.querySelector("div > div"); + let rect = absContainer.getBoundingClientRect(); + // We still don't have a way to retrieve the grabber. Therefore, we need + // to compute a point in the grabber from the absolutely positioned + // element's top-left coordinates. + const kOffsetX = 18; + const kOffsetY = -7; + let waitForSelectionChange = promiseSelectionChange(); + synthesizeMouseAtCenter(absContainer, {}); + await waitForSelectionChange; + synthesizeMouse(absContainer, kOffsetX, kOffsetY, {type: "mousedown"}); + ok(absContainer.hasAttribute("_moz_abspos"), "Mousedown on the grabber should make it in drag mode"); + synthesizeMouseAtPoint(100, 100, {type: "mousemove"}); + synthesizeMouseAtPoint(100, 100, {type: "mouseup"}); + isnot(absContainer.getBoundingClientRect().x, rect.x, + "The absolutely positioned container should be moved along x-axis"); + isnot(absContainer.getBoundingClientRect().y, rect.y, + "The absolutely positioned container should be moved along y-axis"); + + rect = absContainer.getBoundingClientRect(); + synthesizeMouse(absContainer, kOffsetX, kOffsetY, {type: "mousedown"}); + ok(absContainer.hasAttribute("_moz_abspos"), "Mousedown on the grabber should make it in drag mode again"); + document.execCommand("enableAbsolutePositionEditing", false, false); + ok(!absContainer.hasAttribute("_moz_abspos"), "Disabling the grabber makes it not in drag mode (before mouse move)"); + synthesizeMouseAtPoint(50, 50, {type: "mousemove"}); + synthesizeMouseAtPoint(50, 50, {type: "mouseup"}); + is(absContainer.getBoundingClientRect().x, rect.x, + "The absolutely positioned container shouldn't be moved along x-axis due to the UI is killed by the web app (before mouse move)"); + is(absContainer.getBoundingClientRect().y, rect.y, + "The absolutely positioned container shouldn't be moved along y-axis due to the UI is killed by the web app (before mouse move)"); + document.execCommand("enableAbsolutePositionEditing", false, true); + + rect = absContainer.getBoundingClientRect(); + synthesizeMouse(absContainer, kOffsetX, kOffsetY, {type: "mousedown"}); + ok(absContainer.hasAttribute("_moz_abspos"), "Mousedown on the grabber should make it in drag mode again"); + document.execCommand("enableAbsolutePositionEditing", false, false); + synthesizeMouseAtPoint(50, 50, {type: "mousemove"}); + ok(!absContainer.hasAttribute("_moz_abspos"), "Disabling the grabber makes it not in drag mode (during mouse move)"); + synthesizeMouseAtPoint(50, 50, {type: "mousemove"}); + synthesizeMouseAtPoint(50, 50, {type: "mouseup"}); + is(absContainer.getBoundingClientRect().x, rect.x, + "The absolutely positioned container shouldn't be moved along x-axis due to the UI is killed by the web app (during mouse move)"); + is(absContainer.getBoundingClientRect().y, rect.y, + "The absolutely positioned container shouldn't be moved along y-axis due to the UI is killed by the web app (during mouse move)"); + } finally { + disableNonTestMouseEvents(false); + SimpleTest.finish(); + } +}); +</script> diff --git a/editor/libeditor/tests/test_abs_positioner_positioning_elements.html b/editor/libeditor/tests/test_abs_positioner_positioning_elements.html new file mode 100644 index 0000000000..45342933ba --- /dev/null +++ b/editor/libeditor/tests/test_abs_positioner_positioning_elements.html @@ -0,0 +1,196 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for positioners of absolute positioned elements</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style> + #target { + background-color: green; + } + </style> +</head> +<body> +<p id="display"></p> +<div id="content" contenteditable style="height: 200px; width: 200px;"></div> +<div id="clickaway" style="position: absolute; top: 250px; width: 10px; height: 10px; z-index: 100;"></div> +<img src="green.png"><!-- for ensuring to load the image at first test of <img> case --> +<pre id="test"> +<script type="application/javascript"> +"use strict"; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(async function() { + document.execCommand("enableAbsolutePositionEditing", false, true); + ok(document.queryCommandState("enableAbsolutePositionEditing"), + "Absolute positioned element editor should be enabled by the call of execCommand"); + + let outOfEditor = document.getElementById("clickaway"); + + function cancel(e) { e.stopPropagation(); } + let content = document.getElementById("content"); + content.addEventListener("mousedown", cancel); + content.addEventListener("mousemove", cancel); + content.addEventListener("mouseup", cancel); + + async function waitForSelectionChange() { + return new Promise(resolve => { + document.addEventListener("selectionchange", () => { + resolve(); + }, {once: true}); + }); + } + + async function doTest(aDescription, aInnerHTML) { + content.innerHTML = aInnerHTML; + let description = aDescription + ": "; + let target = document.getElementById("target"); + target.style.position = "absolute"; + + async function testPositioner(aDeltaX, aDeltaY) { + ok(true, description + "testPositioner(" + [aDeltaX, aDeltaY].join(", ") + ")"); + + // Reset the position of the target. + target.style.top = "50px"; + target.style.left = "50px"; + + // Click on the target to show the positioner. + let promiseSelectionChangeEvent = waitForSelectionChange(); + synthesizeMouseAtCenter(target, {}); + await promiseSelectionChangeEvent; + + let rect = target.getBoundingClientRect(); + + ok(target.hasAttribute("_moz_abspos"), + description + "While enableAbsolutePositionEditing is enabled, the positioner should appear"); + + // left is abs positioned element's left + margin-left + border-left-width + 12. + // XXX Perhaps, we need to add border-left-width here if you add new test to have thick border. + const kPositionerX = 18; + // top is abs positioned element's top + margin-top + border-top-width - 14. + // XXX Perhaps, we need to add border-top-width here if you add new test to have thick border. + const kPositionerY = -7; + + let beforeInputEventExpected = true; + let beforeInputFired = false; + let inputEventExpected = true; + let inputFired = false; + function onBeforeInput(aEvent) { + beforeInputFired = true; + aEvent.preventDefault(); // For making sure this preventDefault() call does not cancel the operation. + if (!beforeInputEventExpected) { + ok(false, '"beforeinput" event should not be fired after stopping resizing'); + return; + } + ok(aEvent instanceof InputEvent, + '"beforeinput" event for position changing of absolute position should be dispatched with InputEvent interface'); + is(aEvent.cancelable, false, + '"beforeinput" event for position changing of absolute position container should not be cancelable'); + is(aEvent.bubbles, true, + '"beforeinput" event for position changing of absolute position should always bubble'); + is(aEvent.inputType, "", + 'inputType of "beforeinput" event for position changing of absolute position should be empty string'); + is(aEvent.data, null, + 'data of "beforeinput" event for position changing of absolute position should be null'); + is(aEvent.dataTransfer, null, + 'dataTransfer of "beforeinput" event for position changing of absolute position should be null'); + let targetRanges = aEvent.getTargetRanges(); + let selection = document.getSelection(); + is(targetRanges.length, selection.rangeCount, + 'getTargetRanges() of "beforeinput" event for position changing of absolute position should return selection ranges'); + if (targetRanges.length === selection.rangeCount) { + for (let i = 0; i < selection.rangeCount; i++) { + let range = selection.getRangeAt(i); + is(targetRanges[i].startContainer, range.startContainer, + `startContainer of getTargetRanges()[${i}] of "beforeinput" event for position changing of absolute position does not match`); + is(targetRanges[i].startOffset, range.startOffset, + `startOffset of getTargetRanges()[${i}] of "beforeinput" event for position changing of absolute position does not match`); + is(targetRanges[i].endContainer, range.endContainer, + `endContainer of getTargetRanges()[${i}] of "beforeinput" event for position changing of absolute position does not match`); + is(targetRanges[i].endOffset, range.endOffset, + `endOffset of getTargetRanges()[${i}] of "beforeinput" event for position changing of absolute position does not match`); + } + } + } + function onInput(aEvent) { + inputFired = true; + if (!inputEventExpected) { + ok(false, '"input" event should not be fired after stopping resizing'); + return; + } + ok(aEvent instanceof InputEvent, + '"input" event for position changing of absolute position container should be dispatched with InputEvent interface'); + is(aEvent.cancelable, false, + '"input" event for position changing of absolute position container should be never cancelable'); + is(aEvent.bubbles, true, + '"input" event for position changing of absolute position should always bubble'); + is(aEvent.inputType, "", + 'inputType of "input" event for position changing of absolute position should be empty string'); + is(aEvent.data, null, + 'data of "input" event for position changing of absolute position should be null'); + is(aEvent.dataTransfer, null, + 'dataTransfer of "input" event for position changing of absolute position should be null'); + is(aEvent.getTargetRanges().length, 0, + 'getTargetRanges() of "input" event for position changing of absolute position should return empty array'); + } + + content.addEventListener("beforeinput", onBeforeInput); + content.addEventListener("input", onInput); + + // Click on the positioner. + synthesizeMouse(target, kPositionerX, kPositionerY, {type: "mousedown"}); + // Drag it delta pixels. + synthesizeMouse(target, kPositionerX + aDeltaX, kPositionerY + aDeltaY, {type: "mousemove"}); + // Release the mouse button + synthesizeMouse(target, kPositionerX + aDeltaX, kPositionerY + aDeltaY, {type: "mouseup"}); + + ok(beforeInputFired, `${description}"beforeinput" event should be fired by moving absolute position container`); + ok(inputFired, `${description}"input" event should be fired by moving absolute position container`); + + beforeInputEventExpected = false; + inputEventExpected = false; + + // Move the mouse delta more pixels to the same direction to make sure that the + // positioning operation has stopped. + synthesizeMouse(target, kPositionerX + aDeltaX * 2, kPositionerY + aDeltaY * 2, {type: "mousemove"}); + // Click outside of the image to hide the positioner. + synthesizeMouseAtCenter(outOfEditor, {}); + + content.removeEventListener("beforeinput", onBeforeInput); + content.removeEventListener("input", onInput); + + // Get the new dimensions for the absolute positioned element. + let newRect = target.getBoundingClientRect(); + isfuzzy(newRect.x, rect.x + aDeltaX, 1, description + "The left should be increased by " + aDeltaX + " pixels"); + isfuzzy(newRect.y, rect.y + aDeltaY, 1, description + "The top should be increased by " + aDeltaY + "pixels"); + } + + await testPositioner( 10, 10); + await testPositioner( 10, -10); + await testPositioner(-10, 10); + await testPositioner(-10, -10); + } + + const kTests = [ + { description: "Positioner for <img>", + innerHTML: "<img id=\"target\" src=\"green.png\">", + }, + { description: "Positioner for <table>", + innerHTML: "<table id=\"target\" border><tr><td>cell</td><td>cell</td></tr></table>", + }, + { description: "Positioner for <div>", + innerHTML: "<div id=\"target\">div element</div>", + }, + ]; + + for (const kTest of kTests) { + await doTest(kTest.description, kTest.innerHTML); + } + content.innerHTML = ""; + SimpleTest.finish(); +}); +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_backspace_vs.html b/editor/libeditor/tests/test_backspace_vs.html new file mode 100644 index 0000000000..d52d83f79e --- /dev/null +++ b/editor/libeditor/tests/test_backspace_vs.html @@ -0,0 +1,128 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1216427 +--> +<head> + <title>Test for Bug 1216427</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1216427">Mozilla Bug 1216427</a> +<p id="display"></p> +<div id="content"> + <div id="edit1" contenteditable="true">a☺️b</div><!-- BMP symbol with VS16 --> + <div id="edit2" contenteditable="true">a🌐︎b</div><!-- plane 1 symbol with VS15 --> + <div id="edit3" contenteditable="true">a㐂󠄀b</div><!-- BMP ideograph with VS17 --> + <div id="edit4" contenteditable="true">a𠀀󠄁b</div><!-- SMP ideograph with VS18 --> + <div id="edit5" contenteditable="true">a☺︁︂︃b</div><!-- BMP symbol with extra VSes --> + <div id="edit6" contenteditable="true">a𠀀󠄀󠄁󠄂b</div><!-- SMP symbol with extra VSes --> + <!-- The Regional Indicator combinations here were supported by Apple Color Emoji + even prior to the major extension of coverage in the 10.10.5 timeframe. --> + <div id="edit7" contenteditable="true">a🇨🇳b</div><!-- Regional Indicator flag: CN --> + <div id="edit8" contenteditable="true">a🇨🇳🇩🇪b</div><!-- two RI flags: CN, DE --> + <div id="edit9" contenteditable="true">a🇨🇳🇩🇪🇪🇸b</div><!-- three RI flags: CN, DE, ES --> + <div id="edit10" contenteditable="true">a🇨🇳🇩🇪🇪🇸🇫🇷b</div><!-- four RI flags: CN, DE, ES, FR --> + <div id="edit11" contenteditable="true">a🇨🇳🇩🇪🇪🇸🇫🇷🇬🇧b</div><!-- five RI flags: CN, DE, ES, FR, GB --> + + <div id="edit1b" contenteditable="true">a☺️b</div><!-- BMP symbol with VS16 --> + <div id="edit2b" contenteditable="true">a🌐︎b</div><!-- plane 1 symbol with VS15 --> + <div id="edit3b" contenteditable="true">a㐂󠄀b</div><!-- BMP ideograph with VS17 --> + <div id="edit4b" contenteditable="true">a𠀀󠄁b</div><!-- SMP ideograph with VS18 --> + <div id="edit5b" contenteditable="true">a☺︁︂︃b</div><!-- BMP symbol with extra VSes --> + <div id="edit6b" contenteditable="true">a𠀀󠄀󠄁󠄂b</div><!-- SMP symbol with extra VSes --> + <div id="edit7b" contenteditable="true">a🇨🇳b</div><!-- Regional Indicator flag: CN --> + <div id="edit8b" contenteditable="true">a🇨🇳🇩🇪b</div><!-- two RI flags: CN, DE --> + <div id="edit9b" contenteditable="true">a🇨🇳🇩🇪🇪🇸b</div><!-- three RI flags: CN, DE, ES --> + <div id="edit10b" contenteditable="true">a🇨🇳🇩🇪🇪🇸🇫🇷b</div><!-- four RI flags: CN, DE, ES, FR --> + <div id="edit11b" contenteditable="true">a🇨🇳🇩🇪🇪🇸🇫🇷🇬🇧b</div><!-- five RI flags: CN, DE, ES, FR, GB --> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 1216427 **/ + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTest); + +function test(edit, bsCount) { + edit.focus(); + var sel = window.getSelection(); + sel.collapse(edit.childNodes[0], edit.textContent.length - 1); + for (let i = 0; i < bsCount; ++i) { + synthesizeKey("KEY_Backspace"); + } + is(edit.textContent, "ab", "The backspace key should delete the characters correctly"); +} + +function testWithMove(edit, offset, bsCount) { + edit.focus(); + var sel = window.getSelection(); + sel.collapse(edit.childNodes[0], 0); + var i; + for (i = 0; i < offset; ++i) { + synthesizeKey("KEY_ArrowRight"); + synthesizeKey("KEY_ArrowLeft"); + synthesizeKey("KEY_ArrowRight"); + } + synthesizeKey("KEY_Backspace", {repeat: bsCount}); + is(edit.textContent, "ab", "The backspace key should delete the characters correctly"); +} + +function runTest() { + /* test backspace-deletion of the middle character(s) */ + test(document.getElementById("edit1"), 1); + test(document.getElementById("edit2"), 1); + test(document.getElementById("edit3"), 1); + test(document.getElementById("edit4"), 1); + test(document.getElementById("edit5"), 1); + test(document.getElementById("edit6"), 1); + + /* + * Tests with Regional Indicator flags: these behave differently depending + * whether an emoji font is present, as ligated flags are edited as single + * characters whereas non-ligated RI characters act individually. + * + * For now, only rely on such an emoji font on OS X 10.7+. (Note that the + * Segoe UI Emoji font on Win8.1 and Win10 does not implement Regional + * Indicator flags.) + * + * Once the Firefox Emoji font is ready, we can load that via @font-face + * and expect these tests to work across all platforms. + */ + let hasEmojiFont = + (navigator.platform.indexOf("Mac") == 0 && + /10\.([7-9]|[1-9][0-9])/.test(navigator.oscpu)); + + if (hasEmojiFont) { + test(document.getElementById("edit7"), 1); + test(document.getElementById("edit8"), 2); + test(document.getElementById("edit9"), 3); + test(document.getElementById("edit10"), 4); + test(document.getElementById("edit11"), 5); + } + + /* extra tests with the use of RIGHT and LEFT to get to the right place */ + testWithMove(document.getElementById("edit1b"), 2, 1); + testWithMove(document.getElementById("edit2b"), 2, 1); + testWithMove(document.getElementById("edit3b"), 2, 1); + testWithMove(document.getElementById("edit4b"), 2, 1); + testWithMove(document.getElementById("edit5b"), 2, 1); + testWithMove(document.getElementById("edit6b"), 2, 1); + if (hasEmojiFont) { + testWithMove(document.getElementById("edit7b"), 2, 1); + testWithMove(document.getElementById("edit8b"), 3, 2); + testWithMove(document.getElementById("edit9b"), 4, 3); + testWithMove(document.getElementById("edit10b"), 5, 4); + testWithMove(document.getElementById("edit11b"), 6, 5); + } + + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1026397.html b/editor/libeditor/tests/test_bug1026397.html new file mode 100644 index 0000000000..ef54befe57 --- /dev/null +++ b/editor/libeditor/tests/test_bug1026397.html @@ -0,0 +1,101 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1026397 +--> +<head> + <title>Test for Bug 1026397</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1026397">Mozilla Bug 1026397</a> +<p id="display"></p> +<div id="content"> +<input id="input"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 1026397 **/ +SimpleTest.waitForExplicitFinish(); + +function runTests() { + var input = document.getElementById("input"); + input.focus(); + + function doTest(aMaxLength, aInitialValue, aCaretOffset, + aInsertString, aExpectedValueDuringComposition, + aExpectedValueAfterCompositionEnd, aAdditionalExplanation) { + input.value = aInitialValue; + var maxLengthStr = ""; + if (aMaxLength >= 0) { + input.maxLength = aMaxLength; + maxLengthStr = aMaxLength.toString(); + } else { + input.removeAttribute("maxlength"); + maxLengthStr = "not specified"; + } + input.selectionStart = input.selectionEnd = aCaretOffset; + if (aAdditionalExplanation) { + aAdditionalExplanation = " " + aAdditionalExplanation; + } else { + aAdditionalExplanation = ""; + } + + synthesizeCompositionChange( + { "composition": + { "string": aInsertString, + "clauses": + [ + { "length": aInsertString.length, "attr": COMPOSITION_ATTR_RAW_CLAUSE }, + ], + }, + "caret": { "start": aInsertString.length, "length": 0 }, + }); + is(input.value, aExpectedValueDuringComposition, + "The value of input whose maxlength is " + maxLengthStr + " should be " + + aExpectedValueDuringComposition + " during composition" + aAdditionalExplanation); + synthesizeComposition({ type: "compositioncommitasis" }); + is(input.value, aExpectedValueAfterCompositionEnd, + "The value of input whose maxlength is " + maxLengthStr + " should be " + + aExpectedValueAfterCompositionEnd + " after compositionend" + aAdditionalExplanation); + } + + // maxlength hasn't been specified yet. + doTest(-1, "", 0, "\uD842\uDFB7\u91CE\u5BB6", "\uD842\uDFB7\u91CE\u5BB6", "\uD842\uDFB7\u91CE\u5BB6"); + + // maxlength="1" + doTest(1, "", 0, "\uD842\uDFB7\u91CE\u5BB6", "\uD842\uDFB7\u91CE\u5BB6", ""); + + // maxlength="2" + doTest(2, "", 0, "\uD842\uDFB7\u91CE\u5BB6", "\uD842\uDFB7\u91CE\u5BB6", "\uD842\uDFB7"); + doTest(2, "X", 1, "\uD842\uDFB7\u91CE\u5BB6", "X\uD842\uDFB7\u91CE\u5BB6", "X"); + doTest(2, "Y", 0, "\uD842\uDFB7\u91CE\u5BB6", "\uD842\uDFB7\u91CE\u5BB6Y", "Y"); + + // maxlength="3" + doTest(3, "", 0, "\uD842\uDFB7\u91CE\u5BB6", "\uD842\uDFB7\u91CE\u5BB6", "\uD842\uDFB7\u91CE"); + doTest(3, "A", 1, "\uD842\uDFB7\u91CE\u5BB6", "A\uD842\uDFB7\u91CE\u5BB6", "A\uD842\uDFB7"); + doTest(3, "B", 0, "\uD842\uDFB7\u91CE\u5BB6", "\uD842\uDFB7\u91CE\u5BB6B", "\uD842\uDFB7B"); + doTest(3, "CD", 1, "\uD842\uDFB7\u91CE\u5BB6", "C\uD842\uDFB7\u91CE\u5BB6D", "CD"); + + // maxlength="4" + doTest(4, "EF", 1, "\uD842\uDFB7\u91CE\u5BB6", "E\uD842\uDFB7\u91CE\u5BB6F", "E\uD842\uDFB7F"); + doTest(4, "GHI", 1, "\uD842\uDFB7\u91CE\u5BB6", "G\uD842\uDFB7\u91CE\u5BB6HI", "GHI"); + + // maxlength="1", inputting only high surrogate + doTest(1, "", 0, "\uD842", "\uD842", "\uD842", "even if input string is only a high surrogate"); + + // maxlength="1", inputting only low surrogate + doTest(1, "", 0, "\uDFB7", "\uDFB7", "\uDFB7", "even if input string is only a low surrogate"); + + SimpleTest.finish(); +} + +SimpleTest.waitForFocus(runTests); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1053048.html b/editor/libeditor/tests/test_bug1053048.html new file mode 100644 index 0000000000..4f9df5e602 --- /dev/null +++ b/editor/libeditor/tests/test_bug1053048.html @@ -0,0 +1,71 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1053048 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1053048</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> + <script type="application/javascript"> + + /** Test for Bug 1053048 **/ + SimpleTest.waitForExplicitFinish(); + SimpleTest.waitForFocus(runTests); + + const nsISelectionListener = SpecialPowers.Ci.nsISelectionListener; + + async function runTests() { + var textarea = SpecialPowers.wrap(document.getElementById("textarea")); + textarea.focus(); + + var editor = textarea.editor; + var selectionPrivate = editor.selection; + + // Move caret to the end of the textarea + synthesizeMouse(textarea, 290, 10, {}); + is(textarea.selectionStart, 3, "selectionStart should be 3 (after \"foo\")"); + is(textarea.selectionEnd, 3, "selectionEnd should be 3 (after \"foo\")"); + + // This test **was** trying to check whether a selection listener which + // runs while an editor handles an edit action does not stop handling it. + // However, this selection listener caught previous selection change + // notification immediately before synthesizing the `Enter` key press + // unexpectedly. And now, selection listener may not run immediately after + // synthesizing the key press. So, we don't need to check whether a + // notification actually comes here. + let selectionListener = { + notifySelectionChanged(aDocument, aSelection, aReason, aAmount) { + ok(true, "selectionStart: " + textarea.selectionStart); + ok(true, "selectionEnd: " + textarea.selectionEnd); + }, + }; + selectionPrivate.addSelectionListener(selectionListener); + synthesizeKey("KEY_Enter"); + is(textarea.selectionStart, 4, "selectionStart should be 4"); + is(textarea.selectionEnd, 4, "selectionEnd should be 4"); + is(textarea.value, "foo\n", "The line break should be appended"); + selectionPrivate.removeSelectionListener(selectionListener); + + SimpleTest.finish(); + } + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1053048">Mozilla Bug 1053048</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> + +<textarea id="textarea" + style="height: 100px; width: 300px; -moz-appearance: none" + spellcheck="false" + onkeydown="this.style.display='block'; this.style.height='200px';">foo</textarea> + +<pre id="test"> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1068979.html b/editor/libeditor/tests/test_bug1068979.html new file mode 100644 index 0000000000..189be35f91 --- /dev/null +++ b/editor/libeditor/tests/test_bug1068979.html @@ -0,0 +1,72 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1068979 +--> +<head> + <title>Test for Bug 1068979</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1068979">Mozilla Bug 1068979</a> +<p id="display"></p> +<div id="content"> + <div id="editor1" contenteditable="true">𝐀</div> + <div id="editor2" contenteditable="true">a<u>𝐁</u>b</div> + <div id="editor3" contenteditable="true">a𝐂<u>b</u></div> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 1068979 **/ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + // Test backspacing over SMP characters pasted-in to a contentEditable + getSelection().selectAllChildren(document.getElementById("editor1")); + var ed1 = document.getElementById("editor1"); + var ch1 = ed1.textContent; + ed1.focus(); + synthesizeKey("C", {accelKey: true}); + synthesizeKey("V", {accelKey: true}); + synthesizeKey("V", {accelKey: true}); + synthesizeKey("V", {accelKey: true}); + synthesizeKey("V", {accelKey: true}); + is(ed1.textContent, ch1 + ch1 + ch1 + ch1, "Should have four SMP characters"); + sendKey("back_space"); + is(ed1.textContent, ch1 + ch1 + ch1, "Three complete characters should remain"); + sendKey("back_space"); + is(ed1.textContent, ch1 + ch1, "Two complete characters should remain"); + sendKey("back_space"); + is(ed1.textContent, ch1, "Only one complete SMP character should remain"); + ed1.blur(); + + // Test backspacing across an SMP character in a sub-element + getSelection().selectAllChildren(document.getElementById("editor2")); + var ed2 = document.getElementById("editor2"); + ed2.focus(); + sendKey("right"); + sendKey("back_space"); + sendKey("back_space"); + is(ed2.textContent, "a", "Only the 'a' should remain"); + ed2.blur(); + + // Test backspacing across an SMP character from a following sub-element + getSelection().selectAllChildren(document.getElementById("editor3")); + var ed3 = document.getElementById("editor3"); + ed3.focus(); + sendKey("right"); + sendKey("left"); + sendKey("back_space"); + is(ed3.textContent, "ab", "The letters 'ab' should remain"); + ed3.blur(); + + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1094000.html b/editor/libeditor/tests/test_bug1094000.html new file mode 100644 index 0000000000..f41ebeeedb --- /dev/null +++ b/editor/libeditor/tests/test_bug1094000.html @@ -0,0 +1,147 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1094000 +--> +<head> + <title>Test for Bug 1094000</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1094000">Mozilla Bug 1094000</a> +<p id="display"></p> +<div id="content"> + <div id="editor0" contenteditable></div> + <div id="editor1" contenteditable><br></div> + <div id="editor2" contenteditable>a</div> + <div id="editor3" contenteditable>b</div> + <div id="editor4" contenteditable>c</div> + <div id="editor5" contenteditable>d</div> + <div id="editor6" contenteditable>e</div> + <div id="editor7" contenteditable><span>f</span></div> + <div id="editor8" contenteditable><span>g</span></div> + <div id="editor9" contenteditable><span>h</span></div> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 1094000 **/ + +SimpleTest.waitForExplicitFinish(); + +const kIsLinux = navigator.platform.indexOf("Linux") == 0; + +function runTests() { + var editor0 = document.getElementById("editor0"); + var editor1 = document.getElementById("editor1"); + var editor2 = document.getElementById("editor2"); + var editor3 = document.getElementById("editor3"); + var editor4 = document.getElementById("editor4"); + var editor5 = document.getElementById("editor5"); + var editor6 = document.getElementById("editor6"); + var editor7 = document.getElementById("editor7"); + var editor8 = document.getElementById("editor8"); + var editor9 = document.getElementById("editor9"); + + ok(Math.abs(editor1.getBoundingClientRect().height - editor0.getBoundingClientRect().height) <= 1, + "an editor having a <br> element and an empty editor should be same height"); + ok(Math.abs(editor1.getBoundingClientRect().height - editor2.getBoundingClientRect().height) <= 1, + "an editor having only a <br> element and an editor having \"a\" should be same height"); + + editor2.focus(); + synthesizeKey("KEY_ArrowRight"); + synthesizeKey("KEY_Backspace"); + is(editor2.innerHTML, "<br>", + "an editor which had \"a\" should have only <br> element after Backspace keypress"); + ok(Math.abs(editor2.getBoundingClientRect().height - editor1.getBoundingClientRect().height) <= 1, + "an editor whose content was removed by Backspace key should have a place to put a caret"); + + editor3.focus(); + synthesizeKey("KEY_ArrowLeft"); + synthesizeKey("KEY_Delete"); + is(editor3.innerHTML, "<br>", + "an editor which had \"b\" should have only <br> element after Delete keypress"); + ok(Math.abs(editor3.getBoundingClientRect().height - editor1.getBoundingClientRect().height) <= 1, + "an editor whose content was removed by Delete key should have a place to put a caret"); + + editor4.focus(); + window.getSelection().selectAllChildren(editor4); + synthesizeKey("KEY_Backspace"); + is(editor4.innerHTML, "<br>", + "an editor which had \"c\" should have only <br> element after removing selected text with Backspace key"); + ok(Math.abs(editor4.getBoundingClientRect().height - editor1.getBoundingClientRect().height) <= 1, + "an editor whose content was selected and removed by Backspace key should have a place to put a caret"); + + editor5.focus(); + window.getSelection().selectAllChildren(editor5); + synthesizeKey("KEY_Delete"); + is(editor5.innerHTML, "<br>", + "an editor which had \"d\" should have only <br> element after removing selected text with Delete key"); + ok(Math.abs(editor5.getBoundingClientRect().height - editor1.getBoundingClientRect().height) <= 1, + "an editor whose content was selected and removed by Delete key should have a place to put a caret"); + + editor6.focus(); + window.getSelection().selectAllChildren(editor6); + synthesizeKey("x", {accelKey: true}); + is(editor6.innerHTML, "<br>", + "an editor which had \"e\" should have only <br> element after removing selected text by \"Cut\""); + ok(Math.abs(editor6.getBoundingClientRect().height - editor1.getBoundingClientRect().height) <= 1, + "an editor whose content was selected and removed by \"Cut\" should have a place to put a caret"); + + editor7.focus(); + synthesizeKey("KEY_ArrowRight"); + synthesizeKey("KEY_Backspace"); + is( + editor7.innerHTML, + SpecialPowers.getBoolPref("editor.white_space_normalization.blink_compatible") + ? "<br>" + : "<span><br></span>", + "an editor which had \"f\" in a <span> element should have only <br> element after Backspace keypress" + ); + ok(Math.abs(editor7.getBoundingClientRect().height - editor1.getBoundingClientRect().height) <= 1, + "an editor whose content was removed by Backspace key should have a place to put a caret"); + + editor8.focus(); + synthesizeKey("KEY_ArrowLeft"); + synthesizeKey("KEY_Delete"); + todo_is(editor8.innerHTML, "<br>", + "an editor which had \"g\" in a <span> element should have only <br> element after Delete keypress"); + todo_isnot( + editor8.innerHTML, + SpecialPowers.getBoolPref("editor.white_space_normalization.blink_compatible") + ? "<br><span></span>" + : "<span><br></span>", + "an editor which had \"g\" in a <span> element should have only <br> element after Delete keypress" + ); + ok(Math.abs(editor8.getBoundingClientRect().height - editor1.getBoundingClientRect().height) <= 1, + "an editor whose content was removed by Backspace key should have a place to put a caret"); + + editor9.focus(); + window.getSelection().selectAllChildren(editor9.querySelector("span")); + synthesizeKey("KEY_Backspace"); + todo_is(editor9.innerHTML, "<br>", + "an editor which had \"h\" in a <span> element should have only <br> element after removing selected text with Backspace key"); + todo_isnot(editor9.innerHTML, "<span><br></span>", + "an editor which had \"h\" in a <span> element should have only <br> element after removing selected text with Backspace key"); + ok(Math.abs(editor9.getBoundingClientRect().height - editor1.getBoundingClientRect().height) <= 1, + "an editor whose content was removed by Backspace key should have a place to put a caret"); + + editor0.focus(); + synthesizeKey("KEY_Backspace"); + is(editor0.innerHTML, "", + "an empty editor should keep being empty even if Backspace key is pressed"); + synthesizeKey("KEY_Delete"); + is(editor0.innerHTML, "", + "an empty editor should keep being empty even if Delete key is pressed"); + + SimpleTest.finish(); +} + +SimpleTest.waitForFocus(runTests); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1102906.html b/editor/libeditor/tests/test_bug1102906.html new file mode 100644 index 0000000000..26b0592637 --- /dev/null +++ b/editor/libeditor/tests/test_bug1102906.html @@ -0,0 +1,51 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1102906 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1102906</title> + + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> + + <script> + "use strict"; + + /* Test for Bug 1102906 */ + /* The caret should be movable by using keyboard after drag-and-drop. */ + + SimpleTest.waitForExplicitFinish(); + SimpleTest.waitForFocus( () => { + let content = document.getElementById("content"); + let drag = document.getElementById("drag"); + let selection = window.getSelection(); + + /* Perform drag-and-drop for an arbitrary content. The caret should be at + the end of the contenteditable. */ + selection.selectAllChildren(drag); + synthesizeDrop(drag, content, {}, "copy"); + + let textContentAfterDrop = content.textContent; + + /* Move the caret to the front of the contenteditable by using keyboard. */ + for (let i = 0; i < content.textContent.length; ++i) { + sendKey("LEFT"); + } + sendChar("!"); + + is(content.textContent, "!" + textContentAfterDrop, + "The exclamation mark should be inserted at the front."); + + SimpleTest.finish(); + }); + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1102906">Mozilla Bug 1102906</a> +<div id="content" contenteditable="true"><span id="drag">Drag</span></div> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1109465.html b/editor/libeditor/tests/test_bug1109465.html new file mode 100644 index 0000000000..97bc6a71e9 --- /dev/null +++ b/editor/libeditor/tests/test_bug1109465.html @@ -0,0 +1,65 @@ +<!DOCTYPE> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1109465 +--> +<head> + <title>Test for Bug 1109465</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<div id="display"> + <textarea></textarea> +</div> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + +/** Test for Bug 1109465 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var t = document.querySelector("textarea"); + t.focus(); + + // Type foo\nbar and place the caret at the end of the last line + sendString("foo"); + synthesizeKey("KEY_Enter"); + sendString("bar"); + synthesizeKey("KEY_ArrowUp"); + is(t.selectionStart, 3, "Correct start of selection"); + is(t.selectionEnd, 3, "Correct end of selection"); + + // Compose an IME string + var composingString = "\u306B"; + // FYI: "compositionstart" will be dispatched automatically. + synthesizeCompositionChange( + { "composition": + { "string": composingString, + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }, + ], + }, + "caret": { "start": 1, "length": 0 }, + }); + synthesizeComposition({ type: "compositioncommitasis" }); + is(t.value, "foo\u306B\nbar", "Correct value after composition"); + + // Now undo to test that the transaction merger has correctly detected the + // IMETextTxn. + synthesizeKey("Z", {accelKey: true}); + is(t.value, "foo\nbar", "Correct value after undo"); + + SimpleTest.finish(); +}); + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_bug1130651.html b/editor/libeditor/tests/test_bug1130651.html new file mode 100644 index 0000000000..a9e1bad8c2 --- /dev/null +++ b/editor/libeditor/tests/test_bug1130651.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<title>Test for Bug 1130651</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1332876">Mozilla Bug 1332876</a> +<div contenteditable>a b</div> +<script> +var div = document.querySelector("div"); +div.focus(); +getSelection().collapse(div.firstChild, 2); +try { + document.execCommand("inserttext", false, "\n"); + ok(true, "No exception thrown"); +} catch (e) { + ok(false, "Exception: " + e); +} +</script> diff --git a/editor/libeditor/tests/test_bug1140105.html b/editor/libeditor/tests/test_bug1140105.html new file mode 100644 index 0000000000..9096f4f99c --- /dev/null +++ b/editor/libeditor/tests/test_bug1140105.html @@ -0,0 +1,64 @@ +<!DOCTYPE> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1140105 +--> +<head> + <title>Test for Bug 1140105</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> +<div id="display"> +</div> + +<div id="content" contenteditable><font face="Arial">1234567890</font></div> + +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + +/** Test for Bug 1140105 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var div = document.getElementById("content"); + div.focus(); + synthesizeMouseAtCenter(div, {}); + synthesizeKey("KEY_ArrowLeft"); + + var sel = window.getSelection(); + var selRange = sel.getRangeAt(0); + is(selRange.endContainer.nodeName, "#text", "selection should be in text node"); + is(selRange.endOffset, 9, "offset should be 9"); + + var firstHas = {}; + var anyHas = {}; + var allHas = {}; + var editor = getEditor(); + + editor.getInlinePropertyWithAttrValue("font", "face", "Arial", firstHas, anyHas, allHas); + is(firstHas.value, true, "Test for Arial: firstHas: true expected"); + is(anyHas.value, true, "Test for Arial: anyHas: true expected"); + is(allHas.value, true, "Test for Arial: allHas: true expected"); + editor.getInlinePropertyWithAttrValue("font", "face", "Courier", firstHas, anyHas, allHas); + is(firstHas.value, false, "Test for Courier: firstHas: false expected"); + is(anyHas.value, false, "Test for Courier: anyHas: false expected"); + is(allHas.value, false, "Test for Courier: allHas: false expected"); + + SimpleTest.finish(); +}); + +function getEditor() { + var Ci = SpecialPowers.Ci; + var editingSession = SpecialPowers.wrap(window).docShell.editingSession; + var editor = editingSession.getEditorForWindow(window); + editor.QueryInterface(Ci.nsIHTMLEditor); + return editor; +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_bug1140617.html b/editor/libeditor/tests/test_bug1140617.html new file mode 100644 index 0000000000..e8a9923489 --- /dev/null +++ b/editor/libeditor/tests/test_bug1140617.html @@ -0,0 +1,37 @@ +<!doctype html> +<title>Mozilla Bug 1140617</title> +<link rel=stylesheet href="/tests/SimpleTest/test.css"> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1140617" + target="_blank">Mozilla Bug 1140617</a> +<iframe id="i1" width="200" height="100" src="about:blank"></iframe> +<img id="i" src="green.png"> +<script> +function runTest() { + SpecialPowers.setCommandNode(window, document.getElementById("i")); + SpecialPowers.doCommand(window, "cmd_copyImageContents"); + + var e = document.getElementById("i1"); + var doc = e.contentDocument; + doc.designMode = "on"; + doc.defaultView.focus(); + var selection = doc.defaultView.getSelection(); + selection.removeAllRanges(); + selection.selectAllChildren(doc.body); + selection.collapseToEnd(); + + doc.execCommand("fontname", false, "Arial"); + doc.execCommand("bold", false, null); + doc.execCommand("insertText", false, "12345"); + doc.execCommand("paste", false, null); + doc.execCommand("insertText", false, "a"); + + is(doc.queryCommandValue("fontname"), "Arial", "Arial expected"); + is(doc.queryCommandState("bold"), true, "Bold expected"); + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTest); +</script> diff --git a/editor/libeditor/tests/test_bug1151186.html b/editor/libeditor/tests/test_bug1151186.html new file mode 100644 index 0000000000..049287a135 --- /dev/null +++ b/editor/libeditor/tests/test_bug1151186.html @@ -0,0 +1,69 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1151186 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1151186</title> + <link rel=stylesheet href="/tests/SimpleTest/test.css"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script> + /** Test for Bug 1151186 **/ + SimpleTest.waitForExplicitFinish(); + + // In this test, we want to check the IME enabled state in the `contenteditable` + // editor which is focused by `focus` event listener of the document. + // However, according to the random oranges filed as bug 1176038 and bug 1611360, + // `focus` event are sometimes not fired on the document and the reason is, + // the document sometimes not focused automatically. Therefore, this test + // will set focus the document when `focus` event is not fired until next + // macro task. + var focusEventFired = false; + function onFocus(event) { + is(event.target.nodeName, "#document", "focus event should be fired on the document node"); + if (event.target != document) { + return; + } + focusEventFired = true; + document.getElementById("editor").focus(); + SimpleTest.executeSoon(runTests); + } + document.addEventListener("focus", onFocus, {once: true}); + + // Register next macro task to check whether `focus` event of the document + // is fired as expected. If not, let's focus our window manually. Then, + // the `focus` event listener starts the test anyway. + setTimeout(() => { + if (focusEventFired) { + return; // We've gotten `focus` event as expected. + } + ok(!document.hasFocus(), "The document should not have focus yet"); + info("Setting focus to the window forcibly..."); + window.focus(); + }, 0); + + function runTests() { + let description = focusEventFired ? + "document got focused normally" : + "document got focused forcibly"; + is(document.activeElement, document.getElementById("editor"), + `The div element should be focused (${description})`); + var utils = SpecialPowers.getDOMWindowUtils(window); + is(utils.IMEStatus, utils.IME_STATUS_ENABLED, + `IME should be enabled (${description})`); + SimpleTest.finish(); + } + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1151186">Mozilla Bug 1151186</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<div id="editor" contenteditable="true"></div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1153237.html b/editor/libeditor/tests/test_bug1153237.html new file mode 100644 index 0000000000..ea1863a059 --- /dev/null +++ b/editor/libeditor/tests/test_bug1153237.html @@ -0,0 +1,48 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1153237 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1153237</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + SimpleTest.waitForExplicitFinish(); + + // Avoid platform selection differences + SimpleTest.waitForFocus(function() { + SpecialPowers.pushPrefEnv({ + "set": [["layout.word_select.eat_space_to_next_word", true]], + }, runTests); + }); + + function runTests() { + var element = document.getElementById("editor"); + var sel = window.getSelection(); + + element.focus(); + is(sel.getRangeAt(0).startOffset, 0, "offset is zero"); + + SpecialPowers.doCommand(window, "cmd_selectRight2"); + is(sel.toString(), "Some ", + "first word + space is selected: got '" + sel.toString() + "'"); + + SpecialPowers.doCommand(window, "cmd_selectRight2"); + is(sel.toString(), "Some text", + "both words are selected: got '" + sel.toString() + "'"); + + SimpleTest.finish(); + } + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1153237">Mozilla Bug 1153237</a> +<div id="editor" contenteditable>Some text</div><span></span> + +<pre id="test"> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1162952.html b/editor/libeditor/tests/test_bug1162952.html new file mode 100644 index 0000000000..0b8287f157 --- /dev/null +++ b/editor/libeditor/tests/test_bug1162952.html @@ -0,0 +1,43 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1162952 +--> +<head> + <title>Test for Bug 1162952</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1162952">Mozilla Bug 1162952</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 1162952 **/ +var userCallbackRun = false; + +document.addEventListener("keydown", function() { + // During a user callback, the commands should be enabled + userCallbackRun = true; + is(true, document.queryCommandEnabled("cut")); + is(true, document.queryCommandEnabled("copy")); +}); + +// Otherwise, they should be disabled +is(false, document.queryCommandEnabled("cut")); +is(false, document.queryCommandEnabled("copy")); + +// Fire a user callback +sendString("A"); + +ok(userCallbackRun, "User callback should've been run"); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1181130-1.html b/editor/libeditor/tests/test_bug1181130-1.html new file mode 100644 index 0000000000..0b9710a3b2 --- /dev/null +++ b/editor/libeditor/tests/test_bug1181130-1.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1181130 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1181130</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1181130">Mozilla Bug 1181130</a> +<p id="display"></p> +<div id="container" contenteditable="true"> + editable div + <div id="noneditable" contenteditable="false"> + non-editable div + <div id="editable" contenteditable="true">nested editable div</div> + </div> +</div> +<script type="application/javascript"> +/** Test for Bug 1181130 **/ +var container = document.getElementById("container"); +var noneditable = document.getElementById("noneditable"); +var editable = document.getElementById("editable"); + +SimpleTest.waitForExplicitFinish(); + +SimpleTest.waitForFocus(function() { + synthesizeMouseAtCenter(noneditable, {}); + ok(!document.getSelection().toString().includes("nested editable div"), + "Selection should not include non-editable content"); + + synthesizeMouseAtCenter(container, {}); + ok(!document.getSelection().toString().includes("nested editable div"), + "Selection should not include non-editable content"); + + synthesizeMouseAtCenter(editable, {}); + ok(!document.getSelection().toString().includes("nested editable div"), + "Selection should not include non-editable content"); + + SimpleTest.finish(); +}); +</script> +<pre id="test"> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1181130-2.html b/editor/libeditor/tests/test_bug1181130-2.html new file mode 100644 index 0000000000..fa091b71a0 --- /dev/null +++ b/editor/libeditor/tests/test_bug1181130-2.html @@ -0,0 +1,44 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1181130 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1181130</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1181130">Mozilla Bug 1181130</a> +<p id="display"></p> +<div id="container" contenteditable="true"> + editable div + <div id="noneditable" contenteditable="false"> + non-editable div + </div> +</div> +<script type="application/javascript"> +/** Test for Bug 1181130 **/ +var container = document.getElementById("container"); +var noneditable = document.getElementById("noneditable"); + +SimpleTest.waitForExplicitFinish(); + +SimpleTest.waitForFocus(function() { + var nonHTMLElement = document.createElementNS("http://www.example.com", "element"); + nonHTMLElement.innerHTML = '<div contenteditable="true">nested editable div</div>'; + noneditable.appendChild(nonHTMLElement); + + synthesizeMouseAtCenter(noneditable, {}); + ok(!document.getSelection().toString().includes("nested editable div"), + "Selection should not include non-editable content"); + + SimpleTest.finish(); +}); +</script> +<pre id="test"> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1186799.html b/editor/libeditor/tests/test_bug1186799.html new file mode 100644 index 0000000000..f5f14eb9c0 --- /dev/null +++ b/editor/libeditor/tests/test_bug1186799.html @@ -0,0 +1,78 @@ +<!DOCTYPE> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1186799 +--> +<head> + <title>Test for Bug 1186799</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<div id="content"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1186799">Mozilla Bug 1186799</a> +<p id="display"></p> +<div id="content"> + <span id="span">span</span> + <div id="editor" contenteditable></div> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 1186799 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var span = document.getElementById("span"); + var editor = document.getElementById("editor"); + editor.focus(); + + synthesizeCompositionChange( + { "composition": + { "string": "\u3042", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }, + ], + }, + "caret": { "start": 1, "length": 0 }, + }); + + ok(isThereIMESelection(), "There should be IME selection"); + + var compositionEnd = false; + editor.addEventListener("compositionend", function() { compositionEnd = true; }, true); + + synthesizeMouseAtCenter(span, {}); + + ok(compositionEnd, "composition end should be fired at clicking outside of the editor"); + ok(!isThereIMESelection(), "There should be no IME selection"); + + SimpleTest.finish(); +}); + +function isThereIMESelection() { + var selCon = SpecialPowers.wrap(window). + docShell. + editingSession. + getEditorForWindow(window). + selectionController; + const kIMESelections = [ + SpecialPowers.Ci.nsISelectionController.SELECTION_IME_RAWINPUT, + SpecialPowers.Ci.nsISelectionController.SELECTION_IME_SELECTEDRAWTEXT, + SpecialPowers.Ci.nsISelectionController.SELECTION_IME_CONVERTEDTEXT, + SpecialPowers.Ci.nsISelectionController.SELECTION_IME_SELECTEDCONVERTEDTEXT, + ]; + for (var i = 0; i < kIMESelections.length; i++) { + var sel = selCon.getSelection(kIMESelections[i]); + if (sel && sel.rangeCount) { + return true; + } + } + return false; +} + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1230473.html b/editor/libeditor/tests/test_bug1230473.html new file mode 100644 index 0000000000..7d1ec43bb0 --- /dev/null +++ b/editor/libeditor/tests/test_bug1230473.html @@ -0,0 +1,103 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1230473 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1230473</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1230473">Mozilla Bug 1230473</a> +<input id="input"> +<textarea id="textarea"></textarea> +<div id="div" contenteditable></div> +<script type="application/javascript"> +/** Test for Bug 1230473 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + function runTest(aEditor) { + function committer() { + aEditor.blur(); + aEditor.focus(); + } + function isNSEditableElement() { + return aEditor.tagName.toLowerCase() == "input" || aEditor.tagName.toLowerCase() == "textarea"; + } + function value() { + return isNSEditableElement() ? aEditor.value : aEditor.textContent; + } + function isComposing() { + return isNSEditableElement() ? SpecialPowers.wrap(aEditor) + .editor + .composing : + SpecialPowers.wrap(window) + .docShell + .editor + .composing; + } + function clear() { + if (isNSEditableElement()) { + aEditor.value = ""; + } else { + aEditor.textContent = ""; + } + } + + clear(); + + // FYI: Chrome commits composition if blur() and focus() are called during + // composition. But note that if they are called by compositionupdate + // listener, the behavior is unstable. On Windows, composition is + // canceled. On Linux and macOS, the composition is committed + // internally but the string keeps underlined. If they are called + // by input event listener, committed on any platforms though. + // On the other hand, Edge and Safari keeps composition even with + // calling both blur() and focus(). + + // Committing at compositionstart + aEditor.focus(); + aEditor.addEventListener("compositionstart", committer, true); + synthesizeCompositionChange({ composition: { string: "a", clauses: [{length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }] }, + caret: { start: 1, length: 0 }, key: { key: "a" }}); + aEditor.removeEventListener("compositionstart", committer, true); + ok(!isComposing(), "composition in " + aEditor.id + " should be committed by compositionstart event handler"); + is(value(), "", "composition in " + aEditor.id + " shouldn't insert any text since it's committed at compositionstart"); + clear(); + + // Committing at first compositionupdate + aEditor.focus(); + aEditor.addEventListener("compositionupdate", committer, true); + synthesizeCompositionChange({ composition: { string: "a", clauses: [{length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }] }, + caret: { start: 1, length: 0 }, key: { key: "a" }}); + aEditor.removeEventListener("compositionupdate", committer, true); + ok(!isComposing(), "composition in " + aEditor.id + " should be committed by compositionupdate event handler"); + is(value(), "a", "composition in " + aEditor.id + " should have \"a\" since IME committed with it"); + clear(); + + // Committing at second compositionupdate + aEditor.focus(); + // FYI: "compositionstart" will be dispatched automatically. + synthesizeCompositionChange({ composition: { string: "a", clauses: [{length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }] }, + caret: { start: 1, length: 0 }, key: { key: "a" }}); + ok(isComposing(), "composition should be in " + aEditor.id + " before dispatching second compositionupdate"); + is(value(), "a", "composition in " + aEditor.id + " should be 'a' before dispatching second compositionupdate"); + aEditor.addEventListener("compositionupdate", committer, true); + synthesizeCompositionChange({ composition: { string: "ab", clauses: [{length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE }] }, + caret: { start: 2, length: 0 }, key: { key: "b" }}); + aEditor.removeEventListener("compositionupdate", committer, true); + ok(!isComposing(), "composition in " + aEditor.id + " should be committed by compositionupdate event handler"); + is(value(), "ab", "composition in " + aEditor.id + " should have \"ab\" since IME committed with it"); + clear(); + } + runTest(document.getElementById("input")); + runTest(document.getElementById("textarea")); + runTest(document.getElementById("div")); + SimpleTest.finish(); +}); +</script> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1247483.html b/editor/libeditor/tests/test_bug1247483.html new file mode 100644 index 0000000000..c8b45b4439 --- /dev/null +++ b/editor/libeditor/tests/test_bug1247483.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html><head> +<title>Test for bug 1247483</title> +<style src="/tests/SimpleTest/test.css" type="text/css"></style> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> + +<script class="testbody" type="application/javascript"> + +function runTest() { + // Copy content from table. + var selection = getSelection(); + var startRange = document.createRange(); + startRange.setStart(document.getElementById("start"), 0); + startRange.setEnd(document.getElementById("end"), 2); + selection.removeAllRanges(); + selection.addRange(startRange); + SpecialPowers.wrap(document).execCommand("copy", false, null); + + // Paste content into "pastecontainer" + var pasteContainer = document.getElementById("pastecontainer"); + var pasteRange = document.createRange(); + pasteRange.selectNodeContents(pasteContainer); + pasteRange.collapse(false); + selection.removeAllRanges(); + selection.addRange(pasteRange); + SpecialPowers.wrap(document).execCommand("paste", false, null); + + is(pasteContainer.querySelectorAll("td").length, 4, "4 <td> should be pasted."); + + document.execCommand("undo", false, null); + + is(pasteContainer.querySelectorAll("td").length, 0, "Undo should have remove the 4 pasted <td>."); + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(runTest); +</script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1247483">Mozilla Bug 1247483</a> +<p id="display"></p> + +<pre id="test"> +</pre> + +<div id="container" contenteditable="true"> +<table> + <tr id="start"><td>1 1</td><td>1 2</td></tr> + <tr id="end"><td>2 1</td><td>2 2</td></tr> +</table> +</div> + +<div id="pastecontainer" contenteditable="true"> +</div> + +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1248128.html b/editor/libeditor/tests/test_bug1248128.html new file mode 100644 index 0000000000..30c39bcff8 --- /dev/null +++ b/editor/libeditor/tests/test_bug1248128.html @@ -0,0 +1,52 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1248128 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1248128</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + SimpleTest.waitForExplicitFinish(); + + SimpleTest.waitForFocus(function() { + var outer = document.querySelector("html"); + ok(outer.scrollTop == 0, "scrollTop is zero: got " + outer.scrollTop); + + var input = document.getElementById("testInput"); + input.focus(); + + var scroll = outer.scrollTop; + ok(scroll > 0, "element has scrolled: new value " + scroll); + + try { + SpecialPowers.doCommand(window, "cmd_moveLeft"); + ok(false, "should not be able to do kMoveLeft"); + } catch (e) { + ok(true, "unable to perform kMoveLeft"); + } + + ok(outer.scrollTop == scroll, + "scroll is unchanged: got " + outer.scrollTop + ", expected " + scroll); + + // Make sure cmd_moveLeft isn't failing for some unrelated reason + sendString("a"); + is(input.selectionStart, 1, "selectionStart after typing"); + SpecialPowers.doCommand(window, "cmd_moveLeft"); + is(input.selectionStart, 0, "selectionStart after move left"); + + SimpleTest.finish(); + }); + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1248128">Mozilla Bug 1248128</a> +<div style="height: 2000px;"></div> +<input type="text" id="testInput"></input> +<div style="height: 200px;"></div> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1248185.html b/editor/libeditor/tests/test_bug1248185.html new file mode 100644 index 0000000000..d18b62b025 --- /dev/null +++ b/editor/libeditor/tests/test_bug1248185.html @@ -0,0 +1,56 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1248185 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1248185</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + SimpleTest.waitForExplicitFinish(); + + // Avoid platform selection differences + SimpleTest.waitForFocus(function() { + SpecialPowers.pushPrefEnv({ + "set": [["layout.word_select.eat_space_to_next_word", true]], + }, runTests); + }); + + function runTests() { + var editor = document.querySelector("#test"); + editor.focus(); + + var sel = window.getSelection(); + + SpecialPowers.doCommand(window, "cmd_moveRight2"); + SpecialPowers.doCommand(window, "cmd_moveRight2"); + SpecialPowers.doCommand(window, "cmd_moveRight2"); + SpecialPowers.doCommand(window, "cmd_selectRight2"); + ok(sel.toString() == "three ", "expected 'three ' to be selected"); + + SpecialPowers.doCommand(window, "cmd_moveRight2"); + SpecialPowers.doCommand(window, "cmd_moveRight2"); + SpecialPowers.doCommand(window, "cmd_moveRight2"); + ok(sel.toString() == "", "expected empty selection"); + + SpecialPowers.doCommand(window, "cmd_selectLeft2"); + ok(sel.toString() == "five", "expected 'five' to be selected"); + + SimpleTest.finish(); + } + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1248185">Mozilla Bug 1248185</a> +<body> +<div style="font: 12px monospace; width: 45ch;"> +<span contenteditable="" id="test">blablablablablablablablablablablablablabla one two three four five</span> +<div> +<span>foo</span> +</div> +</div> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1248186.html b/editor/libeditor/tests/test_bug1248186.html new file mode 100644 index 0000000000..f23e905b65 --- /dev/null +++ b/editor/libeditor/tests/test_bug1248186.html @@ -0,0 +1,56 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1248186 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1248186</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + + SimpleTest.waitForFocus(function () { + let editor0 = document.getElementById("editor0"); + let editor1 = document.getElementById("editor1"); + let editor2 = document.getElementById("editor2"); + editor0.focus(); + for (let i = 0; i < 5; i++) { + synthesizeKey("KEY_ArrowRight"); + is(document.activeElement, editor0, "hitting right should not de-focus the editor"); + } + editor1.focus(); + for (let i = 0; i < 5; i++) { + synthesizeKey("KEY_ArrowRight"); + is(document.activeElement, editor1, "hitting right should not de-focus the editor"); + } + editor2.focus(); + for (let i = 0; i < 8; i++) { + synthesizeKey("KEY_ArrowRight"); + is(document.activeElement, editor2, "hitting right should not de-focus the editor"); + } + // make sure we don't get stuck at the end of the "foo" span + let selection = getSelection(); + is(selection.focusNode.parentElement.id, "bar"); + is(selection.focusOffset, 3); + + SimpleTest.finish(); + }); + </script> +</head> +<body> +<div> + <span id="editor0" contenteditable>foo</span> + <span></span> +</div> +<div> + <span id="editor1" contenteditable><b>foo</b></span> + <span></span> +</div> +<span id="editor2" contenteditable> + <span>foo</span><br> + <span id="bar">bar</span></span> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1250010.html b/editor/libeditor/tests/test_bug1250010.html new file mode 100644 index 0000000000..4113ecdb29 --- /dev/null +++ b/editor/libeditor/tests/test_bug1250010.html @@ -0,0 +1,87 @@ +<!DOCTYPE> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1250010 +--> +<head> + <title>Test for Bug 1250010</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> +<div id="display"> +</div> + +<div id="test1" contenteditable><p><b><font color="red">1234567890</font></b></p></div> +<div id="test2" contenteditable><p><tt>xyz</tt></p><p><tt><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAIAAABvrngfAAAAFklEQVQImWMwjWhCQwxECoW3oCHihAB0LyYv5/oAHwAAAABJRU5ErkJggg=="></tt></p></div> + +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + +function getImageDataURI() { + return document.getElementsByTagName("img")[0].getAttribute("src"); +} + +/** Test for Bug 1250010 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + // First test: Empty paragraph is split correctly. + var div = document.getElementById("test1"); + div.focus(); + synthesizeMouseAtCenter(div, {}); + + var sel = window.getSelection(); + var selRange = sel.getRangeAt(0); + is(selRange.endContainer.nodeName, "#text", "selection should be at the end of text node"); + is(selRange.endOffset, 10, "offset should be 10"); + + synthesizeKey("KEY_Enter"); + synthesizeKey("KEY_Enter"); + sendString("b"); + synthesizeKey("KEY_ArrowUp"); + sendString("a"); + + is(div.innerHTML, "<p><b><font color=\"red\">1234567890</font></b></p>" + + "<p><b><font color=\"red\">a<br></font></b></p>" + + "<p><b><font color=\"red\">b<br></font></b></p>", + "unexpected HTML"); + + // Second test: Since we modified the code path that splits non-text nodes, + // test that this works, if the split node is not empty. + div = document.getElementById("test2"); + div.focus(); + synthesizeMouseAtCenter(div, {}); + + selRange = sel.getRangeAt(0); + is(selRange.endContainer.nodeName, "#text", "selection should be at the end of text node"); + is(selRange.endOffset, 3, "offset should be 3"); + + // Move behind the image and press enter, insert an "A". + // That should insert a new empty paragraph with the "A" after what we have. + synthesizeKey("KEY_ArrowRight"); + synthesizeKey("KEY_ArrowRight"); + synthesizeKey("KEY_Enter"); + sendString("A"); + + // TODO: The <br> in the new paragraph and the paragraph containing the <img> + // should be removed at typing "A" because they are not necessary + // anymore to make the paragraphs visible (bug 503838). + const expectedHTML = + "<p><tt>xyz</tt></p><p><tt><img src=\"" + getImageDataURI() + "\"><br></tt></p>" + + "<p><tt>A<br></tt></p>"; + is( + div.innerHTML, + expectedHTML, + "Pressing Enter after the <img> should create new paragraph and which contain <tt> and new text should be inserted in it" + ); + + SimpleTest.finish(); +}); + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_bug1257363.html b/editor/libeditor/tests/test_bug1257363.html new file mode 100644 index 0000000000..c059633483 --- /dev/null +++ b/editor/libeditor/tests/test_bug1257363.html @@ -0,0 +1,180 @@ +<!DOCTYPE> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1257363 +--> +<head> + <title>Test for Bug 1257363</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> +<div id="display"> +</div> + +<div id="backspaceCSS" contenteditable><p style="color:red;">12345</p>67</div> +<div id="backspace" contenteditable><p><font color="red">12345</font></p>67</div> +<div id="deleteCSS" contenteditable><p style="color:red;">x</p></div> +<div id="delete" contenteditable><p><font color="red">y</font></p></div> + +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + +/** Test for Bug 1257363 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + // ***** Backspace test ***** + var div = document.getElementById("backspaceCSS"); + div.focus(); + synthesizeMouse(div, 100, 2, {}); /* click behind and down */ + + var sel = window.getSelection(); + var selRange = sel.getRangeAt(0); + is(selRange.endContainer.nodeName, "#text", "selection should be at the end of text node"); + is(selRange.endOffset, 5, "offset should be 5"); + + // Return and backspace should take us to where we started. + synthesizeKey("KEY_Enter"); + synthesizeKey("KEY_Backspace"); + + selRange = sel.getRangeAt(0); + is(selRange.endContainer.nodeName, "#text", "selection should be at the end of text node"); + is(selRange.endOffset, 5, "offset should be 5"); + + // Add an "a" to the end of the paragraph. + sendString("a"); + + // Return and forward delete should take us to the following line. + synthesizeKey("KEY_Enter"); + synthesizeKey("KEY_Delete"); + + // Add a "b" to the start. + sendString("b"); + + is(div.innerHTML, "<p style=\"color:red;\">12345a</p>b67", + "unexpected HTML"); + + // Let's repeat the whole thing, but a font tag instead of CSS. + // The behaviour is different since the font is carried over. + div = document.getElementById("backspace"); + div.focus(); + synthesizeMouse(div, 100, 2, {}); /* click behind and down */ + + sel = window.getSelection(); + selRange = sel.getRangeAt(0); + is(selRange.endContainer.nodeName, "#text", "selection should be at the end of text node"); + is(selRange.endOffset, 5, "offset should be 5"); + + // Return and backspace should take us to where we started. + synthesizeKey("KEY_Enter"); + synthesizeKey("KEY_Backspace"); + + selRange = sel.getRangeAt(0); + is(selRange.endContainer.nodeName, "#text", "selection should be at the end of text node"); + is(selRange.endOffset, 5, "offset should be 5"); + + // Add an "a" to the end of the paragraph. + sendString("a"); + + // Return and forward delete should take us to the following line. + synthesizeKey("KEY_Enter"); + synthesizeKey("KEY_Delete"); + + // Add a "b" to the start. + sendString("b"); + + // Here we get a somewhat ugly result since the red sticks. + is(div.innerHTML, "<p><font color=\"red\">12345a</font></p><font color=\"#ff0000\">b</font>67", + "unexpected HTML"); + + // ***** Delete test ***** + div = document.getElementById("deleteCSS"); + div.focus(); + synthesizeMouse(div, 100, 2, {}); /* click behind and down */ + + sel = window.getSelection(); + selRange = sel.getRangeAt(0); + is(selRange.endContainer.nodeName, "#text", "selection should be at the end of text node"); + is(selRange.endOffset, 1, "offset should be 1"); + + // left, enter should create a new empty paragraph before + // but leave the selection at the start of the existing paragraph. + synthesizeKey("KEY_ArrowLeft"); + synthesizeKey("KEY_Enter"); + + selRange = sel.getRangeAt(0); + is(selRange.endContainer.nodeName, "#text", "selection should be at the start of text node"); + is(selRange.endOffset, 0, "offset should be 0"); + is(selRange.endContainer.nodeValue, "x", "we should be in the text node with the x"); + + // Now moving up into the new empty paragraph. + synthesizeKey("KEY_ArrowUp"); + + selRange = sel.getRangeAt(0); + is(selRange.endContainer.nodeName, "P", "selection should be the new empty paragraph"); + is(selRange.endOffset, 0, "offset should be 0"); + + // Forward delete should now take us to where we started. + synthesizeKey("KEY_Delete"); + + selRange = sel.getRangeAt(0); + is(selRange.endContainer.nodeName, "#text", "selection should be at the start of text node"); + is(selRange.endOffset, 0, "offset should be 0"); + + // Add an "a" to the start of the paragraph. + sendString("a"); + + is(div.innerHTML, "<p style=\"color:red;\">ax</p>", + "unexpected HTML"); + + // Let's repeat the whole thing, but a font tag instead of CSS. + div = document.getElementById("delete"); + div.focus(); + synthesizeMouse(div, 100, 2, {}); /* click behind and down */ + + sel = window.getSelection(); + selRange = sel.getRangeAt(0); + is(selRange.endContainer.nodeName, "#text", "selection should be at the end of text node"); + is(selRange.endOffset, 1, "offset should be 1"); + + // left, enter should create a new empty paragraph before + // but leave the selection at the start of the existing paragraph. + synthesizeKey("KEY_ArrowLeft"); + synthesizeKey("KEY_Enter"); + + selRange = sel.getRangeAt(0); + is(selRange.endContainer.nodeName, "#text", "selection should be at the start of text node"); + is(selRange.endOffset, 0, "offset should be 0"); + is(selRange.endContainer.nodeValue, "y", "we should be in the text node with the y"); + + // Now moving up into the new empty paragraph. + synthesizeKey("KEY_ArrowUp"); + + selRange = sel.getRangeAt(0); + is(selRange.endContainer.nodeName, "FONT", "selection should be the font tag"); + is(selRange.endOffset, 0, "offset should be 0"); + is(selRange.endContainer.parentNode.nodeName, "P", "the parent of the font should be a paragraph"); + + // Forward delete should now take us to where we started. + synthesizeKey("KEY_Delete"); + + selRange = sel.getRangeAt(0); + is(selRange.endContainer.nodeName, "#text", "selection should be at the start of text node"); + is(selRange.endOffset, 0, "offset should be 0"); + + // Add an "a" to the start of the paragraph. + sendString("a"); + + is(div.innerHTML, "<p><font color=\"red\">ay</font></p>", + "unexpected HTML"); + + SimpleTest.finish(); +}); + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_bug1258085.html b/editor/libeditor/tests/test_bug1258085.html new file mode 100644 index 0000000000..4d8e1ef888 --- /dev/null +++ b/editor/libeditor/tests/test_bug1258085.html @@ -0,0 +1,66 @@ +<!DOCTYPE html> +<title>Test for Bug 1258085</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +<div contenteditable></div> +<script> +var div = document.querySelector("div"); + +function reset() { + div.innerHTML = "x<br> y"; + div.focus(); + synthesizeKey("KEY_ArrowDown"); +} + +function checks(msg) { + is(div.innerHTML, "x<br><br>", + msg + ": Should add a second <br> to prevent collapse of first"); + is(div.childNodes.length, 3, msg + ": No empty text nodes allowed"); + ok(getSelection().isCollapsed, msg + ": Selection must be collapsed"); + is(getSelection().focusNode, div, msg + ": Focus must be in div"); + is(getSelection().focusOffset, 2, + msg + ": Focus must be between the two <br>s"); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + // Put selection after the "y" and backspace + reset(); + synthesizeKey("KEY_ArrowRight"); + synthesizeKey("KEY_Backspace"); + checks("Collapsed backspace"); + + // Now do the same with delete + reset(); + synthesizeKey("KEY_Delete"); + checks("Collapsed delete"); + + // Forward selection + reset(); + synthesizeKey("KEY_ArrowRight", {shiftKey: true}); + synthesizeKey("KEY_Backspace"); + checks("Forward-selected backspace"); + + // Backward selection + reset(); + synthesizeKey("KEY_ArrowRight"); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}); + synthesizeKey("KEY_Backspace"); + checks("Backward-selected backspace"); + + // Make sure we're not deleting if the whitespace isn't actually collapsed + div.style.whiteSpace = "pre-wrap"; + reset(); + synthesizeKey("KEY_ArrowRight"); + synthesizeKey("KEY_ArrowRight"); + synthesizeKey("KEY_Backspace"); + is(div.innerHTML, "x<br> ", "pre-wrap: Don't delete uncollapsed space"); + ok(getSelection().isCollapsed, "pre-wrap: Selection must be collapsed"); + is(getSelection().focusNode, div.lastChild, + "pre-wrap: Focus must be in final text node"); + is(getSelection().focusOffset, 1, "pre-wrap: Focus must be at end of node"); + + SimpleTest.finish(); +}); +</script> diff --git a/editor/libeditor/tests/test_bug1268736.html b/editor/libeditor/tests/test_bug1268736.html new file mode 100644 index 0000000000..697152b2fd --- /dev/null +++ b/editor/libeditor/tests/test_bug1268736.html @@ -0,0 +1,62 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1268736 +--> +<head> + <title>Test for Bug 1268736</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1268736">Mozilla Bug 1268736</a> +<table id="table" border="1" width="100%"> + <tbody> + <tr> + <td>a</td> + <td>b</td> + <td>c</td> + </tr> + <tr> + <td>d</td> + <td id="cell_readonly">e</td> + <td contenteditable="true" id="cell_writable">f</td> + </tr> + </tbody> +</table> + +<script type="application/javascript"> + +/** + * Test for Bug 1268736 + * + * Tests for editing a table cell's contents when the table cell is or isn't a child of a contenteditable node. + * + */ + +function getEditor() { + const Ci = SpecialPowers.Ci; + const editingSession = SpecialPowers.wrap(window).docShell.editingSession; + return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsITableEditor); +} + +const table = document.getElementById("table"); +const tableHTML = table.innerHTML; +const editor = getEditor(); + +const readOnlyCell = document.getElementById("cell_readonly"); +readOnlyCell.focus(); +try { + editor.deleteTableCellContents(); +} catch (e) {} +is(table.innerHTML == tableHTML, true, "editor should not modify non-editable table cell" ); + +const editableCell = document.getElementById("cell_writable"); +editableCell.focus(); +editor.deleteTableCellContents(); +is(editableCell.innerHTML == "<br>", true, "editor can modify editable table cells" ); + +</script> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1270235.html b/editor/libeditor/tests/test_bug1270235.html new file mode 100644 index 0000000000..1499239ae1 --- /dev/null +++ b/editor/libeditor/tests/test_bug1270235.html @@ -0,0 +1,46 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1270235 +--> +<head> + <title>Test for Bug 1270235</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1270235">Mozilla Bug 1270235</a> +<p id="display"></p> +<div id="content" style="display: none;"></div> + +<div id="edit1" contenteditable="true"><p>AB</p></div> +<script type="application/javascript"> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + let element = document.getElementById("edit1"); + element.focus(); + let textNode = element.firstChild.firstChild; + let node = textNode.splitText(0); + node.remove(); + + ok(!node.parentNode, "parent must be null"); + + let newRange = document.createRange(); + newRange.setStart(node, 0); + newRange.setEnd(node, 0); + let selection = document.getSelection(); + selection.removeAllRanges(); + selection.addRange(newRange); + + ok(selection.isCollapsed, "isCollapsed must be true"); + + // Don't crash by user input + sendString("X"); + + SimpleTest.finish(); +}); +</script> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1306532.html b/editor/libeditor/tests/test_bug1306532.html new file mode 100644 index 0000000000..57c4ad6927 --- /dev/null +++ b/editor/libeditor/tests/test_bug1306532.html @@ -0,0 +1,68 @@ +<!DOCTYPE HTML> +<html><head> +<title>Test for bug 1306532</title> +<style src="/tests/SimpleTest/test.css" type="text/css"></style> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> + +<script class="testbody" type="application/javascript"> + +function runTest() { + const headingone = document.getElementById("headingone"); + const celltwo = document.getElementById("celltwo"); + const pasteframe = document.getElementById("pasteframe"); + + // Copy content from table. + var selection = getSelection(); + var startRange = document.createRange(); + startRange.setStart(headingone, 0); + startRange.setEnd(celltwo, 0); + selection.removeAllRanges(); + selection.addRange(startRange); + SpecialPowers.wrap(document).execCommand("copy", false, null); + + // Paste content into "pasteframe" + var pasteContainer = pasteframe.contentDocument.body; + var pasteRange = pasteframe.contentDocument.createRange(); + pasteRange.selectNodeContents(pasteContainer); + pasteRange.collapse(false); + selection.removeAllRanges(); + selection.addRange(pasteRange); + SpecialPowers.wrap(pasteframe.contentDocument).execCommand("paste", false, null); + + is(pasteContainer.querySelector("#headingone").textContent, "Month", "First heading should be 'Month'."); + is(pasteContainer.querySelector("#headingtwo").textContent, "Savings", "Second heading should be 'Savings'."); + is(pasteContainer.querySelector("#cellone").textContent, "January", "First cell should be 'January'."); + is(pasteContainer.querySelector("#celltwo").textContent, "$100", "Second cell should be '$100'."); + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); + +</script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1306532">Mozilla Bug 1306532</a> +<p id="display"></p> + +<pre id="test"> +</pre> + +<div id="container"> +<table border="1"> + <tr> + <th id="headingone">Month</th> + <th id="headingtwo">Savings</th> + </tr> + <tr> + <td id="cellone">January</td> + <td id="celltwo">$100</td> + </tr> +</table> +</div> + +<iframe onload="runTest();" id="pasteframe" srcdoc="<html><body contenteditable='true'>"></iframe> + +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1310912.html b/editor/libeditor/tests/test_bug1310912.html new file mode 100644 index 0000000000..6c2036e4c0 --- /dev/null +++ b/editor/libeditor/tests/test_bug1310912.html @@ -0,0 +1,242 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1310912 +--> +<html> +<head> + <title>Test for Bug 1310912</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1310912">Mozilla Bug 1310912</a> +<p id="display"></p> +<div id="content" style="display: none;"> + +</div> + +<div contenteditable>ABC</div> +<pre id="test"> + +<script class="testbody" type="application/javascript"> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + const editor = document.querySelector("div[contenteditable]"); + + editor.focus(); + const sel = window.getSelection(); + sel.collapse(editor.childNodes[0], editor.textContent.length); + + (function testInsertEmptyTextNodeWhenCaretIsAtEndOfComposition() { + const description = + "testInsertEmptyTextNodeWhenCaretIsAtEndOfComposition: "; + synthesizeCompositionChange({ + composition: { + string: "DEF", + clauses: [ + { length: 3, attr: COMPOSITION_ATTR_RAW_CLAUSE }, + ], + }, + caret: { start: 3, length: 0 }, + }); + is( + editor.textContent, + "ABCDEF", + `${description} Composing text "DEF" should be inserted at end of the text node` + ); + + window.getSelection().getRangeAt(0).insertNode(document.createTextNode("")); + is( + editor.childNodes[0].data, + "ABCDEF", + `${ + description + } First text node should have both preceding text and the composing text` + ); + is( + editor.childNodes[1].data, + "", + `${description} Second text node should be empty` + ); + })(); + + (function testInsertEmptyTextNodeWhenCaretIsAtStartOfComposition() { + const description = + "testInsertEmptyTextNodeWhenCaretIsAtStartOfComposition: "; + synthesizeCompositionChange({ + composition: { + string: "GHI", + clauses: [ + { length: 3, attr: COMPOSITION_ATTR_CONVERTED_CLAUSE }, + ], + }, + caret: { start: 0, length: 0 }, + }); + is( + editor.textContent, + "ABCGHI", + `${description} Composing text should be replaced with new one` + ); + + window.getSelection().getRangeAt(0).insertNode(document.createTextNode("")); + is( + editor.childNodes[0].data, + "ABC", + `${ + description + } First text node should have only the preceding text of the composition` + ); + is( + editor.childNodes[1].data, + "", + `${description} Second text node should have be empty` + ); + is( + editor.childNodes[2].data, + "GHI", + `${description} Third text node should have only composing text` + ); + })(); + + (function testInsertEmptyTextNodeWhenCaretIsAtStartOfCompositionAgain() { + const description = + "testInsertEmptyTextNodeWhenCaretIsAtStartOfCompositionAgain: "; + synthesizeCompositionChange({ + composition: { + string: "JKL", + clauses: [ + { length: 3, attr: COMPOSITION_ATTR_CONVERTED_CLAUSE }, + ], + }, + caret: { start: 0, length: 0 }, + }); + is( + editor.textContent, + "ABCJKL", + `${description} Composing text should be replaced` + ); + + window.getSelection().getRangeAt(0).insertNode(document.createTextNode("")); + is( + editor.childNodes[0].data, + "ABC", + `${ + description + } First text node should have only the preceding text of the composition` + ); + is( + editor.childNodes[1].data, + "", + `${description} Second text node should have be empty` + ); + is( + editor.childNodes[2].data, + "JKL", + `${description} Third text node should have only composing text` + ); + })(); + + (function testInsertEmptyTextNodeWhenCaretIsAtMiddleOfComposition() { + const description = + "testInsertEmptyTextNodeWhenCaretIsAtMiddleOfComposition: "; + synthesizeCompositionChange({ + composition: { + string: "MNO", + clauses: [ + { length: 3, attr: COMPOSITION_ATTR_CONVERTED_CLAUSE }, + ], + }, + caret: { start: 1, length: 0 }, + }); + is( + editor.textContent, + "ABCMNO", + `${description} Composing text should be replaced` + ); + + // Normal selection is the caret, therefore, inserting empty text node + // creates the following DOM tree: + // <div contenteditable> + // |- #text ("ABCM") + // |- #text ("") + // +- #text ("NO") + window.getSelection().getRangeAt(0).insertNode(document.createTextNode("")); + is( + editor.childNodes[0].data, + "ABCM", + `${ + description + } First text node should have the preceding text and composing string before the split point` + ); + is( + editor.childNodes[1].data, + "", + `${description} Second text node should be empty` + ); + is( + editor.childNodes[2].data, + "NO", + `${ + description + } Third text node should have the remaining composing string` + ); + todo_is(editor.childNodes[3].nodeName, "BR", + "Forth node is empty text node, but I don't where this comes from"); + })(); + + // Then, committing composition makes the commit string into the first + // text node and makes the following text nodes empty. + // XXX I don't know whether the empty text nodes should be removed or not + // at this moment. + (function testCommitComposition() { + const description = "testCommitComposition: "; + synthesizeComposition({ type: "compositioncommitasis" }); + is( + editor.textContent, + "ABCMNO", + `${description} Composing text should be committed as-is` + ); + is( + editor.childNodes[0].data, + "ABCMNO", + `${description} First text node should have the committed string` + ); + })(); + + (function testUndoComposition() { + const description = "testUndoComposition: "; + synthesizeKey("Z", { accelKey: true }); + is( + editor.textContent, + "ABC", + `${description} Text should be undone (commit string should've gone)` + ); + is( + editor.childNodes[0].data, + "ABC", + `${description} First text node should have all text` + ); + })(); + + (function testUndoAgain() { + const description = "testUndoAgain: "; + synthesizeKey("Z", { accelKey: true, shiftKey: true }); + is( + editor.textContent, + "ABCMNO", + `${description} Text should be redone (commit string should've be back)` + ); + is( + editor.childNodes[0].data, + "ABCMNO", + `${description} First text node should have all text` + ); + })(); + + SimpleTest.finish(); +}); +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1314790.html b/editor/libeditor/tests/test_bug1314790.html new file mode 100644 index 0000000000..29c9e471a2 --- /dev/null +++ b/editor/libeditor/tests/test_bug1314790.html @@ -0,0 +1,57 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1314790 +--> +<html> +<head> + <title>Test for Bug 1314790</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1314790">Mozilla Bug 1314790</a> +<p id="display"></p> +<div id="content" style="display: none;"> + +</div> + +<div contenteditable="true" id="contenteditable1"><p>pen pineapple</p></div> +<pre id="test"> + +<script class="testbody" type="application/javascript"> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + let elm = document.getElementById("contenteditable1"); + elm.focus(); + window.getSelection().collapse(elm.childNodes[0], 0); + + SpecialPowers.doCommand(window, "cmd_wordNext"); + SpecialPowers.doCommand(window, "cmd_wordNext"); + + synthesizeKey("KEY_Enter"); + sendString("apple pen"); + + is(elm.childNodes[0].textContent, "pen pineapple", + "'pen pineapple' is first elment"); + is(elm.childNodes[1].textContent, "apple pen", + "'apple pen' is second elment"); + + SpecialPowers.doCommand(window, "cmd_deleteWordBackward"); + SpecialPowers.doCommand(window, "cmd_deleteWordBackward"); + is(elm.childNodes[0].textContent, "pen pineapple", + "'pen pineapple' is first elment"); + + SpecialPowers.doCommand(window, "cmd_deleteWordBackward"); + is(elm.childNodes[0].textContent, "pen pineapple", + "'pen pineapple' is first elment"); + + SpecialPowers.doCommand(window, "cmd_deleteWordBackward"); + is(elm.childNodes[0].textContent, "pen ", "'pen ' is first elment"); + + SimpleTest.finish(); +}); +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1315065.html b/editor/libeditor/tests/test_bug1315065.html new file mode 100644 index 0000000000..c8e861c3a8 --- /dev/null +++ b/editor/libeditor/tests/test_bug1315065.html @@ -0,0 +1,145 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1315065 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1315065</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1315065">Mozilla Bug 1315065</a> +<div contenteditable><p>abc<br></p></div> +<script type="application/javascript"> +/** Test for Bug 1315065 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + var editor = document.getElementsByTagName("div")[0]; + function initForBackspace(aSelectionCollapsedTo /* = 0 ~ 3 */) { + editor.innerHTML = "<p id='p'>abc<br></p>"; + var p = document.getElementById("p"); + // FYI: We cannot inserting empty text nodes as expected with + // Node.appendChild() nor Node.insertBefore(). Therefore, let's use + // Range.insertNode() like actual web apps. + var selection = window.getSelection(); + selection.collapse(p, 1); + var range = selection.getRangeAt(0); + var emptyTextNode3 = document.createTextNode(""); + range.insertNode(emptyTextNode3); + var emptyTextNode2 = document.createTextNode(""); + range.insertNode(emptyTextNode2); + var emptyTextNode1 = document.createTextNode(""); + range.insertNode(emptyTextNode1); + is(p.childNodes.length, 5, "Failed to initialize the editor"); + is(p.childNodes.item(1), emptyTextNode1, "1st text node should be emptyTextNode1"); + is(p.childNodes.item(2), emptyTextNode2, "2nd text node should be emptyTextNode2"); + is(p.childNodes.item(3), emptyTextNode3, "3rd text node should be emptyTextNode3"); + switch (aSelectionCollapsedTo) { + case 0: + selection.collapse(p.firstChild, 3); // next to 'c' + break; + case 1: + selection.collapse(emptyTextNode1, 0); + break; + case 2: + selection.collapse(emptyTextNode2, 0); + break; + case 3: + selection.collapse(emptyTextNode3, 0); + break; + default: + ok(false, "aSelectionCollapsedTo is illegal value"); + } + } + + for (let i = 0; i < 4; i++) { + const kDescription = i == 0 ? "Backspace from immediately after the last character" : + "Backspace from " + i + "th empty text node"; + editor.focus(); + initForBackspace(i); + synthesizeKey("KEY_Backspace"); + let p = document.getElementById("p"); + ok(p, kDescription + ": <p> element shouldn't be removed by Backspace key press"); + is(p.tagName.toLowerCase(), "p", kDescription + ": <p> element shouldn't be removed by Backspace key press"); + // When Backspace key is pressed even in empty text nodes, Gecko should not remove empty text nodes for now + // because we should keep our traditional behavior (same as Edge) for backward compatibility as far as possible. + // In this case, Chromium removes all empty text nodes, but Edge doesn't remove any empty text nodes. + is(p.childNodes.length, 5, kDescription + ": <p> should have 5 children after pressing Backspace key"); + is(p.childNodes.item(0).textContent, "ab", kDescription + ": 'c' should be removed by pressing Backspace key"); + is(p.childNodes.item(1).textContent, "", kDescription + ": 1st empty text node should not be removed by pressing Backspace key"); + is(p.childNodes.item(2).textContent, "", kDescription + ": 2nd empty text node should not be removed by pressing Backspace key"); + is(p.childNodes.item(3).textContent, "", kDescription + ": 3rd empty text node should not be removed by pressing Backspace key"); + editor.blur(); + } + + function initForDelete(aSelectionCollapsedTo /* = 0 ~ 3 */) { + editor.innerHTML = "<p id='p'>abc<br></p>"; + var p = document.getElementById("p"); + // FYI: We cannot inserting empty text nodes as expected with + // Node.appendChild() nor Node.insertBefore(). Therefore, let's use + // Range.insertNode() like actual web apps. + var selection = window.getSelection(); + selection.collapse(p, 0); + var range = selection.getRangeAt(0); + var emptyTextNode1 = document.createTextNode(""); + range.insertNode(emptyTextNode1); + var emptyTextNode2 = document.createTextNode(""); + range.insertNode(emptyTextNode2); + var emptyTextNode3 = document.createTextNode(""); + range.insertNode(emptyTextNode3); + is(p.childNodes.length, 5, "Failed to initialize the editor"); + is(p.childNodes.item(0), emptyTextNode3, "1st text node should be emptyTextNode3"); + is(p.childNodes.item(1), emptyTextNode2, "2nd text node should be emptyTextNode2"); + is(p.childNodes.item(2), emptyTextNode1, "3rd text node should be emptyTextNode1"); + switch (aSelectionCollapsedTo) { + case 0: + selection.collapse(p.childNodes.item(3), 0); // next to 'a' + break; + case 1: + selection.collapse(emptyTextNode1, 0); + break; + case 2: + selection.collapse(emptyTextNode2, 0); + break; + case 3: + selection.collapse(emptyTextNode3, 0); + break; + default: + ok(false, "aSelectionCollapsedTo is illegal value"); + } + } + + for (let i = 0; i < 4; i++) { + const kDescription = i == 0 ? "Delete from immediately before the first character" : + "Delete from " + i + "th empty text node"; + editor.focus(); + initForDelete(i); + synthesizeKey("KEY_Delete"); + var p = document.getElementById("p"); + ok(p, kDescription + ": <p> element shouldn't be removed by Delete key press"); + is(p.tagName.toLowerCase(), "p", kDescription + ": <p> element shouldn't be removed by Delete key press"); + if (i == 0) { + // If Delete key is pressed in non-empty text node, only the text node should be modified. + // This is same behavior as Chromium, but different from Edge. Edge removes all empty text nodes in this case. + is(p.childNodes.length, 5, kDescription + ": <p> should have only 2 children after pressing Delete key (empty text nodes should be removed"); + is(p.childNodes.item(0).textContent, "", kDescription + ": 1st empty text node should not be removed by pressing Delete key"); + is(p.childNodes.item(1).textContent, "", kDescription + ": 2nd empty text node should not be removed by pressing Delete key"); + is(p.childNodes.item(2).textContent, "", kDescription + ": 3rd empty text node should not be removed by pressing Delete key"); + is(p.childNodes.item(3).textContent, "bc", kDescription + ": 'a' should be removed by pressing Delete key"); + } else { + // If Delete key is pressed in an empty text node, it and following empty text nodes should be removed and the non-empty text node should be modified. + // This is same behavior as Chromium, but different from Edge. Edge removes all empty text nodes in this case. + var expectedEmptyTextNodes = 3 - i; + is(p.childNodes.length, expectedEmptyTextNodes + 2, kDescription + ": <p> should have only " + i + " children after pressing Delete key (" + i + " empty text nodes should be removed"); + is(p.childNodes.item(expectedEmptyTextNodes).textContent, "bc", kDescription + ": empty text nodes and 'a' should be removed by pressing Delete key"); + } + editor.blur(); + } + SimpleTest.finish(); +}); +</script> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1316302.html b/editor/libeditor/tests/test_bug1316302.html new file mode 100644 index 0000000000..f8f7299330 --- /dev/null +++ b/editor/libeditor/tests/test_bug1316302.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1316302 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1316302</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1316302">Mozilla Bug 1316302</a> +<div contenteditable> +<blockquote><p>abc</p></blockquote> +</div> +<script type="application/javascript"> +/** Test for Bug 1316302 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + var editor = document.getElementsByTagName("div")[0]; + var blockquote = document.getElementsByTagName("blockquote")[0]; + var selection = window.getSelection(); + + editor.focus(); + + // Try to remove the last character from the end of the <blockquote> + selection.collapse(blockquote, blockquote.childNodes.length); + var range = selection.getRangeAt(0); + ok(range.collapsed, "range should be collapsed at the end of <blockquote>"); + is(range.startContainer, blockquote, "range should be collapsed in the <blockquote>"); + is(range.startOffset, blockquote.childNodes.length, "range should be collapsed at the end"); + synthesizeKey("KEY_Backspace"); + is(blockquote.innerHTML, "<p>ab</p>", "Pressing Backspace key at the end of <blockquote> should remove the last character in the <p>"); + + // Try to remove the first character from the start of the <blockquote> + selection.collapse(blockquote, 0); + range = selection.getRangeAt(0); + ok(range.collapsed, "range should be collapsed at the start of <blockquote>"); + is(range.startContainer, blockquote, "range should be collapsed in the <blockquote>"); + is(range.startOffset, 0, "range should be collapsed at the start"); + synthesizeKey("KEY_Delete"); + is(blockquote.innerHTML, "<p>b</p>", "Pressing Delete key at the start of <blockquote> should remove the first character in the <p>"); + + SimpleTest.finish(); +}); +</script> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1328023.html b/editor/libeditor/tests/test_bug1328023.html new file mode 100644 index 0000000000..b3c7cadee0 --- /dev/null +++ b/editor/libeditor/tests/test_bug1328023.html @@ -0,0 +1,61 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1328023 +--> +<html> +<head> + <title>Test for Bug 1328023</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1328023">Mozilla Bug 1328023</a> +<p id="display"></p> +<div id="content" style="display: none;"> + +</div> + +<input type="text" id="input1"/> +<pre id="test"> + +<script class="testbody" type="application/javascript"> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + let elm = document.getElementById("input1"); + + elm.focus(); + sendString("AB"); + is(elm.value, "AB", "AB is input.value now"); + + synthesizeKey("KEY_Backspace"); + is(elm.value, "A", "A is input.value now"); + + synthesizeKey("Z", { accelKey: true }); + is(elm.value, "AB", "AB is input.value now"); + + sendString("C"); + is(elm.value, "ABC", "ABC is input.value now"); + + synthesizeKey("KEY_Backspace"); + synthesizeKey("KEY_Backspace"); + synthesizeKey("KEY_Backspace"); + + sendString("ABC"); + is(elm.value, "ABC", "ABC is input.value now"); + + synthesizeKey("Z", { accelKey: true }); + is(elm.value, "", "'' is input.value now"); + + synthesizeKey("Z", { accelKey: true, shiftKey: true }); + is(elm.value, "ABC", "ABC is input.value now"); + + sendString("D"); + is(elm.value, "ABCD", "ABCD is input.value now"); + + SimpleTest.finish(); +}); +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1330796.html b/editor/libeditor/tests/test_bug1330796.html new file mode 100644 index 0000000000..1fe1430b78 --- /dev/null +++ b/editor/libeditor/tests/test_bug1330796.html @@ -0,0 +1,95 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1330796 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 772796</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style> .pre { white-space: pre } </style> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=772796">Mozilla Bug 1330796</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> + +<div id="editable" contenteditable></div> + +<pre id="test"> + +<script type="application/javascript"> +// We want to test what happens when the user splits a mail cite by clicking +// at the start, the middle and the end of the cite and hitting the enter key. +// Mail cites are spans, and since bug 1288911 they are displayed as blocks. +// The _moz_quote attribute is used to give the cite a blue color via CSS. +// As an internal attribute, it's not returned from the innerHTML. +// To the user the tests look like: +// > mailcite +// This text is 10 characters long, so we position at 0, 5 and 10. +// Althought since bug 1288911 those cites are displayed as block, +// the tests are repeated also for inline display. +// Each entry of the 'tests' array has the original HTML, the offset to click +// at and the expected result HTML. +var tests = [ + // With style="display: block;". + [ "<span _moz_quote=true style=\"display: block;\">> mailcite<br></span>", 0, + "x<br><span style=\"display: block;\">> mailcite<br></span>" ], + [ "<span _moz_quote=true style=\"display: block;\">> mailcite<br></span>", 5, + "<span style=\"display: block;\">> mai<br></span>x<br><span style=\"display: block;\">lcite<br></span>"], + [ "<span _moz_quote=true style=\"display: block;\">> mailcite<br></span>", 10, + "<span style=\"display: block;\">> mailcite<br></span>x<br>" ], + // No <br> at the end to simulate prior deletion to the end of the quote. + [ "<span _moz_quote=true style=\"display: block;\">> mailcite</span>", 10, + "<span style=\"display: block;\">> mailcite<br></span>x<br>" ], + + // Without style="display: block;". + [ "<span _moz_quote=true>> mailcite<br></span>", 0, + "x<br><span>> mailcite<br></span>" ], + [ "<span _moz_quote=true>> mailcite<br></span>", 5, + "<span>> mai</span><br>x<br><span>lcite<br></span>" ], + [ "<span _moz_quote=true>> mailcite<br></span>", 10, + "<span>> mailcite<br></span>x<br>" ], + // No <br> at the end to simulate prior deletion to the end of the quote. + [ "<span _moz_quote=true>> mailcite</span>", 10, + "<span>> mailcite</span><br>x<br>" ], +]; + +/** Test for Bug 1330796 **/ + +SimpleTest.waitForExplicitFinish(); + +SimpleTest.waitForFocus(function() { + var sel = window.getSelection(); + var theEdit = document.getElementById("editable"); + makeMailEditor(); + + for (let i = 0; i < tests.length; i++) { + theEdit.innerHTML = tests[i][0]; + theEdit.focus(); + var theText = theEdit.firstChild.firstChild; + // Position set at the beginning , middle and end of the text. + sel.collapse(theText, tests[i][1]); + + synthesizeKey("KEY_Enter"); + sendString("x"); + is(theEdit.innerHTML, tests[i][2], "unexpected HTML for test " + i.toString()); + } + + SimpleTest.finish(); +}); + +function makeMailEditor() { + var Ci = SpecialPowers.Ci; + var editingSession = SpecialPowers.wrap(window).docShell.editingSession; + var editor = editingSession.getEditorForWindow(window); + editor.flags |= Ci.nsIEditor.eEditorMailMask; +} +</script> + +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1332876.html b/editor/libeditor/tests/test_bug1332876.html new file mode 100644 index 0000000000..1234b53f62 --- /dev/null +++ b/editor/libeditor/tests/test_bug1332876.html @@ -0,0 +1,57 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1332876 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1332876</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1332876">Mozilla Bug 1332876</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> + +<iframe srcdoc="<html><body><span>Edit me!</span>"></iframe> + +<pre id="test"> + +<script type="application/javascript"> + +/** Test for Bug 1332876 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + let iframe = document.querySelector("iframe"); + iframe.contentDocument.designMode = "on"; + + iframe.contentWindow.addEventListener("keypress", function() { + info("Hiding the iframe..."); + iframe.style.display = "none"; + document.body.offsetHeight; + ok(true, "did not crash"); + SimpleTest.finish(); + }, {once: true}); + + iframe.contentWindow.addEventListener("click", function() { + info("Waiting keypress event..."); + // Use another macro task for avoiding impossible event nesting. + SimpleTest.executeSoon(() => { + synthesizeKey("a", {}, iframe.contentWindow); + }); + }, {once: true}); + + let span = iframe.contentDocument.querySelector("span"); + ok(span != null, "The span element should've been loaded in the iframe"); + info("Waiting click event to focus the iframe..."); + synthesizeMouseAtCenter(span, {}, iframe.contentWindow); +}); + +</script> + +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1352799.html b/editor/libeditor/tests/test_bug1352799.html new file mode 100644 index 0000000000..a3f53fc43b --- /dev/null +++ b/editor/libeditor/tests/test_bug1352799.html @@ -0,0 +1,84 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1352799 +--> +<head> + <title>Test for Bug 1352799</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1352799">Mozilla Bug 1352799</a> +<p id="display"></p> +<div id="content"> +<div id="input-container" style="display: none;"> +<input id="input" maxlength="1"> +</div> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 1352799 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + var input = document.getElementById("input"); + + var inputcontainer = document.getElementById("input-container"); + input.setAttribute("maxlength", 2); + inputcontainer.style.display = "block"; + + input.focus(); + + sendString("123"); + + is(input.value, "12", "value should be 12 with maxlength = 2"); + + input.value = ""; + inputcontainer.style.display = "none"; + + window.setTimeout(() => { + input.setAttribute("maxlength", 4); + inputcontainer.style.display = "block"; + + input.focus(); + + sendString("45678"); + + is(input.value, "4567", "value should be 4567 with maxlength = 4"); + + inputcontainer.style.display = "none"; + + window.setTimeout(() => { + input.setAttribute("maxlength", 2); + inputcontainer.style.display = "block"; + + input.focus(); + + sendString("12"); + + todo_is(input.value, "45", "value should be 45 with maxlength = 2"); + + input.value = ""; + inputcontainer.style.display = "none"; + + window.setTimeout(() => { + input.removeAttribute("maxlength"); + inputcontainer.style.display = "block"; + + input.focus(); + + sendString("12345678"); + + is(input.value, "12345678", "value should be 12345678 without maxlength"); + + SimpleTest.finish(); + }, 0); + }, 0); + }, 0); +}); +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1355792.html b/editor/libeditor/tests/test_bug1355792.html new file mode 100644 index 0000000000..c8231ebec4 --- /dev/null +++ b/editor/libeditor/tests/test_bug1355792.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<title>Test for Bug 1355792</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +<div contenteditable><div><font><table><td>a</table><br><br><table><td>b</table></font></div></div> +<script> +var font = document.querySelector("font"); +getSelection().collapse(font, 1); +document.execCommand("forwarddelete"); +is(document.body.firstChild.innerHTML, + "<div><font><table><tbody><tr><td>a</td></tr></tbody></table><br>" + + "<table><tbody><tr><td>b</td></tr></tbody></table></font></div>", + "No creating an extra <br>"); +is(getSelection().focusNode, font, "Selection node should not change"); +is(getSelection().focusOffset, 1, "Selection offset should not move"); +ok(getSelection().isCollapsed, "Selection should be collapsed"); +</script> diff --git a/editor/libeditor/tests/test_bug1358025.html b/editor/libeditor/tests/test_bug1358025.html new file mode 100644 index 0000000000..331bde5d8a --- /dev/null +++ b/editor/libeditor/tests/test_bug1358025.html @@ -0,0 +1,98 @@ +<html> +<head> + <title>Test for bug 1358025</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" + href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1358025">Mozilla Bug 1358025</a> +<div id="display"> + <input type="text" id="edit"> +</div> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="application/javascript"> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + let input = document.getElementById("edit"); + input.focus(); + + let controller = + SpecialPowers.wrap(input).controllers.getControllerForCommand("cmd_undo"); + + // Create undo transaction + SpecialPowers.wrap(input).setUserInput("XXX"); + SpecialPowers.wrap(input).setUserInput(""); + + // Enable undo because this is user input + SpecialPowers.wrap(input).setUserInput(""); + is(input.value, "", "value is empty"); + ok(controller.isCommandEnabled("cmd_undo"), + "Undo is enabled by same empty string"); + + SpecialPowers.wrap(input).setUserInput("ABC"); + is(input.value, "ABC", "value is ABC"); + ok(controller.isCommandEnabled("cmd_undo"), "Undo is enabled by a string"); + + SpecialPowers.wrap(input).setUserInput("ABC"); + is(input.value, "ABC", "value is ABC"); + ok(controller.isCommandEnabled("cmd_undo"), "Undo is enabled by same string"); + + SpecialPowers.wrap(input).setUserInput("DEF"); + is(input.value, "DEF", "value is DEF"); + ok(controller.isCommandEnabled("cmd_undo"), + "Undo is enabled by different string"); + + SpecialPowers.wrap(input).setUserInput(""); + is(input.value, "", "value is empty"); + ok(controller.isCommandEnabled("cmd_undo"), + "Undo is enabled by empty string"); + + // disable undo because this is by script + + // But Edge and Chrome still turn on undo when setting empty value from + // empty value. So we are same behaviour for this case. + input.value = ""; + is(input.value, "", "value is empty"); + ok(controller.isCommandEnabled("cmd_undo"), + "Undo is still enabled by same empty string"); + + input.value = "ABC"; + is(input.value, "ABC", "value is ABC"); + ok(!controller.isCommandEnabled("cmd_undo"), "Undo is disabled by a string"); + + // When setting same value by script, all browsers (Edge, Safari and Chrome) + // keep undo state. + input.value = ""; + SpecialPowers.wrap(input).setUserInput("ABC"); + ok(controller.isCommandEnabled("cmd_undo"), "Undo is enabled"); + input.value = "ABC"; + is(input.value, "ABC", "value is ABC"); + ok(controller.isCommandEnabled("cmd_undo"), + "Undo is still enabled by same string"); + + input.value = ""; + SpecialPowers.wrap(input).setUserInput("ABC"); + ok(controller.isCommandEnabled("cmd_undo"), "Undo is enabled"); + input.value = "DEF"; + is(input.value, "DEF", "value is DEF"); + ok(!controller.isCommandEnabled("cmd_undo"), + "Undo is disabled by different string"); + + input.value = ""; + SpecialPowers.wrap(input).setUserInput("DEF"); + ok(controller.isCommandEnabled("cmd_undo"), "Undo is enabled"); + input.value = ""; + is(input.value, "", "value is empty"); + ok(!controller.isCommandEnabled("cmd_undo"), + "Undo is disabled by empty string"); + + SimpleTest.finish(); +}, window); +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1361008.html b/editor/libeditor/tests/test_bug1361008.html new file mode 100644 index 0000000000..177ed6085e --- /dev/null +++ b/editor/libeditor/tests/test_bug1361008.html @@ -0,0 +1,61 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi=id=1361008 +--> +<html> + <head> + <title>Bug 1361008</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + </head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1361008">Mozilla Bug 1361008</a> +<p id="display"></p> +<div contenteditable="true" id="edit1"> + <br> +</div> +<div contenteditable="true" id="edit2"> + <br> +</div> +<pre id="test"> +<script type="application/javascript"> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + document.execCommand("defaultparagraphseparator", false, "div"); + let edit1 = document.getElementById("edit1"); + edit1.focus(); + let selection = window.getSelection(); + // Insert text before BR element + selection.collapse(edit1, 0); + sendString("AB"); + synthesizeKey("KEY_Enter"); + // count <div> element into contenteidable + is(edit1.getElementsByTagName("div").length, 2, + "DIV element should be 2 children"); + is(edit1.getElementsByTagName("div")[0].innerText, + "AB", "AB should be in 1st DIV element"); + is(edit1.getElementsByTagName("div")[1].innerHTML, + "<br>", "BR element should be in 2nd DIV element"); + + document.execCommand("defaultparagraphseparator", false, "p"); + let edit2 = document.getElementById("edit2"); + edit2.focus(); + selection.collapse(edit2, 0); + // Insert text before BR element + sendString("AB"); + synthesizeKey("KEY_Enter"); + + is(edit2.getElementsByTagName("p").length, 2, + "P element should be 2 children"); + is(edit2.getElementsByTagName("p")[0].innerText, + "AB", "AB should be in 1st P element"); + is(edit2.getElementsByTagName("p")[1].innerHTML, + "<br>", "BR element should be into 2nd P element"); + + SimpleTest.finish(); +}); +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1361052.html b/editor/libeditor/tests/test_bug1361052.html new file mode 100644 index 0000000000..5e9e4df065 --- /dev/null +++ b/editor/libeditor/tests/test_bug1361052.html @@ -0,0 +1,50 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test for Bug 1361052</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1361052">Mozilla Bug 1361052</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script class="testbody" type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); + +SimpleTest.waitForFocus(() => { + var strike = document.createElement("strike"); + strike.contentEditable = true; + document.documentElement.appendChild(strike); + + var textarea = document.createElement("textarea"); + document.documentElement.appendChild(textarea); + + var h5 = document.createElement("h5"); + strike.appendChild(h5); + + textarea.setCustomValidity("A"); + document.documentElement.dir = "rtl"; + document.designMode = "on"; + document.execCommand("styleWithCSS", false, true); + document.designMode = "off"; + textarea.reportValidity(); + document.documentElement.dir = "ltr"; + + var range = document.createRange(); + range.selectNode(h5); + window.getSelection().addRange(range); + + document.execCommand("inserthorizontalrule", false, null); + ok(true, "No crash"); + + SimpleTest.finish(); +}); +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1385905.html b/editor/libeditor/tests/test_bug1385905.html new file mode 100644 index 0000000000..fda99ae741 --- /dev/null +++ b/editor/libeditor/tests/test_bug1385905.html @@ -0,0 +1,51 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi=id=1385905 +--> +<html> +<head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1385905">Mozilla Bug 1385905</a> +<div id="display"></div> +<div id="editor" contenteditable style="padding: 5px;"><div>contents</div></div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + function ensureNoPaddingBR() { + for (let br of document.querySelectorAll("#editor > div > br")) { + ok(!SpecialPowers.wrap(br).isPaddingForEmptyLastLine, + "padding <br> element shouldn't be used with this test"); + } + } + document.execCommand("defaultparagraphseparator", false, "div"); + var editor = document.getElementById("editor"); + // Click the left blank area of the first line to set cursor to the start of "contents". + synthesizeMouse(editor, 3, 10, {}); + synthesizeKey("KEY_Enter"); + is(editor.innerHTML, "<div><br></div><div>contents</div>", + "Typing Enter at start of the <div> element should split the <div> element"); + synthesizeKey("KEY_ArrowUp"); + sendString("x"); + is(editor.innerHTML, "<div>x<br></div><div>contents</div>", + "Typing 'x' at the empty <div> element should just insert 'x' into the <div> element"); + ensureNoPaddingBR(); + synthesizeKey("KEY_Enter"); + is(editor.innerHTML, "<div>x</div><div><br></div><div>contents</div>", + "Typing Enter next to 'x' in the first <div> element should split the <div> element and inserts <br> element to a new <div> element"); + ensureNoPaddingBR(); + synthesizeKey("KEY_Enter"); + is(editor.innerHTML, "<div>x</div><div><br></div><div><br></div><div>contents</div>", + "Typing Enter in the empty <div> should split the <div> element and inserts <br> element to a new <div> element"); + ensureNoPaddingBR(); + SimpleTest.finish(); +}); +</script> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1390562.html b/editor/libeditor/tests/test_bug1390562.html new file mode 100644 index 0000000000..924062352d --- /dev/null +++ b/editor/libeditor/tests/test_bug1390562.html @@ -0,0 +1,67 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1390562 +--> +<html> +<head> + <title>Test for Bug 1390562</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1390562">Mozilla Bug 1390562</a> +<p id="display"></p> +<div id="content" style="display: none;"> + +</div> + +<div id="editor" contenteditable></div> +<pre id="test"> +<script class="testbody" type="application/javascript"> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + let editor = document.getElementById("editor"); + + editor.focus(); + + // Make the HTML editor's default break is <br> + document.execCommand("defaultParagraphSeparator", false, "br"); + + editor.innerHTML = "<div>abc<br><br></div>def"; + + // Collapse selection at the end of the first text node. + window.getSelection().collapse(editor.firstChild.firstChild, 3); + + // Then, typing Enter should insert <br> for <div> container. + // This is necessary for backward compatibility. When we change default + // value of "defaultParagraphSeparator" to "div" or "p", it may be possible + // to remove this hack. + synthesizeKey("KEY_Enter"); + + is(editor.innerHTML, + "<div>abc<br><br><br></div>def", + "Enter key press at end of a text node followed by a visible <br> shouldn't split <div> container when defaultParagraphSeparator is 'br'"); + + // Check also the case of <p> as container. + editor.innerHTML = "<p>abc<br><br></p>def"; + + // Collapse selection at the end of the first text node. + window.getSelection().collapse(editor.firstChild.firstChild, 3); + + // Then, typing Enter should splitting <p> container and remove the visible + // <br> element next to the caret position. + // This is not consistent with <div> container, but this is better behavior + // and keep using this behavior. + synthesizeKey("KEY_Enter"); + + is(editor.innerHTML, + "<p>abc</p><p><br></p>def", + "Enter key press at end of a text node followed by a visible <br> should split <p> container and remove the visible <br> when defaultParagraphSeparator is 'br'"); + + SimpleTest.finish(); +}); +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1394758.html b/editor/libeditor/tests/test_bug1394758.html new file mode 100644 index 0000000000..d1560e5092 --- /dev/null +++ b/editor/libeditor/tests/test_bug1394758.html @@ -0,0 +1,60 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1394758 +--> +<head> + <title>Test for Bug1394758</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1394758">Mozilla Bug 1394758</a> +<p id="display"></p> +<div id="content"> +<div id="editable" contenteditable="true"> + <span id="span" contenteditable="false"> + Hello + </span> + World +</div> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 611182 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var editable = document.getElementById("editable"); + var span = document.getElementById("span"); + var beforeSpan = span.textContent; + + editable.focus(); + window.getSelection().collapse(span.nextSibling, 0); + + synthesizeKey("KEY_ArrowRight"); + synthesizeKey("KEY_ArrowRight"); + synthesizeKey("KEY_ArrowRight"); + synthesizeKey("KEY_Backspace"); + synthesizeKey("KEY_Backspace"); + + is(span.textContent, beforeSpan, + "VK_BACK_SPACE should not modify non-editable area"); + is(span.nextSibling.textContent.trim(), "rld", + "VK_BACK_SPACE should delete first 2 characters"); + + synthesizeKey("KEY_Delete"); + + is(span.textContent, beforeSpan, + "VK_DELETE should not modify non-editable area"); + is(span.nextSibling.textContent.trim(), "ld", + "VK_DELETE should delete first character"); + + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1397412.xhtml b/editor/libeditor/tests/test_bug1397412.xhtml new file mode 100644 index 0000000000..a975967696 --- /dev/null +++ b/editor/libeditor/tests/test_bug1397412.xhtml @@ -0,0 +1,65 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" + type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1397412 +--> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Mozilla Bug 1397412" onload="runTest();"> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1397412" + target="_blank">Mozilla Bug 1397412</a> + <p/> + <editor xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + id="editor" + type="content" + editortype="textmail" + style="width: 400px; height: 200px;"/> + <p/> + <pre id="test"> + </pre> + </body> + <script class="testbody" type="application/javascript"> + <![CDATA[ +function runTest() { + var initialHTML1 = "xx<br><br>"; + var expectedHTML1 = "xx<br>t<br>"; + var initialHTML2 = "xx<br><br>yy<br>"; + var expectedHTML2 = "xx<br>t<br>yy<br>"; + window.docShell + .rootTreeItem + .QueryInterface(Ci.nsIDocShell) + .appType = Ci.nsIDocShell.APP_TYPE_EDITOR; + var e = document.getElementById("editor"); + var doc = e.contentDocument; + doc.designMode = "on"; + doc.defaultView.focus(); + var selection = doc.defaultView.getSelection(); + var body = doc.body; + + // Test 1. + body.innerHTML = initialHTML1; + selection.collapse(body, 2); + sendString("t"); + var actualHTML = body.innerHTML; + is(actualHTML, expectedHTML1, "'t' should be inserted between <br>s"); + + // Test 2. + body.innerHTML = initialHTML2; + selection.collapse(body, 2); + sendString("t"); + actualHTML = body.innerHTML; + is(actualHTML, expectedHTML2, "'t' should be inserted between <br>s"); + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +]]> +</script> +</window> diff --git a/editor/libeditor/tests/test_bug1399722.html b/editor/libeditor/tests/test_bug1399722.html new file mode 100644 index 0000000000..4b6e2c7c72 --- /dev/null +++ b/editor/libeditor/tests/test_bug1399722.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1399722 +--> +<html> +<head> + <title>Test for Bug 1399722</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1399722">Mozilla Bug 1399722</a> +<p id="display"></p> +<div id="content" style="display: none;"> + +</div> + +<input spellcheck="true" onkeypress="if (event.key=='Enter')this.value='';"> +<pre id="test"> + +<script class="testbody" type="application/javascript"> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + let elm = document.querySelector("input"); + + elm.focus(); + sendString("AB"); + synthesizeKey("KEY_Enter"); + sendString("CD"); + is(elm.value, "CD", "Can type into the textbox successfully after the onkeypress handler deleting the value"); + + SimpleTest.finish(); +}); +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1406726.html b/editor/libeditor/tests/test_bug1406726.html new file mode 100644 index 0000000000..7f7cbca963 --- /dev/null +++ b/editor/libeditor/tests/test_bug1406726.html @@ -0,0 +1,126 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1406726 +--> +<head> + <title>Test for Bug 1406726</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1406726">Mozilla Bug 1406726</a> +<p id="display"></p> +<div id="editor" contenteditable></div> + +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 1406726 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + let editor = document.getElementById("editor"); + let selection = window.getSelection(); + + editor.focus(); + for (let paragraphSeparator of ["div", "p"]) { + document.execCommand("defaultParagraphSeparator", false, paragraphSeparator); + + // The result of editor.innerHTML may be wrong in this tests. + // Currently, editor wraps following elements of <br> element with default + // paragraph separator only when there is only non-editable elements. + // This behavior should be standardized by execCommand spec. + + editor.innerHTML = "foo<br>bar<br><span contenteditable=\"false\">baz</span>"; + selection.collapse(editor.childNodes.item(2), "bar".length); + document.execCommand("insertParagraph", false); + is(editor.innerHTML, "foo<br>" + + "<" + paragraphSeparator + ">bar</" + paragraphSeparator + ">" + + "<" + paragraphSeparator + "><br></" + paragraphSeparator + ">" + + "<" + paragraphSeparator + "><span contenteditable=\"false\">baz</span></" + paragraphSeparator + ">", + "All inline nodes including non-editable <span> element should be wrapped with default paragraph separator, <" + paragraphSeparator + ">"); + ok(selection.isCollapsed, "Selection should be collapsed"); + is(selection.anchorNode, editor.childNodes.item(3), + "Caret should be in the third line"); + is(selection.anchorOffset, 0, + "Caret should be at start of the third line"); + + editor.innerHTML = "foo<br>bar<br><span>baz</span>"; + selection.collapse(editor.childNodes.item(2), "bar".length); + document.execCommand("insertParagraph", false); + is(editor.innerHTML, "foo<br>" + + "<" + paragraphSeparator + ">bar</" + paragraphSeparator + ">" + + "<" + paragraphSeparator + "><br></" + paragraphSeparator + ">" + + "<span>baz</span>", + "All inline nodes in the second line should be wrapped with default paragraph separator, <" + paragraphSeparator + ">"); + ok(selection.isCollapsed, "Selection should be collapsed"); + is(selection.anchorNode, editor.childNodes.item(3), + "Caret should be in the third line"); + is(selection.anchorOffset, 0, + "Caret should be at start of the third line"); + + editor.innerHTML = "foo<br>bar<br><span contenteditable=\"false\">baz</span>qux"; + selection.collapse(editor.childNodes.item(2), "bar".length); + document.execCommand("insertParagraph", false); + is(editor.innerHTML, "foo<br>" + + "<" + paragraphSeparator + ">bar</" + paragraphSeparator + ">" + + "<" + paragraphSeparator + "><br></" + paragraphSeparator + ">" + + "<span contenteditable=\"false\">baz</span>qux", + "All inline nodes in the second line should be wrapped with default paragraph separator, <" + paragraphSeparator + ">"); + ok(selection.isCollapsed, "Selection should be collapsed"); + is(selection.anchorNode, editor.childNodes.item(3), + "Caret should be in the third line"); + is(selection.anchorOffset, 0, + "Caret should be at start of the third line"); + + editor.innerHTML = "foo<br>bar<br><span contenteditable=\"false\">baz</span>"; + selection.collapse(editor.childNodes.item(2), "ba".length); + document.execCommand("insertParagraph", false); + is(editor.innerHTML, "foo<br>" + + "<" + paragraphSeparator + ">ba</" + paragraphSeparator + ">" + + "<" + paragraphSeparator + ">r</" + paragraphSeparator + ">" + + "<" + paragraphSeparator + "><span contenteditable=\"false\">baz</span></" + paragraphSeparator + ">", + "All inline nodes including non-editable <span> element should be wrapped with default paragraph separator, <" + paragraphSeparator + ">"); + ok(selection.isCollapsed, "Selection should be collapsed"); + is(selection.anchorNode, editor.childNodes.item(3).firstChild, + "Caret should be in the text node in the third line"); + is(selection.anchorOffset, 0, + "Caret should be at start of the text node in the third line"); + + editor.innerHTML = "foo<br>bar<br><span>baz</span>"; + selection.collapse(editor.childNodes.item(2), "ba".length); + document.execCommand("insertParagraph", false); + is(editor.innerHTML, "foo<br>" + + "<" + paragraphSeparator + ">ba</" + paragraphSeparator + ">" + + "<" + paragraphSeparator + ">r</" + paragraphSeparator + ">" + + "<span>baz</span>", + "All inline nodes in the second line should be wrapped with default paragraph separator, <" + paragraphSeparator + ">"); + ok(selection.isCollapsed, "Selection should be collapsed"); + is(selection.anchorNode, editor.childNodes.item(3).firstChild, + "Caret should be in the text node in the third line"); + is(selection.anchorOffset, 0, + "Caret should be at start of the text node in the third line"); + + editor.innerHTML = "foo<br>bar<br><span contenteditable=\"false\">baz</span>qux"; + selection.collapse(editor.childNodes.item(2), "ba".length); + document.execCommand("insertParagraph", false); + is(editor.innerHTML, "foo<br>" + + "<" + paragraphSeparator + ">ba</" + paragraphSeparator + ">" + + "<" + paragraphSeparator + ">r</" + paragraphSeparator + ">" + + "<span contenteditable=\"false\">baz</span>qux", + "All inline nodes in the second line should be wrapped with default paragraph separator, <" + paragraphSeparator + ">"); + ok(selection.isCollapsed, "Selection should be collapsed"); + is(selection.anchorNode, editor.childNodes.item(3).firstChild, + "Caret should be in the text node in the third line"); + is(selection.anchorOffset, 0, + "Caret should be at start of the text node in the third line"); + } + + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1409520.html b/editor/libeditor/tests/test_bug1409520.html new file mode 100644 index 0000000000..f610d25a93 --- /dev/null +++ b/editor/libeditor/tests/test_bug1409520.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1409520 +--> +<html> +<head> + <title>Test for Bug 1409520</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1409520">Mozilla Bug 1409520</a> +<p id="display"></p> +<div id="content" style="display: none;"> + +</div> + +<li contenteditable id="editor"> + <select><option>option1</option></select> +</li> + +<pre id="test"> + +<script class="testbody" type="application/javascript"> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var selection = window.getSelection(); + var editor = document.getElementById("editor"); + editor.focus(); + selection.collapse(editor, 0); + document.execCommand("insertText", false, "A"); + is(editor.firstChild.textContent, "A", + "'A' should be inserted at start of the editor"); + is(editor.firstChild.nextSibling.tagName, "SELECT", + "<select> element shouldn't be removed by inserting 'A'"); + is(selection.getRangeAt(0).startContainer, editor.firstChild, + "Caret should be moved after 'A'"); + is(selection.getRangeAt(0).startOffset, 1, + "Caret should be moved after 'A'"); + SimpleTest.finish(); +}); +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1425997.html b/editor/libeditor/tests/test_bug1425997.html new file mode 100644 index 0000000000..637d77a0ca --- /dev/null +++ b/editor/libeditor/tests/test_bug1425997.html @@ -0,0 +1,63 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1425997 +--> +<html> +<head> + <title>Test for Bug 1425997</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1425997">Mozilla Bug 1425997</a> +<p id="display"></p> +<div id="content" style="display: none;"> + +</div> + +<div id="editor" contenteditable> +<!-- --> +<span id="inline">foo</span> +</div> + +<pre id="test"> + +<script class="testbody" type="application/javascript"> +SimpleTest.waitForExplicitFinish(); +// 2 assertions are recorded due to nested execCommand() but not a problem. +// They are necessary to detect invalid method call without mutation event listers. +SimpleTest.expectAssertions(2, 2); +SimpleTest.waitForFocus(async function() { + await SpecialPowers.pushPrefEnv({set: [["dom.document.exec_command.nested_calls_allowed", true]]}); + let selection = window.getSelection(); + let editor = document.getElementById("editor"); + function onCharacterDataModified() { + // Until removing all NBSPs which were inserted by the editor, + // emulates Backspace key with "delete" command. + // When this test is created, the behavior was: + // after 1st delete: "\n<!-- --> \n" + // after 2nd delete: "\n<!-- --> " + // Then, selection is moved into the comment node and deletion won't + // work after that. + while (editor.innerHTML.includes(" ")) { + let preInnerHTML = editor.innerHTML; + if (!document.execCommand("delete", false) || preInnerHTML === editor.innerHTML) { + break; + } + info(`editor.innerHTML: "${editor.innerHTML.replace(/\n/g, "\\n")}"`); + } + } + editor.addEventListener("DOMCharacterDataModified", onCharacterDataModified, { once: true }); + editor.focus(); + selection.selectAllChildren(document.getElementById("inline")); + document.execCommand("insertHTML", false, "text"); + // This expected result is just same as the result of Chrome. + // If the spec says this is wrong, feel free to change this result. + todo_is(editor.innerHTML, "\n<!-- --><span id=\"inline\">text</span>", + "The 'foo' should be replaced with 'text' and whitespaces before the span element should be removed"); + SimpleTest.finish(); +}); +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1543312.html b/editor/libeditor/tests/test_bug1543312.html new file mode 100644 index 0000000000..c0463d6d2f --- /dev/null +++ b/editor/libeditor/tests/test_bug1543312.html @@ -0,0 +1,70 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1543312 +--> +<head> + <title>Test for Bug 1543312</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + +<script> +SimpleTest.expectAssertions(0, 1); + +add_task(async function() { + let iframe = document.getElementById("iframe2"); + iframe.style.display = "none"; + iframe.offsetHeight; // reflow + iframe.style.display = ""; + iframe.offsetHeight; // reflow + + iframe.focus(); + let edit = iframe.contentDocument.getElementById("edit"); + edit.focus(); + synthesizeCompositionChange({ + "composition": { + "string": "foo", + "clauses": [{ + "length": 3, + "attr": COMPOSITION_ATTR_RAW_CLAUSE + }], + "caret": { + "start": 3, + "length": 0 + } + } + }); + synthesizeComposition({type: "compositioncommitasis"}); + synthesizeCompositionChange({ + "composition": { + "string": "bar", + "clauses": [{ + "length": 3, + "attr": COMPOSITION_ATTR_RAW_CLAUSE + }], + "caret": { + "start": 3, + "length": 0 + } + } + }); + synthesizeComposition({type: "compositioncommitasis"}); + + is(edit.textContent, "foobar", "caret is updated correctly"); + + synthesizeKey("1"); + is(edit.textContent, "foobar1", "caret is updated correctly"); +}); +</script> +</head> +<body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1543312">Mozilla Bug 1543312</a> + <p id="display"></p> + + <div> + <iframe id="iframe1" srcdoc="<div contenteditable></div>"></iframe> + <iframe id="iframe2" srcdoc="<div id=edit contenteditable></div>"></iframe> + </div> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1568996.html b/editor/libeditor/tests/test_bug1568996.html new file mode 100644 index 0000000000..e0d48dad76 --- /dev/null +++ b/editor/libeditor/tests/test_bug1568996.html @@ -0,0 +1,67 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1568996 +--> +<html> +<head> +<title>Test for Bug 1568996</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1568996">Bug 1568996</a> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<input id="input1"> +<script> +add_task(async () => { + await new Promise((resolve) => { + SimpleTest.waitForFocus(() => { + SimpleTest.executeSoon(resolve); + }, window); + }); + + let input1 = document.getElementById("input1"); + input1.value = "hello" + input1.focus(); + input1.setSelectionRange(0, 0); + + input1.addEventListener('keydown', () => { + let s = input1.selectionStart; + let e = input1.selectionEnd; + input1.value = input1.value.toUpperCase(); + input1.setSelectionRange(s, e); + }); + + input1.addEventListener('input', () => { + let s = input1.selectionStart; + let e = input1.selectionEnd; + input1.value = input1.value.toLowerCase(); + input1.setSelectionRange(s, e); + }); + + synthesizeKey('1'); + synthesizeKey('KEY_Delete'); + is(input1.value, "1ello", "Delete key should be worked"); + + synthesizeKey('B'); + synthesizeKey('KEY_Delete'); + synthesizeKey('KEY_Delete'); + is(input1.value, "1blo", "Multiple delete key should be worked"); + + synthesizeKey('KEY_ArrowRight'); + synthesizeKey('2'); + synthesizeKey('KEY_Delete'); + is(input1.value, "1bl2", "Delete key should be worked"); + + synthesizeKey('3'); + is(input1.value, "1bl23", "charcter should be inserted"); + + synthesizeKey('KEY_Backspace'); + is(input1.value, "1bl2", "Backspace key should be worked"); +}); +</script> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1574596.html b/editor/libeditor/tests/test_bug1574596.html new file mode 100644 index 0000000000..fe0a090d26 --- /dev/null +++ b/editor/libeditor/tests/test_bug1574596.html @@ -0,0 +1,64 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1574596 +--> +<head> + <title>Test for Bug 1574596</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + +<script> +add_task(async function() { + let iframe = document.getElementById("iframe1"); + iframe.focus(); + let edit = iframe.contentDocument.getElementById("edit"); + edit.focus(); + + iframe.contentDocument.execCommand("enableObjectResizing", false, true); + + async function waitForSelectionChange() { + return new Promise(resolve => { + iframe.contentDocument.addEventListener("selectionchange", () => { + resolve(); + }, {once: true}); + }); + } + + let target = iframe.contentDocument.getElementById("target"); + let promiseSelectionChangeEvent = waitForSelectionChange(); + synthesizeMouseAtCenter(target, {}, iframe.contentWindow); + await promiseSelectionChangeEvent; + + ok(target.hasAttribute("_moz_resizing"), + "resizers of the <img> should be visible"); + + iframe.style.display = "none"; + iframe.offsetHeight; // reflow + + await new Promise(SimpleTest.executeSoon); + ok(!target.hasAttribute("_moz_resizing"), + "resizers of the <img> should be hidden"); + + iframe.style.display = ""; + iframe.offsetHeight; // reflow + + promiseSelectionChangeEvent = waitForSelectionChange(); + synthesizeMouseAtCenter(target, {}, iframe.contentWindow); + await promiseSelectionChangeEvent; + + ok(target.hasAttribute("_moz_resizing"), + "resizers of the <img> should be visible again"); +}); +</script> +</head> +<body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1574596">Mozilla Bug 1574596</a> + <p id="display"></p> + + <div> + <iframe id="iframe1" srcdoc="<div id=edit contenteditable><img id='target' src='green.png'></div>"></iframe> + </div> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1581337.html b/editor/libeditor/tests/test_bug1581337.html new file mode 100644 index 0000000000..c6163f6637 --- /dev/null +++ b/editor/libeditor/tests/test_bug1581337.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1581337 +--> +<head> + <title>Test for Bug 1581337</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1581337">Mozilla Bug 1581337</a> + <p id="display"></p> + + <div id="editor" contenteditable><span _moz_quote="true">foo bar</span></div> +</body> +<script> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + let editor = document.getElementById("editor"); + editor.focus(); + let selection = document.getSelection(); + selection.collapse(editor.firstChild.firstChild, 4); + synthesizeKey("KEY_Backspace"); + // FYI: `_moz_quote` attribute is ignored at serializing HTML content. + is(editor.innerHTML, "<span>foobar</span>", "Backspace should delete the previous whitespace"); + synthesizeKey("KEY_Delete"); + is(editor.innerHTML, "<span>fooar</span>", "Delete should delete the next character, \"b\""); + SimpleTest.finish(); +}); +</script> +</html> diff --git a/editor/libeditor/tests/test_bug1619852.html b/editor/libeditor/tests/test_bug1619852.html new file mode 100644 index 0000000000..4564f36526 --- /dev/null +++ b/editor/libeditor/tests/test_bug1619852.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1619852 +--> +<html> +<head> +<title>Test for Bug 1619852</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1619852">Bug 1619852</a> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<div contenteditable>abcd</div> +<script> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + let editor = document.querySelector("div[contenteditable]"); + // Do nothing, but `HTMLEditor` may use different path to detect unexpected DOM tree or selection change. + editor.addEventListener("DOMNodeRemoved", () => {}); + getSelection().collapse(editor.firstChild, 4); + synthesizeKey("KEY_Backspace"); + is(editor.textContent, "abc", "The last character should've been removed by the Backspace"); + getSelection().collapse(editor.firstChild, 1); + synthesizeKey("KEY_Backspace"); + is(editor.textContent, "bc", "The first character should've been removed by the Backspace"); + SimpleTest.finish(); +}); +</script> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1620778.html b/editor/libeditor/tests/test_bug1620778.html new file mode 100644 index 0000000000..5a1c34fb1a --- /dev/null +++ b/editor/libeditor/tests/test_bug1620778.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<title>Test for Bug 1620778</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +<input id=a value=abcd autocomplete=off> +<input id=a value=abcd> +<script> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + let expectedPosition = null; + for (let input of document.querySelectorAll("input")) { + input.focus(); + input.selectionStart = 0; + synthesizeKey("KEY_ArrowRight"); + synthesizeKey("KEY_ArrowRight"); + synthesizeKey("KEY_ArrowDown"); + if (expectedPosition === null) + expectedPosition = input.selectionStart; + isnot(input.selectionStart, 0); + is(input.selectionStart, expectedPosition, "autocomplete shouldn't make a difference on inputs that have no completion results of any kind"); + } + SimpleTest.finish(); +}); +</script> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug1649005.html b/editor/libeditor/tests/test_bug1649005.html new file mode 100644 index 0000000000..fbd8e16ef8 --- /dev/null +++ b/editor/libeditor/tests/test_bug1649005.html @@ -0,0 +1,49 @@ +<!DOCTYPE> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1649005 +--> +<head> + <meta charset="UTF-8" /> + <title>Test for bug 1649005</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script> + /** Test for bug 1649005, bug 1779343 **/ + window.addEventListener("DOMContentLoaded", (event) => { + SimpleTest.waitForExplicitFinish(); + SimpleTest.waitForFocus(function() { + document.body.textContent = ""; // It would be \n\n otherwise... + synthesizeMouseAtCenter(document.body, {}); + + var editor = getEditor(); + is(document.body.textContent, "", "Initial body check"); + editor.rewrap(false); + is(document.body.textContent, "", "Initial body check after rewrap"); + + document.body.innerHTML = ">abc<br/>>def<br/>>ghi"; + editor.rewrap(true); + is(document.body.textContent, "> abc def ghi", "Rewrapped"); + + document.body.innerHTML = "> "; + editor.rewrap(true); + is(document.body.textContent, "> ", "Rewrapped half-empty string"); + + SimpleTest.finish(); + }); + }); + + function getEditor() { + var Ci = SpecialPowers.Ci; + var editingSession = SpecialPowers.wrap(window).docShell.editingSession; + var editor = editingSession.getEditorForWindow(window); + editor.QueryInterface(Ci.nsIHTMLEditor); + editor.QueryInterface(Ci.nsIEditorMailSupport); + editor.flags |= SpecialPowers.Ci.nsIEditor.eEditorPlaintextMask; + return editor; + } + </script> +</head> +<body contenteditable></body> +</html> diff --git a/editor/libeditor/tests/test_bug1659276.html b/editor/libeditor/tests/test_bug1659276.html new file mode 100644 index 0000000000..6789db2e77 --- /dev/null +++ b/editor/libeditor/tests/test_bug1659276.html @@ -0,0 +1,78 @@ +<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<title>Test for Bug 1659276</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+<P>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</P>
+</body>
+
+<script>
+document.designMode="on";
+while (window.find("Lorem")) {
+ document.execCommand("BackColor", 0, 'pink');
+}
+document.designMode="off";
+scroll(0,0);
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(async () => {
+ function waitForTickOfRefeshDriver() {
+ function awaitOneRefresh() {
+ return new Promise(function(aResolve, aReject) {
+ requestAnimationFrame(aResolve);
+ });
+ }
+ return awaitOneRefresh().then(awaitOneRefresh);
+ }
+ await waitForTickOfRefeshDriver();
+ is(document.documentElement.scrollTop, 0, "scrollTop should be 0");
+ is(document.documentElement.scrollLeft, 0, "scrollLeft should be 0");
+ SimpleTest.finish();
+});
+</script>
+</html>
diff --git a/editor/libeditor/tests/test_bug1704381.html b/editor/libeditor/tests/test_bug1704381.html new file mode 100644 index 0000000000..54751d922b --- /dev/null +++ b/editor/libeditor/tests/test_bug1704381.html @@ -0,0 +1,48 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="UTF-8"> +<title>Test for Bug 1704381</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> +<textarea>abc</textarea> +</body> +<script> +"use strict"; +SimpleTest.waitForExplicitFinish(); + +SimpleTest.waitForFocus(() => { + const textarea = document.querySelector("textarea"); + (function test_EditorBase_ToggleTextDirectionAsAction() { + textarea.removeAttribute("dir"); + textarea.focus(); + let newValue; + textarea.oninput = () => { + textarea.scrollHeight; // flush pending layout and run re-initializing editor synchronously + newValue = textarea.value; + }; + SpecialPowers.doCommand(window, "cmd_switchTextDirection"); + is(newValue, "abc", + "EditorBase::ToggleTextDirectionAsAction: Getting value should be succeeded immediately after reinitializing the editor"); + textarea.removeAttribute("dir"); + })(); + (function test_EditorBase_NotifyEditorObservers() { + textarea.focus(); + let newValue; + textarea.oninput = () => { + textarea.scrollHeight; // flush pending layout and run re-initializing editor synchronously + newValue = textarea.value; + }; + document.execCommand("insertLineBreak"); + is(newValue, "\nabc", + "EditorBase::NotifyEditorObservers: Getting value should be succeeded immediately after reinitializing the editor"); + textarea.value = "abc"; + })(); + // TODO: Cannot test EditorBase::SwitchTextDirectionTo() since it requires bidi keyboard layout activated. + SimpleTest.finish(); +}); +</script> +</html> diff --git a/editor/libeditor/tests/test_bug200416.html b/editor/libeditor/tests/test_bug200416.html new file mode 100644 index 0000000000..9fb6564250 --- /dev/null +++ b/editor/libeditor/tests/test_bug200416.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=200416 +--> +<title>Test for Bug 200416</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=200416">Mozilla Bug 200416</a> +<div contenteditable><span>foo<p>bar</p></span></div> +<script> +getSelection().collapse(document.querySelector("p").firstChild, 0); +document.execCommand("delete"); +var innerHTML = document.querySelector("div").innerHTML; +ok(/foo.*bar/.test(innerHTML), "foo needs to still come before bar"); +</script> diff --git a/editor/libeditor/tests/test_bug289384.html b/editor/libeditor/tests/test_bug289384.html new file mode 100644 index 0000000000..2352dd3bc8 --- /dev/null +++ b/editor/libeditor/tests/test_bug289384.html @@ -0,0 +1,48 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=289384 +--> +<head> + <title>Test for Bug 289384</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=289384">Mozilla Bug 289384</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(function() { + var win = window.open("file_bug289384-1.html", "", "test-289384"); + win.addEventListener("load", function() { + win.document.querySelector("a").click(); + }, {once: true}); +}); + +function continueTest(win) { + SimpleTest.waitForFocus(function() { + var doc = win.document; + var sel = win.getSelection(); + doc.body.focus(); + sel.collapse(doc.body.firstChild, 3); + SimpleTest.executeSoon(function() { + synthesizeKey("KEY_ArrowLeft", {accelKey: true}, win); + ok(sel.isCollapsed, "The selection must be collapsed"); + is(sel.anchorNode, doc.body.firstChild, "The anchor node should be the body element's text node"); + is(sel.anchorOffset, 0, "The anchor offset should be 0"); + win.close(); + SimpleTest.finish(); + }); + }, win); +} + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug290026.html b/editor/libeditor/tests/test_bug290026.html new file mode 100644 index 0000000000..786a12018c --- /dev/null +++ b/editor/libeditor/tests/test_bug290026.html @@ -0,0 +1,52 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=290026 +--> +<head> + <title>Test for Bug 290026</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=290026">Mozilla Bug 290026</a> +<p id="display"></p> +<div id="editor" contenteditable></div> + +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 290026 **/ +SimpleTest.waitForExplicitFinish(); + +var editor = document.getElementById("editor"); +editor.innerHTML = "<p></p><ul><li>Item 1</li><li>Item 2</li></ul><p></p>"; +editor.focus(); + +addLoadEvent(function() { + document.execCommand("stylewithcss", false, "true"); + var sel = window.getSelection(); + sel.removeAllRanges(); + var lis = document.getElementsByTagName("li"); + var range = document.createRange(); + range.setStart(lis[0], 0); + range.setEnd(lis[1], lis[1].childNodes.length); + sel.addRange(range); + document.execCommand("indent", false, false); + var oneindent = '<p></p><ul style="margin-left: 40px;"><li>Item 1</li><li>Item 2</li></ul><p></p>'; + is(editor.innerHTML, oneindent, "a once indented bulleted list"); + document.execCommand("indent", false, false); + var twoindent = '<p></p><ul style="margin-left: 80px;"><li>Item 1</li><li>Item 2</li></ul><p></p>'; + is(editor.innerHTML, twoindent, "a twice indented bulleted list"); + document.execCommand("outdent", false, false); + is(editor.innerHTML, oneindent, "outdenting a twice indented bulleted list"); + + // done + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug291780.html b/editor/libeditor/tests/test_bug291780.html new file mode 100644 index 0000000000..3a97073fad --- /dev/null +++ b/editor/libeditor/tests/test_bug291780.html @@ -0,0 +1,49 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=291780 +--> +<head> + <title>Test for Bug 291780</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=291780">Mozilla Bug 291780</a> +<p id="display"></p> +<div id="editor" contenteditable></div> + +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 291780 **/ +SimpleTest.waitForExplicitFinish(); + +var original = '<ul style="margin-left: 40px;"><li>Item 1</li><li>Item 2</li><li>Item 3</li><li>Item 4</li></ul>'; +var editor = document.getElementById("editor"); +editor.innerHTML = original; +editor.focus(); + +addLoadEvent(function() { + var sel = window.getSelection(); + sel.removeAllRanges(); + var lis = document.getElementsByTagName("li"); + var range = document.createRange(); + range.setStart(lis[1], 0); + range.setEnd(lis[2], lis[2].childNodes.length); + sel.addRange(range); + document.execCommand("indent", false, false); + var expected = '<ul style="margin-left: 40px;"><li>Item 1</li><ul><li>Item 2</li><li>Item 3</li></ul><li>Item 4</li></ul>'; + is(editor.innerHTML, expected, "indenting part of an already indented bulleted list"); + document.execCommand("outdent", false, false); + is(editor.innerHTML, original, "outdenting the partially indented part of an already indented bulleted list"); + + // done + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug309731.html b/editor/libeditor/tests/test_bug309731.html new file mode 100644 index 0000000000..6e2a2e6001 --- /dev/null +++ b/editor/libeditor/tests/test_bug309731.html @@ -0,0 +1,58 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=309731 +--> +<head> + <title>Test for Bug 309731</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=309731">Mozilla Bug 309731</a> +<p id="display"></p> + +<div id="content"> + <div id="input" contentEditable="true"></div> +</div> +<pre id="test"> +<script type="application/javascript"> +/** Test for Bug 309731 **/ + +function selectNode(node) { + getSelection().selectAllChildren(node); +} + +function selectInNode(node) { + getSelection().collapse(node, 0); +} + +function doTest() { + var input = document.getElementById("input"); + + is(input.textContent, "", "Input node starts empty"); + + selectInNode(input); + ok(document.execCommand("inserthtml", false, ""), "execCommand should return true"); + is(input.textContent, "", "empty inserthtml with empty selection shouldn't change contents"); + + selectInNode(input); + ok(document.execCommand("inserthtml", false, "foo"), "execCommand should return true"); + is(input.textContent, "foo", "'foo'inserthtml with empty selection should add foo to contents"); + + selectNode(input); + ok(document.execCommand("inserthtml", false, "bar"), "execCommand should return true"); + is(input.textContent, "bar", "'bar' inserthtml with complete selection should replace contents with bar"); + + selectNode(input); + ok(document.execCommand("inserthtml", false, ""), "execCommand should return true"); + is(input.textContent, "", "empty inserthtml with complete selection should delete everything"); +} + +doTest(); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug316447.html b/editor/libeditor/tests/test_bug316447.html new file mode 100644 index 0000000000..76d123815b --- /dev/null +++ b/editor/libeditor/tests/test_bug316447.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=316447 +--> +<title>Test for Bug 316447</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=316447">Mozilla Bug 316447</a> +<div contenteditable><br></div> +<script> +/** Test for Bug 316447 **/ + +getSelection().selectAllChildren(document.querySelector("div")); +document.execCommand("inserthorizontalrule"); +is(document.querySelector("div").innerHTML, "<hr>", "Wrong innerHTML"); +</script> diff --git a/editor/libeditor/tests/test_bug318065.html b/editor/libeditor/tests/test_bug318065.html new file mode 100644 index 0000000000..be989fcdef --- /dev/null +++ b/editor/libeditor/tests/test_bug318065.html @@ -0,0 +1,82 @@ +<!DOCTYPE HTML> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=318065 +--> + +<head> + <title>Test for Bug 318065</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> + +<body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=318065">Mozilla Bug 318065</a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + + <pre id="test"> + <script type="application/javascript"> + + /** Test for Bug 318065 **/ + SimpleTest.waitForExplicitFinish(); + SimpleTest.waitForFocus(function() { + var expectedValues = ["A", "", "A", "", "A", "", "A"]; + var messages = ["Initial text inserted", + "Initial text deleted", + "Undo of deletion", + "Redo of deletion", + "Initial text typed", + "Undo of typing", + "Redo of typing"]; + var step = 0; + + function onInput() { + is(this.value, expectedValues[step], messages[step]); + step++; + if (step == expectedValues.length) { + this.removeEventListener("input", onInput); + SimpleTest.finish(); + } + } + + var input = document.getElementById("t1"); + input.addEventListener("input", onInput); + var input2 = document.getElementById("t2"); + input2.addEventListener("input", onInput); + + input.focus(); + + // Tests 0 + 1: Input letter and delete it again + sendString("A"); + synthesizeKey("KEY_Backspace"); + + // Test 2: Undo deletion. Value of input should be "A" + synthesizeKey("Z", {accelKey: true}); + + // Test 3: Redo deletion. Value of input should be "" + synthesizeKey("Z", {accelKey: true, shiftKey: true}); + + input2.focus(); + + // Test 4: Input letter + sendString("A"); + + // Test 5: Undo typing. Value of input should be "" + synthesizeKey("Z", {accelKey: true}); + + // Test 6: Redo typing. Value of input should be "A" + synthesizeKey("Z", {accelKey: true, shiftKey: true}); + }); + </script> + </pre> + + <input type="text" value="" id="t1" /> + <input type="text" value="" id="t2" /> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug332636.html b/editor/libeditor/tests/test_bug332636.html new file mode 100644 index 0000000000..6fc255f7d6 --- /dev/null +++ b/editor/libeditor/tests/test_bug332636.html @@ -0,0 +1,75 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=332636 +--> +<head> + <title>Test for Bug 332636</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=332636">Mozilla Bug 332636</a> +<p id="display"></p> +<div id="content"> + <div id="edit0" contenteditable="true">axb</div><!-- reference: plane 0 base character --> + <div id="edit1" contenteditable="true">äb</div><!-- reference: plane 0 diacritic --> + <div id="edit2" contenteditable="true">a𐐀b</div><!-- plane 1 base character --> + <div id="edit3" contenteditable="true">a𐨏b</div><!-- plane 1 diacritic --> + + <div id="edit0b" contenteditable="true">axb</div><!-- reference: plane 0 base character --> + <div id="edit1b" contenteditable="true">äb</div><!-- reference: plane 0 diacritic --> + <div id="edit2b" contenteditable="true">a𐐀b</div><!-- plane 1 base character --> + <div id="edit3b" contenteditable="true">a𐨏b</div><!-- plane 1 diacritic --> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 332636 **/ + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTest); + +function test(edit) { + edit.focus(); + var sel = window.getSelection(); + sel.collapse(edit.childNodes[0], edit.textContent.length - 1); + synthesizeKey("KEY_Backspace"); + is(edit.textContent, "ab", "The backspace key should delete the UTF-16 surrogate pair correctly"); +} + +function testWithMove(edit, offset) { + edit.focus(); + var sel = window.getSelection(); + sel.collapse(edit.childNodes[0], 0); + var i; + for (i = 0; i < offset; ++i) { + synthesizeKey("KEY_ArrowRight"); + synthesizeKey("KEY_ArrowLeft"); + synthesizeKey("KEY_ArrowRight"); + } + synthesizeKey("KEY_Backspace"); + is(edit.textContent, "ab", "The backspace key should delete the UTF-16 surrogate pair correctly"); +} + +function runTest() { + /* test backspace-deletion of the middle character */ + test(document.getElementById("edit0")); + test(document.getElementById("edit1")); + test(document.getElementById("edit2")); + test(document.getElementById("edit3")); + + /* extra tests with the use of RIGHT and LEFT to get to the right place */ + testWithMove(document.getElementById("edit0b"), 2); + testWithMove(document.getElementById("edit1b"), 1); + testWithMove(document.getElementById("edit2b"), 2); + testWithMove(document.getElementById("edit3b"), 1); + + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug332636.html^headers^ b/editor/libeditor/tests/test_bug332636.html^headers^ new file mode 100644 index 0000000000..e853d6cee5 --- /dev/null +++ b/editor/libeditor/tests/test_bug332636.html^headers^ @@ -0,0 +1 @@ +Content-Type: text/html; charset=UTF-8 diff --git a/editor/libeditor/tests/test_bug358033.html b/editor/libeditor/tests/test_bug358033.html new file mode 100644 index 0000000000..db2b00400e --- /dev/null +++ b/editor/libeditor/tests/test_bug358033.html @@ -0,0 +1,41 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=358033 +--> +<head> + <title>Test for Bug 358033</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=358033">Mozilla Bug 358033</a> +<p id="display"></p> +<div id="content"> +<input type="text" id="input1"> +</div> +<pre id="test"> +<script> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + let input = document.getElementById("input1"); + + input.value = "ABC DEF"; + input.focus(); + input.setSelectionRange(4, 7, "backward"); + synthesizeKey("KEY_Backspace"); + is(input.value, "ABC ", "KEY_Backspace should remove selected string"); + synthesizeKey("Z", { accelKey: true }); + is(input.value, "ABC DEF", "Undo should restore string"); + synthesizeKey("KEY_ArrowLeft", { shiftKey: true }); + synthesizeKey("KEY_ArrowLeft", { shiftKey: true }); + synthesizeKey("KEY_Backspace"); + + is(input.value, "AB", "anchor node and focus node should be kept order after undo"); + + SimpleTest.finish(); +}); +</script> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug372345.html b/editor/libeditor/tests/test_bug372345.html new file mode 100644 index 0000000000..bd2de542c4 --- /dev/null +++ b/editor/libeditor/tests/test_bug372345.html @@ -0,0 +1,58 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=372345 +--> +<head> + <title>Test for Bug 372345</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=372345">Mozilla Bug 372345</a> +<p id="display"></p> +<div id="content"> + <iframe srcdoc="<body>"></iframe> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 372345 **/ +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var iframe = document.querySelector("iframe"); + var doc = iframe.contentDocument; + var content = doc.body; + function testCursor(post) { + setTimeout(function() { + var link = document.createElement("a"); + link.href = "http://mozilla.org/"; + link.textContent = "link"; + link.style.cursor = "pointer"; + content.appendChild(link); + is(iframe.contentWindow.getComputedStyle(link).cursor, "pointer", "Make sure that the cursor is set to pointer"); + setTimeout(post, 0); + }, 0); + } + testCursor(function() { + doc.designMode = "on"; + testCursor(function() { + doc.designMode = "off"; + testCursor(function() { + content.setAttribute("contenteditable", "true"); + testCursor(function() { + content.removeAttribute("contenteditable"); + testCursor(function() { + SimpleTest.finish(); + }); + }); + }); + }); + }); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug404320.html b/editor/libeditor/tests/test_bug404320.html new file mode 100644 index 0000000000..fe27dfc875 --- /dev/null +++ b/editor/libeditor/tests/test_bug404320.html @@ -0,0 +1,87 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=404320 +--> +<head> + <title>Test for Bug 404320</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=404320">Mozilla Bug 404320</a> +<p id="display"></p> +<div id="content"> + <iframe id="testIframe"></iframe> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 404320 **/ + +SimpleTest.waitForExplicitFinish(); + +function runTests() { + var win = document.getElementById("testIframe").contentWindow; + var doc = document.getElementById("testIframe").contentDocument; + + function testFormatBlock(tag, withAngleBrackets, shouldSucceed) { + win.getSelection().selectAllChildren(doc.body.firstChild); + doc.execCommand("FormatBlock", false, + withAngleBrackets ? tag : "<" + tag + ">"); + var resultNode; + if (shouldSucceed && (tag == "dd" || tag == "dt")) { + is(doc.body.firstChild.tagName, "DL", "tag was changed"); + resultNode = doc.body.firstChild.firstChild; + } else { + resultNode = doc.body.firstChild; + } + + is(resultNode.tagName, shouldSucceed ? tag.toUpperCase() : "P", "tag was changed"); + } + + function formatBlockTests(tags, shouldSucceed) { + var html = "<p>Content</p>"; + for (var i = 0; i < tags.length; ++i) { + var tag = tags[i]; + + doc.body.innerHTML = html; + testFormatBlock(tag, false, shouldSucceed); + + doc.body.innerHTML = html; + testFormatBlock(tag, true, shouldSucceed); + } + } + + doc.designMode = "on"; + + var goodTags = [ "address", + "blockquote", + "dd", + "div", + "dl", + "dt", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "p", + "pre" ]; + var badTags = [ "b", + "i", + "span", + "foo" ]; + + formatBlockTests(goodTags, true); + formatBlockTests(badTags, false); + SimpleTest.finish(); +} + +addLoadEvent(runTests); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug408231.html b/editor/libeditor/tests/test_bug408231.html new file mode 100644 index 0000000000..517b20619a --- /dev/null +++ b/editor/libeditor/tests/test_bug408231.html @@ -0,0 +1,233 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=408231 +--> +<head> + <title>Test for Bug 408231</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body style="font-family: serif"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=408231">Mozilla Bug 408231</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 408231 **/ + + var commandEnabledResults = [ + ["copy", "false"], + ["createlink", "true"], + ["cut", "false"], + ["delete", "true"], + ["fontname", "true"], + ["fontsize", "true"], + ["formatblock", "true"], + ["hilitecolor", "true"], + ["indent", "true"], + ["inserthorizontalrule", "true"], + ["inserthtml", "true"], + ["insertimage", "true"], + ["insertorderedlist", "true"], + ["insertunorderedlist", "true"], + ["insertparagraph", "true"], + ["italic", "true"], + ["justifycenter", "true"], + ["justifyfull", "true"], + ["justifyleft", "true"], + ["justifyright", "true"], + ["outdent", "true"], + ["paste", "false"], + ["redo", "false"], + ["removeformat", "true"], + ["selectall", "true"], + ["strikethrough", "true"], + ["styleWithCSS", "true"], + ["subscript", "true"], + ["superscript", "true"], + ["underline", "true"], + ["undo", "false"], + ["unlink", "true"], + ["not-a-command", "false"], + ]; + + var commandIndetermResults = [ + ["copy", "false"], + ["createlink", "false"], + ["cut", "false"], + ["delete", "false"], + ["fontname", "false"], + ["fontsize", "false"], + ["formatblock", "false"], + ["hilitecolor", "false"], + ["indent", "false"], + ["inserthorizontalrule", "false"], + ["inserthtml", "false"], + ["insertimage", "false"], + ["insertorderedlist", "false"], + ["insertunorderedlist", "false"], + ["insertparagraph", "false"], + ["italic", "false"], + ["justifycenter", "false"], + ["justifyfull", "false"], + ["justifyleft", "false"], + ["justifyright", "false"], + ["outdent", "false"], + // ["paste", "false"], + ["redo", "false"], + ["removeformat", "false"], + ["selectall", "false"], + ["strikethrough", "false"], + ["styleWithCSS", "false"], + ["subscript", "false"], + ["superscript", "false"], + ["underline", "false"], + ["undo", "false"], + ["unlink", "false"], + ["not-a-command", "false"], + ]; + + var commandStateResults = [ + ["copy", "false"], + ["createlink", "false"], + ["cut", "false"], + ["delete", "false"], + ["fontname", "false"], + ["fontsize", "false"], + ["formatblock", "false"], + ["hilitecolor", "false"], + ["indent", "false"], + ["inserthorizontalrule", "false"], + ["inserthtml", "false"], + ["insertimage", "false"], + ["insertorderedlist", "false"], + ["insertunorderedlist", "false"], + ["insertparagraph", "false"], + ["italic", "false"], + ["justifycenter", "false"], + ["justifyfull", "false"], + ["justifyleft", "true"], + ["justifyright", "false"], + ["outdent", "false"], + // ["paste", "false"], + ["redo", "false"], + ["removeformat", "false"], + ["selectall", "false"], + ["strikethrough", "false"], + ["styleWithCSS", "false"], + ["subscript", "false"], + ["superscript", "false"], + ["underline", "false"], + ["undo", "false"], + ["unlink", "false"], + ["not-a-command", "false"], + ]; + + var commandValueResults = [ + ["copy", ""], + ["createlink", ""], + ["cut", ""], + ["delete", ""], + ["fontname", "serif"], + ["fontsize", ""], + ["formatblock", ""], + ["hilitecolor", "transparent"], + ["indent", ""], + ["inserthorizontalrule", ""], + ["inserthtml", ""], + ["insertimage", ""], + ["insertorderedlist", ""], + ["insertunorderedlist", ""], + ["insertparagraph", ""], + ["italic", ""], + ["justifycenter", "left"], + ["justifyfull", "left"], + ["justifyleft", "left"], + ["justifyright", "left"], + ["outdent", ""], + // ["paste", ""], + ["redo", ""], + ["removeformat", ""], + ["selectall", ""], + ["strikethrough", ""], + ["styleWithCSS", ""], + ["subscript", ""], + ["superscript", ""], + ["underline", ""], + ["undo", ""], + ["unlink", ""], + ["not-a-command", ""], + ]; + + + function callQueryCommandEnabled(cmdName) { + var result; + try { + result = "" + document.queryCommandEnabled(cmdName); + } catch (error) { + result = "name" in error ? error.name : "exception"; + } + return result; + } + + function callQueryCommandIndeterm(cmdName) { + var result; + try { + result = "" + document.queryCommandIndeterm(cmdName); + } catch (error) { + result = "name" in error ? error.name : "exception"; + } + return result; + } + + function callQueryCommandState(cmdName) { + var result; + try { + result = "" + document.queryCommandState(cmdName); + } catch (error) { + result = "name" in error ? error.name : "exception"; + } + return result; + } + + function callQueryCommandValue(cmdName) { + var result; + try { + result = "" + document.queryCommandValue(cmdName); + } catch (error) { + result = "name" in error ? error.name : "exception"; + } + return result; + } + + function testQueryCommand(expectedResults, fun, funName) { + for (let i = 0; i < expectedResults.length; i++) { + var commandName = expectedResults[i][0]; + var expectedResult = expectedResults[i][1]; + var result = fun(commandName); + ok(result == expectedResult, funName + "(" + commandName + ") result=" + result + " expected=" + expectedResult); + } + } + + function runTests() { + document.designMode = "on"; + window.getSelection().collapse(document.body, 0); + testQueryCommand(commandEnabledResults, callQueryCommandEnabled, "queryCommandEnabled"); + testQueryCommand(commandIndetermResults, callQueryCommandIndeterm, "queryCommandIndeterm"); + testQueryCommand(commandStateResults, callQueryCommandState, "queryCommandState"); + testQueryCommand(commandValueResults, callQueryCommandValue, "queryCommandValue"); + document.designMode = "off"; + SimpleTest.finish(); + } + + window.onload = runTests; + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug410986.html b/editor/libeditor/tests/test_bug410986.html new file mode 100644 index 0000000000..fdc5d27631 --- /dev/null +++ b/editor/libeditor/tests/test_bug410986.html @@ -0,0 +1,80 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=410986 +--> +<head> + <title>Test for Bug 410986</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=410986">Mozilla Bug 410986</a> +<p id="display"></p> +<div id="content"> + <div id="contents"><span style="color: green;">green text</span></div> + <div id="editor" contenteditable="true"></div> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 410986 **/ + +var gPasteEvents = 0; +document.getElementById("editor").addEventListener("paste", function() { + ++gPasteEvents; +}); + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + getSelection().selectAllChildren(document.getElementById("contents")); + SimpleTest.waitForClipboard("green text", + function() { + synthesizeKey("C", {accelKey: true}); + }, + function() { + var ed = document.getElementById("editor"); + ed.focus(); + if (navigator.platform.includes("Mac")) { + synthesizeKey("V", {accelKey: true, shiftKey: true, altKey: true}); + } else { + synthesizeKey("V", {accelKey: true, shiftKey: true}); + } + is(ed.innerHTML, "green text", "Content should be pasted in plaintext format"); + is(gPasteEvents, 1, "One paste event must be fired"); + + ed.innerHTML = ""; + ed.blur(); + getSelection().selectAllChildren(document.getElementById("contents")); + SimpleTest.waitForClipboard("green text", + function() { + synthesizeKey("C", {accelKey: true}); + }, + function() { + var ed1 = document.getElementById("editor"); + ed1.focus(); + synthesizeKey("V", {accelKey: true}); + isnot(ed1.innerHTML.indexOf("<span style=\"color: green;\">green text</span>"), -1, + "Content should be pasted in HTML format"); + is(gPasteEvents, 2, "Two paste events must be fired"); + + SimpleTest.finish(); + }, + function() { + ok(false, "Failed to copy the second item to the clipboard"); + SimpleTest.finish(); + } + ); + }, + function() { + ok(false, "Failed to copy the first item to the clipboard"); + SimpleTest.finish(); + } + ); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug414526.html b/editor/libeditor/tests/test_bug414526.html new file mode 100644 index 0000000000..03aa72867e --- /dev/null +++ b/editor/libeditor/tests/test_bug414526.html @@ -0,0 +1,234 @@ +<html> +<head> + <title>Test for backspace key and delete key shouldn't remove another editing host's text</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<div id="display"></div> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + +function runTests() { + var container = document.getElementById("display"); + + function reset() { + document.execCommand("Undo", false, null); + } + + var selection = window.getSelection(); + function moveCaretToStartOf(aEditor) { + selection.selectAllChildren(aEditor); + selection.collapseToStart(); + } + + function moveCaretToEndOf(aEditor) { + selection.selectAllChildren(aEditor); + selection.collapseToEnd(); + } + + /* TestCase #1 + */ + const kTestCase1 = + "<p id=\"editor1\" contenteditable=\"true\">editor1</p>" + + "<p id=\"editor2\" contenteditable=\"true\">editor2</p>" + + "<div id=\"editor3\" contenteditable=\"true\"><div>editor3</div></div>" + + "<p id=\"editor4\" contenteditable=\"true\">editor4</p>" + + "non-editable text" + + "<p id=\"editor5\" contenteditable=\"true\">editor5</p>"; + + const kTestCase1_editor3_deleteAtStart = + "<p id=\"editor1\" contenteditable=\"true\">editor1</p>" + + "<p id=\"editor2\" contenteditable=\"true\">editor2</p>" + + "<div id=\"editor3\" contenteditable=\"true\"><div>ditor3</div></div>" + + "<p id=\"editor4\" contenteditable=\"true\">editor4</p>" + + "non-editable text" + + "<p id=\"editor5\" contenteditable=\"true\">editor5</p>"; + + const kTestCase1_editor3_backspaceAtEnd = + "<p id=\"editor1\" contenteditable=\"true\">editor1</p>" + + "<p id=\"editor2\" contenteditable=\"true\">editor2</p>" + + "<div id=\"editor3\" contenteditable=\"true\"><div>editor</div></div>" + + "<p id=\"editor4\" contenteditable=\"true\">editor4</p>" + + "non-editable text" + + "<p id=\"editor5\" contenteditable=\"true\">editor5</p>"; + + container.innerHTML = kTestCase1; + + var editor1 = document.getElementById("editor1"); + var editor2 = document.getElementById("editor2"); + var editor3 = document.getElementById("editor3"); + var editor4 = document.getElementById("editor4"); + var editor5 = document.getElementById("editor5"); + + /* TestCase #1: + * pressing backspace key at start should not change the content. + */ + editor2.focus(); + moveCaretToStartOf(editor2); + synthesizeKey("KEY_Backspace"); + is(container.innerHTML, kTestCase1, + "Pressing backspace key at start of editor2 changes the content"); + reset(); + + editor3.focus(); + moveCaretToStartOf(editor3); + synthesizeKey("KEY_Backspace"); + is(container.innerHTML, kTestCase1, + "Pressing backspace key at start of editor3 changes the content"); + reset(); + + editor4.focus(); + moveCaretToStartOf(editor4); + synthesizeKey("KEY_Backspace"); + is(container.innerHTML, kTestCase1, + "Pressing backspace key at start of editor4 changes the content"); + reset(); + + editor5.focus(); + moveCaretToStartOf(editor5); + synthesizeKey("KEY_Backspace"); + is(container.innerHTML, kTestCase1, + "Pressing backspace key at start of editor5 changes the content"); + reset(); + + /* TestCase #1: + * pressing delete key at end should not change the content. + */ + editor1.focus(); + moveCaretToEndOf(editor1); + synthesizeKey("KEY_Delete"); + is(container.innerHTML, kTestCase1, + "Pressing delete key at end of editor1 changes the content"); + reset(); + + editor2.focus(); + moveCaretToEndOf(editor2); + synthesizeKey("KEY_Delete"); + is(container.innerHTML, kTestCase1, + "Pressing delete key at end of editor2 changes the content"); + reset(); + + editor3.focus(); + moveCaretToEndOf(editor3); + synthesizeKey("KEY_Delete"); + is(container.innerHTML, kTestCase1, + "Pressing delete key at end of editor3 changes the content"); + reset(); + + editor4.focus(); + moveCaretToEndOf(editor4); + synthesizeKey("KEY_Delete"); + is(container.innerHTML, kTestCase1, + "Pressing delete key at end of editor4 changes the content"); + reset(); + + /* TestCase #1: cases when the caret is not on text node. + * - pressing delete key at start should remove the first character + * - pressing backspace key at end should remove the first character + * and the adjacent blocks should not be changed. + */ + editor3.focus(); + moveCaretToStartOf(editor3); + synthesizeKey("KEY_Delete"); + is(container.innerHTML, kTestCase1_editor3_deleteAtStart, + "Pressing delete key at start of editor3 changes adjacent elements" + + " and/or does not remove the first character."); + reset(); + + editor3.focus(); + moveCaretToEndOf(editor3); + synthesizeKey("KEY_Backspace"); + is(container.innerHTML, kTestCase1_editor3_backspaceAtEnd, + "Pressing backspace key at end of editor3 changes adjacent elements" + + " and/or does not remove the last character."); + reset(); + + /* TestCase #2: + * two adjacent editable <span> in a table cell. + */ + const kTestCase2 = "<table><tbody><tr><td><span id=\"editor1\" contenteditable=\"true\">test</span>" + + "<span id=\"editor2\" contenteditable=\"true\">test</span></td></tr></tbody></table>"; + + container.innerHTML = kTestCase2; + editor1 = document.getElementById("editor1"); + editor2 = document.getElementById("editor2"); + + editor2.focus(); + moveCaretToStartOf(editor2); + synthesizeKey("KEY_Backspace"); + is(container.innerHTML, kTestCase2, + "Pressing backspace key at the start of editor2 changes the content for kTestCase2"); + reset(); + + editor1.focus(); + moveCaretToEndOf(editor1); + synthesizeKey("KEY_Delete"); + is(container.innerHTML, kTestCase2, + "Pressing delete key at the end of editor1 changes the content for kTestCase2"); + reset(); + + /* TestCase #3: + * editable <span> in two adjacent table cells. + */ + const kTestCase3 = "<table><tbody><tr><td><span id=\"editor1\" contenteditable=\"true\">test</span></td>" + + "<td><span id=\"editor2\" contenteditable=\"true\">test</span></td></tr></tbody></table>"; + + container.innerHTML = kTestCase3; + editor1 = document.getElementById("editor1"); + editor2 = document.getElementById("editor2"); + + editor2.focus(); + moveCaretToStartOf(editor2); + synthesizeKey("KEY_Backspace"); + is(container.innerHTML, kTestCase3, + "Pressing backspace key at the start of editor2 changes the content for kTestCase3"); + reset(); + + editor1.focus(); + moveCaretToEndOf(editor1); + synthesizeKey("KEY_Delete"); + is(container.innerHTML, kTestCase3, + "Pressing delete key at the end of editor1 changes the content for kTestCase3"); + reset(); + + /* TestCase #4: + * editable <div> in two adjacent table cells. + */ + const kTestCase4 = "<table><tbody><tr><td><div id=\"editor1\" contenteditable=\"true\">test</div></td>" + + "<td><div id=\"editor2\" contenteditable=\"true\">test</div></td></tr></tbody></table>"; + + container.innerHTML = kTestCase4; + editor1 = document.getElementById("editor1"); + editor2 = document.getElementById("editor2"); + + editor2.focus(); + moveCaretToStartOf(editor2); + synthesizeKey("KEY_Backspace"); + is(container.innerHTML, kTestCase4, + "Pressing backspace key at the start of editor2 changes the content for kTestCase4"); + reset(); + + editor1.focus(); + moveCaretToEndOf(editor1); + synthesizeKey("KEY_Delete"); + is(container.innerHTML, kTestCase4, + "Pressing delete key at the end of editor1 changes the content for kTestCase4"); + reset(); + + SimpleTest.finish(); +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_bug417418.html b/editor/libeditor/tests/test_bug417418.html new file mode 100644 index 0000000000..d319e4e984 --- /dev/null +++ b/editor/libeditor/tests/test_bug417418.html @@ -0,0 +1,78 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=417418 +--> +<head> + <title>Test for Bug 417418</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=417418">Mozilla Bug 417418</a> +<div id="display" contenteditable="true"> +<p id="coin">first paragraph</p> +<p>second paragraph. <img id="img" src="green.png"></p> +</div> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 417418 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTest); + +function resetSelection() { + window.getSelection().collapse(document.getElementById("coin"), 0); +} + +function runTest() { + var rightClickDown = {type: "mousedown", button: 2}, + rightClickUp = {type: "mouseup", button: 2}, + singleClickDown = {type: "mousedown", button: 0}, + singleClickUp = {type: "mouseup", button: 0}; + var selection = window.getSelection(); + + var div = document.getElementById("display"); + var img = document.getElementById("img"); + var divRect = div.getBoundingClientRect(); + var imgselected; + + resetSelection(); + synthesizeMouse(div, divRect.width - 1, divRect.height - 1, rightClickDown); + synthesizeMouse(div, divRect.width - 1, divRect.height - 1, rightClickUp); + ok(selection.isCollapsed, "selection is not collapsed"); + + resetSelection(); + synthesizeMouse(div, divRect.width - 1, divRect.height - 1, singleClickDown); + synthesizeMouse(div, divRect.width - 1, divRect.height - 1, singleClickUp); + ok(selection.isCollapsed, "selection is not collapsed"); + + resetSelection(); + synthesizeMouseAtCenter(img, rightClickDown); + synthesizeMouseAtCenter(img, rightClickUp); + imgselected = selection.anchorNode == img.parentNode && + selection.anchorOffset === 1 && + selection.rangeCount === 1; + ok(imgselected, "image is not selected"); + + resetSelection(); + synthesizeMouseAtCenter(img, singleClickDown); + synthesizeMouseAtCenter(img, singleClickUp); + imgselected = selection.anchorNode == img.parentNode && + selection.anchorOffset === 1 && + selection.rangeCount === 1; + ok(imgselected, "image is not selected"); + + SimpleTest.finish(); +} + + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug426246.html b/editor/libeditor/tests/test_bug426246.html new file mode 100644 index 0000000000..50e5df2cb4 --- /dev/null +++ b/editor/libeditor/tests/test_bug426246.html @@ -0,0 +1,71 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=426246 +--> +<html> +<head> + <title>Test for Bug 426246</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=426246">Mozilla Bug 426246</a> +<p id="display"></p> +<div id="content" style="display: none;"> + +</div> + +<div contenteditable="true" id="contenteditable1"> + <p>first line</p> + <p>this is the second line</p> +</div> + +<div contenteditable="true" id="contenteditable2">first line<br>this is the second line</div> +<div contenteditable="true" id="contenteditable3"><ul><li>first line</li><li>this is the second line</li></ul></div> +<pre contenteditable="true" id="contenteditable4">first line +this is the second line</pre> + +<pre id="test"> + +<script class="testbody" type="application/javascript"> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + let elm1 = document.getElementById("contenteditable1"); + elm1.focus(); + window.getSelection().collapse(elm1.lastElementChild.firstChild, "this is the ".length); + SpecialPowers.doCommand(window, "cmd_deleteToBeginningOfLine"); + is(elm1.firstElementChild.textContent, "first line", "two paragraphs: the first line should stay untouched"); + is(elm1.lastElementChild.textContent, "second line", "two paragraphs: the characters after the caret should remain"); + + let elm2 = document.getElementById("contenteditable2"); + elm2.focus(); + window.getSelection().collapse(elm2.lastChild, "this is the ".length); + is(elm2.lastChild.textContent, "this is the second line", "br: correct initial content"); + SpecialPowers.doCommand(window, "cmd_deleteToBeginningOfLine"); + is(elm2.firstChild.textContent, "first line", "br: the first line should stay untouched"); + is(elm2.lastChild.textContent, "second line", "br: the characters after the caret should remain"); + + let elm3 = document.getElementById("contenteditable3"); + elm3.focus(); + let firstLineLI = elm3.querySelector("li:first-child"); + let secondLineLI = elm3.querySelector("li:last-child"); + window.getSelection().collapse(secondLineLI.firstChild, "this is the ".length); + is(secondLineLI.textContent, "this is the second line", "li: correct initial content"); + SpecialPowers.doCommand(window, "cmd_deleteToBeginningOfLine"); + is(firstLineLI.textContent, "first line", "li: the first line should stay untouched"); + is(secondLineLI.textContent, "second line", "li: the characters after the caret should remain"); + + let elm4 = document.getElementById("contenteditable4"); + elm4.focus(); + window.getSelection().collapse(elm4.firstChild, "first line\nthis is the ".length); + is(elm4.textContent, "first line\nthis is the second line", "pre: correct initial content"); + SpecialPowers.doCommand(window, "cmd_deleteToBeginningOfLine"); + is(elm4.textContent, "first line\nsecond line", "pre: the first line should stay untouched and the characters after the caret in the second line should remain"); + + SimpleTest.finish(); +}); +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug430392.html b/editor/libeditor/tests/test_bug430392.html new file mode 100644 index 0000000000..7ae6b0f7b0 --- /dev/null +++ b/editor/libeditor/tests/test_bug430392.html @@ -0,0 +1,171 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=430392 +--> +<head> + <title>Test for Bug 430392</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=430392">Mozilla Bug 430392</a> +<p id="display"></p> +<div id="content"> + <div contenteditable="true" id="edit"> <span contenteditable="false">A</span> ; <span contenteditable="false">B</span> ; <span contenteditable="false">C</span> </div> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 430392 **/ + +function test() { + var edit = document.getElementById("edit"); + var html = edit.innerHTML; + var expectedText = edit.textContent; + document.getElementById("edit").focus(); + + // Each test is [desc, callback, inputType of `beforeinput`, inputType of `input`]. + // callback() is called and we check that the textContent didn't change. + // For expected failures, the format is + // [desc, callback, undefined, inputType of `beforeinput`, inputType of `input`, expectedValue], + // and the test will be marked as an expected fail if the textContent changes + // to expectedValue, and an unexpected fail if it's neither the original value + // nor expectedValue. + let tests = [["adding returns", () => { + getSelection().collapse(edit.firstChild, 0); + synthesizeKey("KEY_ArrowRight"); + synthesizeKey("KEY_Enter"); + synthesizeKey("KEY_Enter"); + synthesizeKey("KEY_Backspace"); + synthesizeKey("KEY_Backspace"); + }, [ + "insertParagraph", + "insertParagraph", + "deleteContentBackward", + "deleteContentBackward", + ], + // Blink does nothing in this case, but WebKit does insert paragraph. + [/* no input events for NOOP */]], + ["adding shift-returns", () => { + getSelection().collapse(edit.firstChild, 0); + synthesizeKey("KEY_ArrowRight"); + synthesizeKey("KEY_Enter", {shiftKey: true}); + synthesizeKey("KEY_Enter", {shiftKey: true}); + synthesizeKey("KEY_Backspace"); + synthesizeKey("KEY_Backspace"); + }, [ + "insertLineBreak", + "insertLineBreak", + "deleteContentBackward", + "deleteContentBackward", + ], + // Blink does nothing in this case, but WebKit inserts `<br>` element. + [/* no input events for NOOP */]], + ]; + [ + ["insertorderedlist", "insertOrderedList"], + ["insertunorderedlist", "insertUnorderedList"], + ["formatblock", "", "p"], + ] + .forEach(item => { + let cmd = item[0]; + let param = item[2]; + let inputType = item[1]; + tests.push([cmd, () => { document.execCommand(cmd, false, param); }, + [/* execCommand shouldn't cause beforeinput event */], + [inputType]]); + }); + // These are all TODO -- they don't move the non-editable elements + [ + ["bold", "formatBold"], + ["italic", "formatItalic"], + ["underline", "formatUnderline"], + ["strikethrough", "formatStrikeThrough"], + ["subscript", "formatSubscript"], + ["superscript", "formatSuperscript"], + ["forecolor", "formatFontColor", "blue"], + ["backcolor", "formatBackColor", "blue"], + ["hilitecolor", "formatBackColor", "blue"], + ["fontname", "formatFontName", "monospace"], + ["fontsize", "", "1"], + ["justifyright", "formatJustifyRight"], + ["justifycenter", "formatJustifyCenter"], + ["justifyfull", "formatJustifyFull"], + ] + .forEach(item => { + let cmd = item[0]; + let param = item[2]; + let inputType = item[1]; + tests.push([cmd, () => { document.execCommand(cmd, false, param); }, + [/* execCommand shouldn't cause beforeinput event */], + [inputType], + " A ; ; BC "]); + }); + tests.push(["indent", () => { document.execCommand("indent"); }, + [/* execCommand shouldn't cause beforeinput event */], + ["formatIndent"], + " ; ; ABC"]); + + let beforeinputTypes = []; + let inputTypes = []; + edit.addEventListener("beforeinput", event => { beforeinputTypes.push(event.inputType); }); + edit.addEventListener("input", event => { inputTypes.push(event.inputType); }); + tests.forEach(arr => { + ["div", "br", "p"].forEach(sep => { + document.execCommand("defaultParagraphSeparator", false, sep); + + let expectedFailText = typeof arr[4] == "function" ? arr[4]() : arr[4]; + + edit.innerHTML = html; + edit.focus(); + getSelection().selectAllChildren(edit); + beforeinputTypes = []; + inputTypes = []; + arr[1](); + if (typeof expectedFailText != "undefined") { + todo_is(edit.textContent, expectedText, + arr[0] + " should not change text (" + sep + ")"); + if (edit.textContent !== expectedText && + edit.textContent !== expectedFailText) { + is(edit.textContent, expectedFailText, + arr[0] + " changed to different failure (" + sep + ")"); + } + } else { + is(edit.textContent, expectedText, + arr[0] + " should not change text (" + sep + ")"); + } + is(beforeinputTypes.length, arr[2].length, `${arr[0]}: number of beforeinput events should be ${arr[2].length} (${sep})`); + for (let i = 0; i < Math.max(beforeinputTypes.length, arr[2].length); i++) { + if (i < beforeinputTypes.length && i < arr[2].length) { + is(beforeinputTypes[i], arr[2][i], `${arr[0]}: ${i + 1}th inputType of beforeinput event should be "${arr[2][i]}" (${sep})`); + } else if (i < beforeinputTypes.length) { + ok(false, `${arr[0]}: Redundant beforeinput event shouldn't be fired, its inputType was "${beforeinputTypes[i]}" (${sep})`); + } else { + ok(false, `${arr[0]}: beforeinput event whose inputType is "${arr[2][i]}" should be fired, but not fired (${sep})`); + } + } + is(inputTypes.length, arr[3].length, `${arr[0]}: number of input events is unexpected (${sep})`); + for (let i = 0; i < Math.max(inputTypes.length, arr[3].length); i++) { + if (i < inputTypes.length && i < arr[3].length) { + is(inputTypes[i], arr[3][i], `${arr[0]}: ${i + 1}th inputType of input event should be "${arr[3][i]}" (${sep})`); + } else if (i < inputTypes.length) { + ok(false, `${arr[0]}: Redundant input event shouldn't be fired, its inputType was "${inputTypes[i]}" (${sep})`); + } else { + ok(false, `${arr[0]}: input event whose inputType is "${arr[3][i]}" should be fired, but not fired (${sep})`); + } + } + }); + }); + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(test); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug439808.html b/editor/libeditor/tests/test_bug439808.html new file mode 100644 index 0000000000..169ba5bd70 --- /dev/null +++ b/editor/libeditor/tests/test_bug439808.html @@ -0,0 +1,37 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=439808 +--> +<head> + <title>Test for Bug 439808</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=439808">Mozilla Bug 439808</a> +<p id="display"></p> +<div id="content"> +<span><span contenteditable id="e">twest</span></span> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 439808 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var e = document.getElementById("e"); + e.focus(); + getSelection().collapse(e.firstChild, 1); + synthesizeKey("KEY_Delete"); + is(e.textContent, "test", "Delete key worked"); + synthesizeKey("KEY_Backspace"); + is(e.textContent, "est", "Backspace key worked"); + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug442186.html b/editor/libeditor/tests/test_bug442186.html new file mode 100644 index 0000000000..deecbed5f1 --- /dev/null +++ b/editor/libeditor/tests/test_bug442186.html @@ -0,0 +1,103 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=442186 +--> +<head> + <title>Test for Bug 442186</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=442186">Mozilla Bug 442186</a> +<p id="display"></p> +<div id="content"> + <h2> two <div> containers </h2> + <section contenteditable id="test1"> + <div> First paragraph with some text. </div> + <div> Second paragraph with some text. </div> + </section> + + <h2> two paragraphs </h2> + <section contenteditable id="test2"> + <p> First paragraph with some text. </p> + <p> Second paragraph with some text. </p> + </section> + + <h2> one text node, one paragraph </h2> + <section contenteditable id="test3"> + First paragraph with some text. + <p> Second paragraph with some text. </p> + </section> +</div> + +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 442186 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + +function justify(textNode, pos) { + if (!pos) pos = 10; + + // put the caret on the requested character + var range = document.createRange(); + var sel = window.getSelection(); + range.setStart(textNode, pos); + range.setEnd(textNode, pos); + sel.addRange(range); + + // align + document.execCommand("justifyright", false, null); +} + +function runTests() { + document.execCommand("stylewithcss", false, "true"); + + const test1 = document.getElementById("test1"); + const test2 = document.getElementById("test2"); + const test3 = document.getElementById("test3"); + + // #test1: two <div> containers + const line1 = test1.querySelector("div").firstChild; + test1.focus(); + justify(line1); + is(test1.querySelectorAll("*").length, 2, + "Aligning the first child should not create nor remove any element."); + is(line1.parentNode.nodeName.toLowerCase(), "div", + "Aligning the first <div> should not modify its node type."); + is(line1.parentNode.style.textAlign, "right", + "Aligning the first <div> should set a 'text-align: right' style rule."); + + // #test2: two paragraphs + const line2 = test2.querySelector("p").firstChild; + test2.focus(); + justify(line2); + is(test2.querySelectorAll("*").length, 2, + "Aligning the first child should not create nor remove any element."); + is(line2.parentNode.nodeName.toLowerCase(), "p", + "Aligning the first paragraph should not modify its node type."); + is(line2.parentNode.style.textAlign, "right", + "Aligning the first paragraph should set a 'text-align: right' style rule."); + + // #test3: one text node, two paragraphs + const line3 = test3.firstChild; + test3.focus(); + justify(line3); + is(test3.querySelectorAll("*").length, 2, + "Aligning the first child should create a block element."); + is(line3.parentNode.nodeName.toLowerCase(), "div", + "Aligning the first child should create a block element."); + is(line3.parentNode.style.textAlign, "right", + "Aligning the first line should set a 'text-align: right' style rule."); + + // done + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug455992.html b/editor/libeditor/tests/test_bug455992.html new file mode 100644 index 0000000000..57f6716336 --- /dev/null +++ b/editor/libeditor/tests/test_bug455992.html @@ -0,0 +1,102 @@ +<!DOCTYPE HTML> +<html><head> +<title>Test for bug 455992</title> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> + +<script class="testbody" type="application/javascript"> +function runTest() { + function select(id) { + var e = document.getElementById(id); + e.focus(); + return e; + } + + function setupIframe(id) { + var e = document.getElementById(id); + var doc = e.contentDocument; + doc.body.innerHTML = String.fromCharCode(10) + '<span id="' + id + '_span" style="border:1px solid blue" contenteditable="true">X</span>' + String.fromCharCode(10); + e = doc.getElementById(id + "_span"); + e.focus(); + return e; + } + + function test_begin_bs(e) { + const msg = "BACKSPACE at beginning of contenteditable inline element"; + var before = e.parentNode.childNodes[0].nodeValue; + sendKey("back_space"); + is(e.parentNode.childNodes[0].nodeValue, before, msg + " with id=" + e.id); + is(e.innerHTML, "X", msg + " with id=" + e.id); + } + + function test_begin_space(e) { + const msg = "SPACE at beginning of contenteditable inline element"; + var before = e.parentNode.childNodes[0].nodeValue; + sendChar(" "); + is(e.parentNode.childNodes[0].nodeValue, before, msg + " with id=" + e.id); + is(e.innerHTML, " X", msg + " with id=" + e.id); + } + + function test_end_delete(e) { + const msg = "DEL at end of contenteditable inline element"; + var before = e.parentNode.childNodes[2].nodeValue; + sendKey("right"); + sendKey("delete"); + is(e.parentNode.childNodes[2].nodeValue, before, msg + " with id=" + e.id); + is(e.innerHTML, "X", msg + " with id=" + e.id); + } + + function test_end_space(e) { + const msg = "SPACE at end of contenteditable inline element"; + var before = e.parentNode.childNodes[2].nodeValue; + sendKey("right"); + sendChar(" "); + is(e.parentNode.childNodes[2].nodeValue, before, msg + " with id=" + e.id); + is( + e.innerHTML, + SpecialPowers.getBoolPref("editor.white_space_normalization.blink_compatible") || e.tagName == "SPAN" + ? "X " + : "X <br>", + msg + " with id=" + e.id + ); + } + + test_begin_bs(select("t1")); + test_begin_space(select("t2")); + test_end_delete(select("t3")); + test_end_space(select("t4")); + test_end_space(select("t5")); + + test_begin_bs(setupIframe("i1")); + test_begin_space(setupIframe("i2")); + test_end_delete(setupIframe("i3")); + test_end_space(setupIframe("i4")); + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTest); +</script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=455992">Mozilla Bug 455992</a> +<p id="display"></p> + +<pre id="test"> +</pre> + +<div> <span id="t1" style="border:1px solid blue" contenteditable="true">X</span> Y</div> +<div> <span id="t2" style="border:1px solid blue" contenteditable="true">X</span> Y</div> +<div> <span id="t3" style="border:1px solid blue" contenteditable="true">X</span> Y</div> +<div> <span id="t4" style="border:1px solid blue" contenteditable="true">X</span> Y</div> +<div> <div id="t5" style="border:1px solid blue" contenteditable="true">X</div> Y</div> + +<iframe id="i1" width="200" height="100" src="about:blank"></iframe><br> +<iframe id="i2" width="200" height="100" src="about:blank"></iframe><br> +<iframe id="i3" width="200" height="100" src="about:blank"></iframe><br> +<iframe id="i4" width="200" height="100" src="about:blank"></iframe><br> + +</body> +</html> diff --git a/editor/libeditor/tests/test_bug456244.html b/editor/libeditor/tests/test_bug456244.html new file mode 100644 index 0000000000..11ae5ad987 --- /dev/null +++ b/editor/libeditor/tests/test_bug456244.html @@ -0,0 +1,69 @@ +<!DOCTYPE HTML> +<html><head> +<title>Test for bug 456244</title> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> + +<script class="testbody" type="application/javascript"> +function runTest() { + function select(id) { + var e = document.getElementById(id); + e.focus(); + return e; + } + + function setupIframe(id) { + var e = document.getElementById(id); + var doc = e.contentDocument; + doc.body.innerHTML = String.fromCharCode(10) + '<span id="' + id + '_span" style="border:1px solid blue" contenteditable="true">X</span>' + String.fromCharCode(10); + e = doc.getElementById(id + "_span"); + e.focus(); + return e; + } + + function test_end_bs(e) { + const msg = "Deleting all text in contenteditable inline element"; + var before = e.parentNode.childNodes[0].nodeValue; + sendKey("right"); + sendKey("back_space"); + sendKey("back_space"); + is(e.parentNode.childNodes[0].nodeValue, before, msg + " with id=" + e.id); + is(e.innerHTML, "", msg + " with id=" + e.id); + } + + test_end_bs(select("t1")); + test_end_bs(setupIframe("i1", 0)); + + { + const msg = "Deleting all text in contenteditable body element"; + var e = document.getElementById("i2"); + var doc = e.contentDocument; + doc.body.setAttribute("contenteditable", "true"); + doc.body.focus(); + sendKey("right"); + sendKey("back_space"); + is(doc.body.innerHTML, "<br>", msg + " with id=" + e.id); + } + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTest); +</script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=456244">Mozilla Bug 456244</a> +<p id="display"></p> + +<pre id="test"> +</pre> + +<div> <span id="t1" style="border:1px solid blue" contenteditable="true">X</span> Y</div> + +<iframe id="i1" width="200" height="100" src="about:blank"></iframe><br> +<iframe id="i2" width="200" height="100" src="about:blank">X</iframe><br> + +</body> +</html> diff --git a/editor/libeditor/tests/test_bug460740.html b/editor/libeditor/tests/test_bug460740.html new file mode 100644 index 0000000000..509ad6d6ae --- /dev/null +++ b/editor/libeditor/tests/test_bug460740.html @@ -0,0 +1,124 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=460740 +--> +<head> + <title>Test for Bug 460740</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=460740">Mozilla Bug 460740</a> +<p id="display"></p> +<div id="content"> + <ul> + <li contenteditable> + Editable LI + </li> + <li> + <div contenteditable> + Editable DIV inside LI + </div> + </li> + <li> + <div> + <div contenteditable> + Editable DIV inside DIV inside LI + </div> + </div> + </li> + <li> + <h3> + <div contenteditable> + Editable DIV inside H3 inside LI + </div> + </h3> + </li> + </ul> + <div contenteditable> + Editable DIV + </div> + <h3 contenteditable> + Editable H3 + </h3> + <p contenteditable> + Editable P + </p> + <div> + <p contenteditable> + Editable P in a DIV + </p> + </div> + <p><span contenteditable>Editable SPAN in a P</span></p> +</div> + +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 460740 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + +const CARET_BEGIN = 0; +const CARET_MIDDLE = 1; +const CARET_END = 2; + +function split(element, caretPos) { + // compute the requested position + var len = element.textContent.length; + var pos = -1; + switch (caretPos) { + case CARET_BEGIN: + pos = 0; + break; + case CARET_MIDDLE: + pos = Math.floor(len / 2); + break; + case CARET_END: + pos = len; + break; + } + + // put the caret on the requested position + var range = document.createRange(); + var sel = window.getSelection(); + range.setStart(element.firstChild, pos); + range.setEnd(element.firstChild, pos); + sel.addRange(range); + + // simulates a [Return] keypress + synthesizeKey("VK_RETURN", {shiftKey: true}); +} + +// count the number of non-BR elements in #content +function getBlockCount() { + return document.querySelectorAll("#content *:not(br)").length; +} + +// count the number of BRs in element +function checkBR(element) { + return element.querySelectorAll("br").length; +} + +function runTests() { + var count = getBlockCount(); + var nodes = document.querySelectorAll("#content [contenteditable]"); + for (var i = 0; i < nodes.length; i++) { + var node = nodes[i]; + node.focus(); + is(checkBR(node), 0, node.textContent.trim() + ": This node should not have any <br> element yet."); + for (var j = 0; j < 3; j++) { // CARET_BEGIN|MIDDLE|END + split(node, j); + ok(checkBR(node) > 0, node.textContent.trim() + " " + j + ": Pressing [Return] should add (at least) one <br> element."); + is(getBlockCount(), count, node.textContent.trim() + " " + j + ": Pressing [Return] should not change the number of non-<br> elements."); + document.execCommand("Undo", false, null); + } + } + SimpleTest.finish(); +} +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug46555.html b/editor/libeditor/tests/test_bug46555.html new file mode 100644 index 0000000000..3838bdb3b2 --- /dev/null +++ b/editor/libeditor/tests/test_bug46555.html @@ -0,0 +1,47 @@ +<!DOCTYPE HTML> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=46555 +--> + +<head> + <title>Test for Bug 46555</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> + +<body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=46555">Mozilla Bug 46555</a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + + <input type="text" value="" id="t1" /> + + <pre id="test"> + <script type="application/javascript"> + + /** Test for Bug 46555 **/ + SimpleTest.waitForExplicitFinish(); + SimpleTest.waitForFocus(function() { + const kCmd = "cmd_selectAll"; + + var input = document.getElementById("t1"); + input.focus(); + var controller = + SpecialPowers.wrap(input).controllers.getControllerForCommand(kCmd); + + // Test 1: Select All should be disabled if editor is empty + is(controller.isCommandEnabled(kCmd), false, + "Select All command disabled when editor is empty"); + + SimpleTest.finish(); + }); + </script> + </pre> + +</body> +</html> diff --git a/editor/libeditor/tests/test_bug471319.html b/editor/libeditor/tests/test_bug471319.html new file mode 100644 index 0000000000..5a180cb310 --- /dev/null +++ b/editor/libeditor/tests/test_bug471319.html @@ -0,0 +1,77 @@ +<!DOCTYPE HTML> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=471319 +--> + +<head> + <title>Test for Bug 471319</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> + +<body onload="doTest();"> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=471319">Mozilla Bug 471319</a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + + <pre id="test"> + <script type="application/javascript"> + + /** Test for Bug 471319 **/ + + SimpleTest.waitForExplicitFinish(); + + function doTest() { + let t1 = SpecialPowers.wrap($("t1")); + + // Test 1: Undo on an empty editor - the editor should not forget about + // the padding <br> element for empty editor. + let t1Editor = t1.editor; + + // Did the editor recognize the new padding <br> element? + t1Editor.undo(); + ok(!t1.value, "<br> still recognized as padding on undo"); + + + // Test 2: Redo on an empty editor - the editor should not forget about + // the padding <br> element for empty editor. + let t2 = SpecialPowers.wrap($("t2")); + let t2Editor = t2.editor; + + // Did the editor recognize the new padding <br> element? + t2Editor.redo(); + ok(!t2.value, "<br> still recognized as padding on redo"); + + + // Test 3: Undoing a batched transaction where both end points of the + // transaction are the padding <br> element for empty editor - the + // <br> element should still be recognized as padding. + t1Editor.beginTransaction(); + t1.value = "mozilla"; + t1.value = ""; + t1Editor.endTransaction(); + t1Editor.undo(); + ok(!t1.value, + "recreated <br> from undo transaction recognized as padding"); + + + // Test 4: Redoing a batched transaction where both end points of the + // transaction are the padding <br> element for empty editor - the + // <br> element should still be recognized padding. + t1Editor.redo(); + ok(!t1.value, + "recreated <br> from redo transaction recognized as padding"); + SimpleTest.finish(); + } + </script> + </pre> + + <input type="text" id="t1" /> + <input type="text" id="t2" /> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug471722.html b/editor/libeditor/tests/test_bug471722.html new file mode 100644 index 0000000000..188c30d245 --- /dev/null +++ b/editor/libeditor/tests/test_bug471722.html @@ -0,0 +1,74 @@ +<!DOCTYPE HTML> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=471722 +--> + +<head> + <title>Test for Bug 471722</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> + +<body onload="doTest();"> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=471722">Mozilla Bug 471722</a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + + <pre id="test"> + <script type="application/javascript"> + + /** Test for Bug 471722 **/ + + SimpleTest.waitForExplicitFinish(); + + function doTest() { + var t1 = $("t1"); + var editor = SpecialPowers.wrap(t1).editor; + + ok(editor, "able to get editor for the element"); + t1.focus(); + t1.select(); + + try { + // Cut the initial text in the textbox + ok(editor.canCut(), "can cut text"); + editor.cut(); + is(t1.value, "", "initial text was removed"); + + // So now we will have emptied the textfield and the editor will have + // created a padding <br> element for empty editor. + // Check the transaction is in the undo stack... + ok(editor.canUndo, "undo should be available"); + + // Undo the cut + editor.undo(); + is(t1.value, "minefield", "text reinserted"); + + // So now, the cut should be in the redo stack, so executing the redo + // will clear the text once again and reinsert the padding <br> + // element for empty editor that was removed after undo. + // This will require the editor to figure out that we have a padding + // <br> element again... + ok(editor.canRedo, "redo should be available"); + editor.redo(); + + // Did the editor notice a padding <br> element for empty editor + // reappeared? + is(t1.value, "", "editor found padding <br> element"); + } catch (e) { + ok(false, "test failed with error " + e); + } + SimpleTest.finish(); + } + </script> + </pre> + + <input type="text" value="minefield" id="t1" /> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug478725.html b/editor/libeditor/tests/test_bug478725.html new file mode 100644 index 0000000000..45e6aeed13 --- /dev/null +++ b/editor/libeditor/tests/test_bug478725.html @@ -0,0 +1,130 @@ +<!DOCTYPE HTML> +<html><head> +<title>Test for bug 478725</title> +<style src="/tests/SimpleTest/test.css" type="text/css"></style> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> + +<script class="testbody" type="application/javascript"> + +function runTest() { + function verifyContent(s) { + var e = document.getElementById("i1"); + var doc = e.contentDocument; + is(doc.body.innerHTML, s, ""); + } + + function pasteInto(html, target_id) { + var e = document.getElementById("i1"); + var doc = e.contentDocument; + doc.designMode = "on"; + doc.body.innerHTML = html; + e = doc.getElementById(target_id); + doc.defaultView.focus(); + var selection = doc.defaultView.getSelection(); + selection.removeAllRanges(); + selection.selectAllChildren(e); + selection.collapseToEnd(); + SpecialPowers.wrap(doc).execCommand("paste", false, null); + return e; + } + + function copyToClipBoard(s, asHTML, target_id) { + var e = document.getElementById("i2"); + var doc = e.contentDocument; + if (asHTML) { + doc.body.innerHTML = s; + } else { + var text = doc.createTextNode(s); + doc.body.appendChild(text); + } + doc.designMode = "on"; + doc.defaultView.focus(); + var selection = doc.defaultView.getSelection(); + selection.removeAllRanges(); + if (!target_id) { + selection.selectAllChildren(doc.body); + } else { + var range = document.createRange(); + range.selectNode(doc.getElementById(target_id)); + selection.addRange(range); + } + SpecialPowers.wrap(doc).execCommand("copy", false, null); + return e; + } + + copyToClipBoard("<dl><dd>Hello Kitty</dd></dl>", true); + pasteInto('<ol><li id="paste_here">X</li></ol>', "paste_here"); + verifyContent('<ol><li id="paste_here">X<dl><dd>Hello Kitty</dd></dl></li></ol>'); + + copyToClipBoard("<li>Hello Kitty</li>", true); + pasteInto('<ol><li id="paste_here">X</li></ol>', "paste_here"); + verifyContent('<ol><li id="paste_here">X</li><li>Hello Kitty</li></ol>'); + + copyToClipBoard("<ol><li>Hello Kitty</li></ol>", true); + pasteInto('<ol><li id="paste_here">X</li></ol>', "paste_here"); + verifyContent('<ol><li id="paste_here">X</li><li>Hello Kitty</li></ol>'); + + copyToClipBoard("<ul><li>Hello Kitty</li></ul>", true); + pasteInto('<ol><li id="paste_here">X</li></ol>', "paste_here"); + verifyContent('<ol><li id="paste_here">X</li><li>Hello Kitty</li></ol>'); + + copyToClipBoard("<ul><li>Hello</li><ul><li>Kitty</li></ul></ul>", true); + pasteInto('<ol><li id="paste_here">X</li></ol>', "paste_here"); + verifyContent('<ol><li id="paste_here">X</li><li>Hello</li><ul><li>Kitty</li></ul></ol>'); + + copyToClipBoard("<dl><dd>Hello</dd><dd>Kitty</dd></dl>", true); + pasteInto('<dl><dd id="paste_here">X</dd></dl>', "paste_here"); + verifyContent('<dl><dd id="paste_here">X</dd><dd>Hello</dd><dd>Kitty</dd></dl>'); + + copyToClipBoard("<dl><dd>Hello</dd><dd>Kitty</dd></dl>", true); + pasteInto('<dl><dt id="paste_here">X</dt></dl>', "paste_here"); + verifyContent('<dl><dt id="paste_here">X</dt><dd>Hello</dd><dd>Kitty</dd></dl>'); + + copyToClipBoard("<dl><dt>Hello</dt><dd>Kitty</dd></dl>", true); + pasteInto('<dl><dd id="paste_here">X</dd></dl>', "paste_here"); + verifyContent('<dl><dd id="paste_here">X</dd><dt>Hello</dt><dd>Kitty</dd></dl>'); + + copyToClipBoard("<pre>Kitty</pre>", true); + pasteInto('<pre id="paste_here">Hello </pre>', "paste_here"); + verifyContent('<pre id="paste_here">Hello Kitty</pre>'); + +// I was expecting these to trigger the special TABLE/TR rules in nsHTMLEditor::InsertHTMLWithContext +// but they don't for some reason... +// copyToClipBoard('<table><tr id="copy_here"><td>Kitty</td></tr></table>', true, "copy_here"); +// pasteInto('<table><tr id="paste_here"><td>Hello</td></tr></table>',"paste_here"); +// verifyContent(''); +// +// copyToClipBoard('<table id="copy_here"><tr><td>Kitty</td></tr></table>', true, "copy_here"); +// pasteInto('<table><tr id="paste_here"><td>Hello</td></tr></table>',"paste_here"); +// verifyContent(''); +// +// copyToClipBoard('<table id="copy_here"><tr><td>Kitty</td></tr></table>', true, "copy_here"); +// pasteInto('<table id="paste_here"><tr><td>Hello</td></tr></table>',"paste_here"); +// verifyContent(''); +// +// copyToClipBoard('<table><tr id="copy_here"><td>Kitty</td></tr></table>', true, "copy_here"); +// pasteInto('<table id="paste_here"><tr><td>Hello</td></tr></table>',"paste_here"); +// verifyContent(''); + + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTest); +</script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=478725">Mozilla Bug 478725</a> +<p id="display"></p> + +<pre id="test"> +</pre> + + +<iframe id="i1" width="200" height="100" src="about:blank"></iframe><br> +<iframe id="i2" width="200" height="100" src="about:blank"></iframe><br> + +</body> +</html> diff --git a/editor/libeditor/tests/test_bug480647.html b/editor/libeditor/tests/test_bug480647.html new file mode 100644 index 0000000000..33f088a1b1 --- /dev/null +++ b/editor/libeditor/tests/test_bug480647.html @@ -0,0 +1,110 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=480647 +--> +<title>Test for Bug 480647</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=480647">Mozilla Bug 480647</a> +<div contenteditable></div> +<script> +/** Test for Bug 480647 **/ + +var div = document.querySelector("div"); + +function parseFontSize(input, expected) { + parseFontSizeInner(input, expected, is); +} + +function parseFontSizeTodo(input, expected) { + parseFontSizeInner(input, expected, todo_is); +} + +function parseFontSizeInner(input, expected, fn) { + div.innerHTML = "foo"; + getSelection().selectAllChildren(div); + document.execCommand("fontSize", false, input); + if (expected === null) { + fn(div.innerHTML, "foo", + 'execCommand("fontSize", false, "' + input + '") should be no-op'); + } else { + fn(div.innerHTML, '<font size="' + expected + '">foo</font>', + 'execCommand("fontSize", false, "' + input + '") should parse to ' + + expected); + } +} + +// Parse errors +parseFontSize("", null); +parseFontSize("abc", null); +parseFontSize("larger", null); +parseFontSize("smaller", null); +parseFontSize("xx-small", null); +parseFontSize("x-small", null); +parseFontSize("small", null); +parseFontSize("medium", null); +parseFontSize("large", null); +parseFontSize("x-large", null); +parseFontSize("xx-large", null); +parseFontSize("xxx-large", null); +// Bug 747879 +parseFontSizeTodo("1.2em", null); +parseFontSizeTodo("8px", null); +parseFontSizeTodo("-1.2em", null); +parseFontSizeTodo("-8px", null); +parseFontSizeTodo("+1.2em", null); +parseFontSizeTodo("+8px", null); + +// Numbers +parseFontSize("0", 1); +parseFontSize("1", 1); +parseFontSize("2", 2); +parseFontSize("3", 3); +parseFontSize("4", 4); +parseFontSize("5", 5); +parseFontSize("6", 6); +parseFontSize("7", 7); +parseFontSize("8", 7); +parseFontSize("9", 7); +parseFontSize("10", 7); +parseFontSize("1000000000000000000000", 7); +parseFontSize("2.72", 2); +parseFontSize("2.72e9", 2); + +// Minus sign +parseFontSize("-0", 3); +parseFontSize("-1", 2); +parseFontSize("-2", 1); +parseFontSize("-3", 1); +parseFontSize("-4", 1); +parseFontSize("-5", 1); +parseFontSize("-6", 1); +parseFontSize("-7", 1); +parseFontSize("-8", 1); +parseFontSize("-9", 1); +parseFontSize("-10", 1); +parseFontSize("-1000000000000000000000", 1); +parseFontSize("-1.72", 2); +parseFontSize("-1.72e9", 2); + +// Plus sign +parseFontSize("+0", 3); +parseFontSize("+1", 4); +parseFontSize("+2", 5); +parseFontSize("+3", 6); +parseFontSize("+4", 7); +parseFontSize("+5", 7); +parseFontSize("+6", 7); +parseFontSize("+7", 7); +parseFontSize("+8", 7); +parseFontSize("+9", 7); +parseFontSize("+10", 7); +parseFontSize("+1000000000000000000000", 7); +parseFontSize("+1.72", 4); +parseFontSize("+1.72e9", 4); + +// Whitespace +parseFontSize(" \t\n\r\f5 \t\n\r\f", 5); +parseFontSize("\u00a05", null); +parseFontSize("\b5", null); +</script> diff --git a/editor/libeditor/tests/test_bug480972.html b/editor/libeditor/tests/test_bug480972.html new file mode 100644 index 0000000000..37037b756a --- /dev/null +++ b/editor/libeditor/tests/test_bug480972.html @@ -0,0 +1,97 @@ +<!DOCTYPE HTML> +<html><head> +<title>Test for bug 480972</title> +<style src="/tests/SimpleTest/test.css" type="text/css"></style> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> + +<script class="testbody" type="application/javascript"> + +function runTest() { + function verifyContent(s) { + var e = document.getElementById("i1"); + var doc = e.contentDocument; + is(doc.body.innerHTML, s, ""); + } + + function pasteInto(html, target_id) { + var e = document.getElementById("i1"); + var doc = e.contentDocument; + doc.designMode = "on"; + doc.body.innerHTML = html; + doc.defaultView.focus(); + if (target_id) + e = doc.getElementById(target_id); + else + e = doc.body; + var selection = doc.defaultView.getSelection(); + selection.removeAllRanges(); + selection.selectAllChildren(e); + selection.collapseToEnd(); + SpecialPowers.wrap(doc).execCommand("paste", false, null); + return e; + } + + function copyToClipBoard(s, asHTML, target_id) { + var e = document.getElementById("i2"); + var doc = e.contentDocument; + if (asHTML) { + doc.body.innerHTML = s; + } else { + var text = doc.createTextNode(s); + doc.body.appendChild(text); + } + doc.designMode = "on"; + doc.defaultView.focus(); + var selection = doc.defaultView.getSelection(); + selection.removeAllRanges(); + if (!target_id) { + selection.selectAllChildren(doc.body); + } else { + var range = document.createRange(); + range.selectNode(doc.getElementById(target_id)); + selection.addRange(range); + } + SpecialPowers.wrap(doc).execCommand("copy", false, null); + return e; + } + + copyToClipBoard("<span>Hello</span><span>Kitty</span>", true); + pasteInto(""); + verifyContent("<span>Hello</span><span>Kitty</span>"); + + copyToClipBoard("<dl><dd>Hello Kitty</dd></dl><span>Hello</span><span>Kitty</span>", true); + pasteInto('<ol><li id="paste_here">X</li></ol>', "paste_here"); + verifyContent('<ol><li id="paste_here">X<dl><dd>Hello Kitty</dd></dl><span>Hello</span><span>Kitty</span></li></ol>'); + +// The following test doesn't do what I expected, because the special handling +// of IsList nodes in nsHTMLEditor::InsertHTMLWithContext simply removes +// non-list/item children. See bug 481177. +// copyToClipBoard("<ol><li>Hello Kitty</li><span>Hello</span></ol>", true); +// pasteInto('<ol><li id="paste_here">X</li></ol>',"paste_here"); +// verifyContent('<ol><li id="paste_here">X</li><li>Hello Kitty</li><span>Hello</span></ol>'); + + copyToClipBoard("<pre>Kitty</pre><span>Hello</span>", true); + pasteInto('<pre id="paste_here">Hello </pre>', "paste_here"); + verifyContent('<pre id="paste_here">Hello Kitty<span>Hello</span></pre>'); + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTest); +</script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=480972">Mozilla Bug 480972</a> +<p id="display"></p> + +<pre id="test"> +</pre> + + +<iframe id="i1" width="200" height="100" src="about:blank"></iframe><br> +<iframe id="i2" width="200" height="100" src="about:blank"></iframe><br> + +</body> +</html> diff --git a/editor/libeditor/tests/test_bug483651.html b/editor/libeditor/tests/test_bug483651.html new file mode 100644 index 0000000000..4ad570aa8b --- /dev/null +++ b/editor/libeditor/tests/test_bug483651.html @@ -0,0 +1,52 @@ +<!DOCTYPE HTML> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=483651 +--> + +<head> + <title>Test for Bug 483651</title> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> + +<body onload="doTest();"> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=483651">Mozilla Bug 483651</a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + + <pre id="test"> + <script type="application/javascript"> + + /** Test for Bug 483651 **/ + + SimpleTest.waitForExplicitFinish(); + + function doTest() { + var t1 = $("t1"); + var editor = SpecialPowers.wrap(t1).editor; + + ok(editor, "able to get editor for the element"); + t1.focus(); + sendString("A"); + synthesizeKey("KEY_Backspace"); + + try { + // Was the trailing br removed? + is(editor.documentIsEmpty, true, "trailing <br> correctly removed"); + } catch (e) { + ok(false, "test failed with error " + e); + } + SimpleTest.finish(); + } + </script> + </pre> + + <textarea id="t1" rows="2" columns="80"></textarea> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug489202.xhtml b/editor/libeditor/tests/test_bug489202.xhtml new file mode 100644 index 0000000000..6b26573f69 --- /dev/null +++ b/editor/libeditor/tests/test_bug489202.xhtml @@ -0,0 +1,73 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" + type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=489202 +--> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Mozilla Bug 489202" onload="runTest();"> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=489202" + target="_blank">Mozilla Bug 489202</a> + <p/> + <editor xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + id="i1" + type="content" + editortype="htmlmail" + style="width: 400px; height: 100px;"/> + <p/> + <pre id="test"> + </pre> + </body> + <script class="testbody" type="application/javascript"> + <![CDATA[ + var utils = SpecialPowers.getDOMWindowUtils(window); + +function getLoadContext() { + return window.docShell.QueryInterface(Ci.nsILoadContext); +} + +function runTest() { + var trans = Cc["@mozilla.org/widget/transferable;1"] + .createInstance(Ci.nsITransferable); + trans.init(getLoadContext()); + trans.addDataFlavor("text/html"); + var test_data = '<meta/><a href="http://mozilla.org/">mozilla.org</a>'; + var cstr = Cc["@mozilla.org/supports-string;1"] + .createInstance(Ci.nsISupportsString); + cstr.data = test_data; + trans.setTransferData("text/html", cstr); + + window.docShell + .rootTreeItem + .QueryInterface(Ci.nsIDocShell) + .appType = Ci.nsIDocShell.APP_TYPE_EDITOR; + var e = document.getElementById('i1'); + var doc = e.contentDocument; + doc.designMode = "on"; + doc.body.innerHTML = ""; + doc.defaultView.focus(); + var selection = doc.defaultView.getSelection(); + selection.removeAllRanges(); + selection.selectAllChildren(doc.body); + selection.collapseToEnd(); + + var point = doc.defaultView.getSelection().getRangeAt(0).startOffset; + ok(point==0, "Cursor should be at editor start before paste"); + + utils.sendContentCommandEvent("pasteTransferable", trans); + + point = doc.defaultView.getSelection().getRangeAt(0).startOffset; + ok(point>0, "Cursor should not be at editor start after paste"); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +]]> +</script> +</window> diff --git a/editor/libeditor/tests/test_bug490879.html b/editor/libeditor/tests/test_bug490879.html new file mode 100644 index 0000000000..68458862db --- /dev/null +++ b/editor/libeditor/tests/test_bug490879.html @@ -0,0 +1,54 @@ +<!doctype html> +<title>Mozilla Bug 490879</title> +<link rel=stylesheet href="/tests/SimpleTest/test.css"> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=490879" + target="_blank">Mozilla Bug 490879</a> +<iframe id="i1" width="200" height="100" src="about:blank"></iframe> +<img id="i" src="green.png"> +<script> +async function runTest() { + function verifyContent() { + const kExpectedImgSpec = "data:image/png;base64,"; + var e = document.getElementById("i1"); + var doc = e.contentDocument; + is(doc.getElementsByTagName("img")[0].src.substring(0, kExpectedImgSpec.length), + kExpectedImgSpec, "The pasted image is a base64-encoded data: URI"); + } + + async function pasteInto() { + var e = document.getElementById("i1"); + var doc = e.contentDocument; + doc.designMode = "on"; + doc.defaultView.focus(); + var selection = doc.defaultView.getSelection(); + selection.removeAllRanges(); + selection.selectAllChildren(doc.body); + selection.collapseToEnd(); + + let input = new Promise(resolve => { + doc.body.addEventListener("input", resolve, {once: true}) + }); + + SpecialPowers.doCommand(window, "cmd_paste"); + + info("Waiting for input event"); + await input; + } + + function copyToClipBoard() { + SpecialPowers.setCommandNode(window, document.getElementById("i")); + SpecialPowers.doCommand(window, "cmd_copyImageContents"); + } + + copyToClipBoard(); + + await pasteInto(); + + verifyContent(); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTest); +</script> diff --git a/editor/libeditor/tests/test_bug502673.html b/editor/libeditor/tests/test_bug502673.html new file mode 100644 index 0000000000..0850cf3de0 --- /dev/null +++ b/editor/libeditor/tests/test_bug502673.html @@ -0,0 +1,97 @@ +<!DOCTYPE HTML> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=502673 +--> + +<head> + <title>Test for Bug 502673</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> + +<body onload="doTest();"> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=502673">Mozilla Bug 502673</a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + + <pre id="test"> + <script type="application/javascript"> + + /** Test for Bug 502673 **/ + + SimpleTest.waitForExplicitFinish(); + + function listener() { + } + + listener.prototype = + { + NotifyDocumentWillBeDestroyed() { + var editor = SpecialPowers.wrap(this.input).editor; + editor.removeDocumentStateListener(this); + }, + + NotifyDocumentStateChanged(aNowDirty) { + var editor = SpecialPowers.wrap(this.input).editor; + editor.removeDocumentStateListener(this); + }, + + QueryInterface: SpecialPowers.wrapCallback(function(iid) { + if (iid.equals(SpecialPowers.Ci.nsIDocumentStateListener) || + iid.equals(SpecialPowers.Ci.nsISupports)) + return this; + throw SpecialPowers.Cr.NS_ERROR_NO_INTERFACE; + }), + }; + + function doTest() { + var input = document.getElementById("ip"); + + // Add multiple listeners to the same editor + var editor = SpecialPowers.wrap(input).editor; + var listener1 = new listener(); + listener1.input = input; + var listener2 = new listener(); + listener2.input = input; + var listener3 = new listener(); + listener3.input = input; + editor.addDocumentStateListener(listener1); + editor.addDocumentStateListener(listener2); + editor.addDocumentStateListener(listener3); + + // Test 1. Fire NotifyDocumentStateChanged notifications where the + // listeners remove themselves + input.value = "mozilla"; + editor.undo(); + + // Report success if we get here - clearly we didn't crash + ok(true, "Multiple listeners removed themselves after " + + "NotifyDocumentStateChanged notifications - didn't crash"); + + // Add the listeners again for the next test + editor.addDocumentStateListener(listener1); + editor.addDocumentStateListener(listener2); + editor.addDocumentStateListener(listener3); + + // Test 2. Fire NotifyDocumentWillBeDestroyed notifications where the + // listeners remove themselves (though in the real world, listeners + // shouldn't do this as nsEditor::PreDestroy removes them as + // listeners anyway) + document.body.removeChild(input); + ok(true, "Multiple listeners removed themselves after " + + "NotifyDocumentWillBeDestroyed notifications - didn't crash"); + + SimpleTest.finish(); + } + </script> + </pre> + + <input type="text" id="ip" /> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug514156.html b/editor/libeditor/tests/test_bug514156.html new file mode 100644 index 0000000000..1331d62b12 --- /dev/null +++ b/editor/libeditor/tests/test_bug514156.html @@ -0,0 +1,46 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=514156 +--> +<head> + <title>Test for Bug 514156</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/WindowSnapshot.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="test()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=514156">Mozilla Bug 514156</a> +<p id="display"></p> +<div id="content"> +<input type="text" id="input1"> +<input type="text" id="input2"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 514156 **/ + +SimpleTest.waitForExplicitFinish(); + +function test() { + var input1 = $("input1"); + input1.focus(); + sendString("\u200e\u05d0\u05d1"); + is(escape(input1.value), escape("\u200e\u05d0\u05d1"), "non-spacing character and direction change shouldn't change content"); + + var input2 = $("input2"); + input2.focus(); + sendString("\u05b6"); + sendString("abc"); + is(escape(input2.value), escape("\u05b6abc"), "non-spacing character and direction change shouldn't change content"); + + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> + diff --git a/editor/libeditor/tests/test_bug520189.html b/editor/libeditor/tests/test_bug520189.html new file mode 100644 index 0000000000..0e8d286abc --- /dev/null +++ b/editor/libeditor/tests/test_bug520189.html @@ -0,0 +1,618 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=520182 +--> +<head> + <title>Test for Bug 520182</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=520182">Mozilla Bug 520182</a> +<p id="display"></p> +<div id="content"> + <iframe id="a" src="about:blank"></iframe> + <iframe id="b" src="about:blank"></iframe> + <iframe id="c" src="about:blank"></iframe> + <div id="d" contenteditable="true"></div> + <div id="e" contenteditable="true"></div> + <div id="f" contenteditable="true"></div> + <iframe id="g" src="about:blank"></iframe> + <iframe id="h" src="about:blank"></iframe> + <div id="i" contenteditable="true"></div> + <div id="j" contenteditable="true"></div> + <iframe id="k" src="about:blank"></iframe> + <div id="l" contenteditable="true"></div> + <iframe id="m" src="about:blank"></iframe> + <div id="n" contenteditable="true"></div> + <iframe id="o" src="about:blank"></iframe> + <div id="p" contenteditable="true"></div> + <iframe id="q" src="about:blank"></iframe> + <div id="r" contenteditable="true"></div> + <iframe id="s" src="about:blank"></iframe> + <div id="t" contenteditable="true"></div> + <iframe id="u" src="about:blank"></iframe> + <div id="v" contenteditable="true"></div> + <iframe id="w" src="about:blank"></iframe> + <div id="x" contenteditable="true"></div> + <iframe id="y" src="about:blank"></iframe> + <div id="z" contenteditable="true"></div> + <iframe id="aa" src="about:blank"></iframe> + <div id="bb" contenteditable="true"></div> + <iframe id="cc" src="about:blank"></iframe> + <div id="dd" contenteditable="true"></div> + <iframe id="ee" src="about:blank"></iframe> + <div id="ff" contenteditable="true"></div> + <iframe id="gg" src="about:blank"></iframe> + <div id="hh" contenteditable="true"></div> + <iframe id="ii" src="about:blank"></iframe> + <div id="jj" contenteditable="true"></div> + <iframe id="kk" src="about:blank"></iframe> + <div id="ll" contenteditable="true"></div> + <iframe id="mm" src="about:blank"></iframe> + <div id="nn" contenteditable="true"></div> + <iframe id="oo" src="about:blank"></iframe> + <div id="pp" contenteditable="true"></div> + <iframe id="qq" src="about:blank"></iframe> + <div id="rr" contenteditable="true"></div> + <iframe id="ss" src="about:blank"></iframe> + <div id="tt" contenteditable="true"></div> + <iframe id="uu" src="about:blank"></iframe> + <div id="vv" contenteditable="true"></div> + <div id="sss" contenteditable="true"></div> + <iframe id="ssss" src="about:blank"></iframe> + <div id="ttt" contenteditable="true"></div> + <iframe id="tttt" src="about:blank"></iframe> + <div id="uuu" contenteditable="true"></div> + <iframe id="uuuu" src="about:blank"></iframe> + <div id="vvv" contenteditable="true"></div> + <iframe id="vvvv" src="about:blank"></iframe> + <div id="www" contenteditable="true"></div> + <iframe id="wwww" src="about:blank"></iframe> + <div id="xxx" contenteditable="true"></div> + <iframe id="xxxx" src="about:blank"></iframe> + <div id="yyy" contenteditable="true"></div> + <iframe id="yyyy" src="about:blank"></iframe> +</div> +<pre id="test"> +<script type="application/javascript"> + +/* eslint-disable no-useless-concat */ + +/** Test for Bug 520182 **/ + +const dataPayload = "foo<iframe src=\"data:text/html,bar\"></iframe>baz"; +const jsPayload = "foo<iframe src=\"javascript:void('bar');\"></iframe>baz"; +const httpPayload = "foo<iframe src=\"http://mochi.test:8888/\"></iframe>baz"; +const scriptPayload = "foo<script>document.write(\"<iframe></iframe>\");</sc" + "ript>baz"; +const scriptExternalPayload = "foo<script src=\"data:text/javascript,document.write('<iframe></iframe>');\"></sc" + "ript>baz"; +const validStyle1Payload = "foo<style>#bar{color:red;}</style>baz"; +const validStyle2Payload = "foo<span style=\"color:red\">bar</span>baz"; +const validStyle3Payload = "foo<style>@font-face{font-family:xxx;src:'xxx.ttf';}</style>baz"; +const validStyle4Payload = "foo<style>@namespace xxx url(http://example.com/);</style>baz"; +const invalidStyle1Payload = "foo<style>#bar{-moz-binding:url('data:text/xml,<?xml version=\"1.0\"><binding xmlns=\"http://www.mozilla.org/xbl\"/>');}</style>baz"; +const invalidStyle2Payload = "foo<span style=\"-moz-binding:url('data:text/xml,<?xml version="1.0"><binding xmlns="http://www.mozilla.org/xbl"/>');\">bar</span>baz"; +const invalidStyle3Payload = "foo<style>@import 'xxx.css';</style>baz"; +const invalidStyle4Payload = "foo<span style=\"@import 'xxx.css';\">bar</span>baz"; +const invalidStyle5Payload = "foo<span style=\"@font-face{font-family:xxx;src:'xxx.ttf';}\">bar</span>baz"; +const invalidStyle6Payload = "foo<span style=\"@namespace xxx url(http://example.com/);\">bar</span>baz"; +const invalidStyle7Payload = "<html><head><title>xxx</title></head><body>foo</body></html>"; +const invalidStyle8Payload = "foo<style>@-moz-document url-prefix() {};</style>baz"; +const invalidStyle9Payload = "foo<style>@keyframes bar {};</style>baz"; +const nestedStylePayload = "foo<style>#bar1{-moz-binding:url('data:text/xml,<?xml version="1.0"><binding xmlns="http://www.mozilla.org/xbl" id="binding-1"/>');<style></style>#bar2{-moz-binding:url('data:text/xml,<?xml version="1.0"><binding xmlns="http://www.mozilla.org/xbl" id="binding-2"/>');</style>baz"; +const validImgSrc1Payload = "foo<img src=\"data:image/png,bar\">baz"; +const validImgSrc2Payload = "foo<img src=\"javascript:void('bar');\">baz"; +const validImgSrc3Payload = "foo<img src=\"file:///bar.png\">baz"; +const validDataFooPayload = "foo<span data-bar=\"value\">baz</span>"; +const validDataFoo2Payload = "foo<span _bar=\"value\">baz</span>"; +const svgPayload = "foo<svg><title>svgtitle</title></svg>bar"; +const svg2Payload = "foo<svg><bogussvg/></svg>bar"; +const mathPayload = "foo<math><bogusmath/></math>bar"; +const math2Payload = "foo<math><style>@import \"yyy.css\";</style</math>bar"; +const math3Payload = "foo<math><mi></mi></math>bar"; +const videoPayload = "foo<video></video>bar"; +const microdataPayload = "<head><meta name=foo content=bar><link rel=stylesheet href=url></head><body><meta itemprop=foo content=bar><link itemprop=bar href=url></body>"; + +var tests = [ + { + id: "a", + isIFrame: true, + payload: dataPayload, + iframeCount: 0, + rootElement() { return document.getElementById("a").contentDocument.documentElement; }, + }, + { + id: "b", + isIFrame: true, + payload: jsPayload, + iframeCount: 0, + rootElement() { return document.getElementById("b").contentDocument.documentElement; }, + }, + { + id: "c", + isIFrame: true, + payload: httpPayload, + iframeCount: 0, + rootElement() { return document.getElementById("c").contentDocument.documentElement; }, + }, + { + id: "g", + isIFrame: true, + payload: scriptPayload, + rootElement() { return document.getElementById("g").contentDocument.documentElement; }, + iframeCount: 0, + }, + { + id: "h", + isIFrame: true, + payload: scriptExternalPayload, + rootElement() { return document.getElementById("h").contentDocument.documentElement; }, + iframeCount: 0, + }, + { + id: "d", + payload: dataPayload, + iframeCount: 0, + rootElement() { return document.getElementById("d"); }, + }, + { + id: "e", + payload: jsPayload, + iframeCount: 0, + rootElement() { return document.getElementById("e"); }, + }, + { + id: "f", + payload: httpPayload, + iframeCount: 0, + rootElement() { return document.getElementById("f"); }, + }, + { + id: "i", + payload: scriptPayload, + rootElement() { return document.getElementById("i"); }, + iframeCount: 0, + }, + { + id: "j", + payload: scriptExternalPayload, + rootElement() { return document.getElementById("j"); }, + iframeCount: 0, + }, + { + id: "k", + isIFrame: true, + payload: validStyle1Payload, + rootElement() { return document.getElementById("k").contentDocument.documentElement; }, + checkResult(html) { isnot(html.indexOf("style"), -1, "Should have retained style"); }, + }, + { + id: "l", + payload: validStyle1Payload, + rootElement() { return document.getElementById("l"); }, + checkResult(html) { isnot(html.indexOf("style"), -1, "Should have retained style"); }, + }, + { + id: "m", + isIFrame: true, + payload: validStyle2Payload, + rootElement() { return document.getElementById("m").contentDocument.documentElement; }, + checkResult(html) { isnot(html.indexOf("style"), -1, "Should have retained style"); }, + }, + { + id: "n", + payload: validStyle2Payload, + rootElement() { return document.getElementById("n"); }, + checkResult(html) { isnot(html.indexOf("style"), -1, "Should have retained style"); }, + }, + { + id: "s", + isIFrame: true, + payload: invalidStyle1Payload, + rootElement() { return document.getElementById("s").contentDocument.documentElement; }, + checkResult(html) { is(html.indexOf("xxx"), -1, "Should not have retained the import style"); }, + }, + { + id: "t", + payload: invalidStyle1Payload, + rootElement() { return document.getElementById("t"); }, + checkResult(html) { is(html.indexOf("xxx"), -1, "Should not have retained the import style"); }, + }, + { + id: "u", + isIFrame: true, + payload: invalidStyle2Payload, + rootElement() { return document.getElementById("u").contentDocument.documentElement; }, + checkResult(html) { is(html.indexOf("xxx"), -1, "Should not have retained the import style"); }, + }, + { + id: "v", + payload: invalidStyle2Payload, + rootElement() { return document.getElementById("v"); }, + checkResult(html) { is(html.indexOf("xxx"), -1, "Should not have retained the import style"); }, + }, + { + id: "w", + isIFrame: true, + payload: validStyle3Payload, + rootElement() { return document.getElementById("w").contentDocument.documentElement; }, + checkResult(html) { isnot(html.indexOf("xxx"), -1, "Should have retained the font-face style"); }, + }, + { + id: "x", + payload: validStyle3Payload, + rootElement() { return document.getElementById("x"); }, + checkResult(html) { isnot(html.indexOf("xxx"), -1, "Should have retained the font-face style"); }, + }, + { + id: "y", + isIFrame: true, + payload: invalidStyle5Payload, + rootElement() { return document.getElementById("y").contentDocument.documentElement; }, + checkResult(html) { isnot(html.indexOf("xxx"), -1, "Should not have retained the font-face style"); }, + }, + { + id: "z", + payload: invalidStyle5Payload, + rootElement() { return document.getElementById("z"); }, + checkResult(html) { isnot(html.indexOf("xxx"), -1, "Should not have retained the font-face style"); }, + }, + { + id: "cc", + isIFrame: true, + payload: validStyle4Payload, + rootElement() { return document.getElementById("cc").contentDocument.documentElement; }, + checkResult(html) { isnot(html.indexOf("xxx"), -1, "Should have retained the namespace style"); }, + }, + { + id: "dd", + payload: validStyle4Payload, + rootElement() { return document.getElementById("dd"); }, + checkResult(html) { isnot(html.indexOf("xxx"), -1, "Should have retained the namespace style"); }, + }, + { + id: "ee", + isIFrame: true, + payload: invalidStyle6Payload, + rootElement() { return document.getElementById("ee").contentDocument.documentElement; }, + checkResult(html) { isnot(html.indexOf("xxx"), -1, "Should not have retained the namespace style"); }, + }, + { + id: "ff", + payload: invalidStyle6Payload, + rootElement() { return document.getElementById("ff"); }, + checkResult(html) { isnot(html.indexOf("xxx"), -1, "Should not have retained the namespace style"); }, + }, + { + id: "gg", + isIFrame: true, + payload: invalidStyle6Payload, + rootElement() { return document.getElementById("gg").contentDocument.documentElement; }, + checkResult(html) { isnot(html.indexOf("bar"), -1, "Should have retained the src attribute for the image"); }, + }, + { + id: "hh", + payload: invalidStyle6Payload, + rootElement() { return document.getElementById("hh"); }, + checkResult(html) { isnot(html.indexOf("bar"), -1, "Should have retained the src attribute for the image"); }, + }, + { + id: "ii", + isIFrame: true, + payload: invalidStyle6Payload, + rootElement() { return document.getElementById("ii").contentDocument.documentElement; }, + checkResult(html) { isnot(html.indexOf("bar"), -1, "Should have retained the src attribute for the image"); }, + }, + { + id: "jj", + payload: invalidStyle6Payload, + rootElement() { return document.getElementById("jj"); }, + checkResult(html) { isnot(html.indexOf("bar"), -1, "Should have retained the src attribute for the image"); }, + }, + { + id: "kk", + isIFrame: true, + payload: invalidStyle6Payload, + rootElement() { return document.getElementById("kk").contentDocument.documentElement; }, + checkResult(html) { isnot(html.indexOf("bar"), -1, "Should have retained the src attribute for the image"); }, + }, + { + id: "ll", + payload: invalidStyle6Payload, + rootElement() { return document.getElementById("ll"); }, + checkResult(html) { isnot(html.indexOf("bar"), -1, "Should have retained the src attribute for the image"); }, + }, + { + id: "mm", + isIFrame: true, + indirectPaste: true, + payload: invalidStyle7Payload, + rootElement() { return document.getElementById("mm").contentDocument.documentElement; }, + checkResult(html) { + is(html.indexOf("xxx"), -1, "Should not have retained the title text"); + isnot(html.indexOf("foo"), -1, "Should have retained the body text"); + }, + }, + { + id: "nn", + indirectPaste: true, + payload: invalidStyle7Payload, + rootElement() { return document.getElementById("nn"); }, + checkResult(html) { + is(html.indexOf("xxx"), -1, "Should not have retained the title text"); + isnot(html.indexOf("foo"), -1, "Should have retained the body text"); + }, + }, + { + id: "oo", + isIFrame: true, + payload: validDataFooPayload, + rootElement() { return document.getElementById("oo").contentDocument.documentElement; }, + checkResult(html) { isnot(html.indexOf("bar"), -1, "Should have retained the data-bar attribute"); }, + }, + { + id: "pp", + payload: validDataFooPayload, + rootElement() { return document.getElementById("pp"); }, + checkResult(html) { isnot(html.indexOf("bar"), -1, "Should have retained the data-bar attribute"); }, + }, + { + id: "qq", + isIFrame: true, + payload: validDataFoo2Payload, + rootElement() { return document.getElementById("qq").contentDocument.documentElement; }, + checkResult(html) { isnot(html.indexOf("bar"), -1, "Should have retained the _bar attribute"); }, + }, + { + id: "rr", + payload: validDataFoo2Payload, + rootElement() { return document.getElementById("rr"); }, + checkResult(html) { isnot(html.indexOf("bar"), -1, "Should have retained the _bar attribute"); }, + }, + { + id: "ss", + isIFrame: true, + payload: invalidStyle8Payload, + rootElement() { return document.getElementById("ss").contentDocument.documentElement; }, + checkResult(html) { is(html.indexOf("@-moz-document"), -1, "Should not have retained the @-moz-document rule"); }, + }, + { + id: "tt", + payload: invalidStyle8Payload, + rootElement() { return document.getElementById("tt"); }, + checkResult(html) { is(html.indexOf("@-moz-document"), -1, "Should not have retained the @-moz-document rule"); }, + }, + { + id: "uu", + isIFrame: true, + payload: invalidStyle9Payload, + rootElement() { return document.getElementById("uu").contentDocument.documentElement; }, + checkResult(html) { is(html.indexOf("@keyframes"), -1, "Should not have retained the @keyframes rule"); }, + }, + { + id: "vv", + payload: invalidStyle9Payload, + rootElement() { return document.getElementById("vv"); }, + checkResult(html) { is(html.indexOf("@keyframes"), -1, "Should not have retained the @keyframes rule"); }, + }, + { + id: "sss", + payload: svgPayload, + rootElement() { return document.getElementById("sss"); }, + checkResult(html) { isnot(html.indexOf("svgtitle"), -1, "Should have retained SVG title"); }, + }, + { + id: "ssss", + isIFrame: true, + payload: svgPayload, + rootElement() { return document.getElementById("ssss").contentDocument.documentElement; }, + checkResult(html) { isnot(html.indexOf("svgtitle"), -1, "Should have retained SVG title"); }, + }, + { + id: "ttt", + payload: svg2Payload, + rootElement() { return document.getElementById("ttt"); }, + checkResult(html) { is(html.indexOf("bogussvg"), -1, "Should have dropped bogussvg element"); }, + }, + { + id: "tttt", + isIFrame: true, + payload: svg2Payload, + rootElement() { return document.getElementById("tttt").contentDocument.documentElement; }, + checkResult(html) { is(html.indexOf("bogussvg"), -1, "Should have dropped bogussvg element"); }, + }, + { + id: "uuu", + payload: mathPayload, + rootElement() { return document.getElementById("uuu"); }, + checkResult(html) { is(html.indexOf("bogusmath"), -1, "Should have dropped bogusmath element"); }, + }, + { + id: "uuuu", + isIFrame: true, + payload: mathPayload, + rootElement() { return document.getElementById("uuuu").contentDocument.documentElement; }, + checkResult(html) { is(html.indexOf("bogusmath"), -1, "Should have dropped bogusmath element"); }, + }, + { + id: "vvv", + payload: math2Payload, + rootElement() { return document.getElementById("vvv"); }, + checkResult(html) { is(html.indexOf("yyy.css"), -1, "Should have dropped MathML style element"); }, + }, + { + id: "vvvv", + isIFrame: true, + payload: math2Payload, + rootElement() { return document.getElementById("vvvv").contentDocument.documentElement; }, + checkResult(html) { is(html.indexOf("yyy.css"), -1, "Should have dropped MathML style element"); }, + }, + { + id: "www", + payload: math3Payload, + rootElement() { return document.getElementById("www"); }, + checkResult(html) { isnot(html.indexOf("<mi"), -1, "Should not have dropped MathML mi element"); }, + }, + { + id: "wwww", + isIFrame: true, + payload: math3Payload, + rootElement() { return document.getElementById("wwww").contentDocument.documentElement; }, + checkResult(html) { isnot(html.indexOf("<mi"), -1, "Should not have dropped MathML mi element"); }, + }, + { + id: "xxx", + payload: videoPayload, + rootElement() { return document.getElementById("xxx"); }, + checkResult(html) { isnot(html.indexOf("controls="), -1, "Should have added the controls attribute"); }, + }, + { + id: "xxxx", + isIFrame: true, + payload: videoPayload, + rootElement() { return document.getElementById("xxxx").contentDocument.documentElement; }, + checkResult(html) { isnot(html.indexOf("controls="), -1, "Should have added the controls attribute"); }, + }, + { + id: "yyy", + payload: microdataPayload, + rootElement() { return document.getElementById("yyy"); }, + checkResult(html) { is(html.indexOf("name"), -1, "Should have dropped name."); is(html.indexOf("rel"), -1, "Should have dropped rel."); isnot(html.indexOf("itemprop"), -1, "Should not have dropped itemprop."); }, + }, + { + id: "yyyy", + isIFrame: true, + payload: microdataPayload, + rootElement() { return document.getElementById("yyyy").contentDocument.documentElement; }, + checkResult(html) { is(html.indexOf("name"), -1, "Should have dropped name."); is(html.indexOf("rel"), -1, "Should have dropped rel."); isnot(html.indexOf("itemprop"), -1, "Should not have dropped itemprop."); }, + }, +]; + +function doNextTest() { + /* global testCounter:true */ + if (typeof testCounter == "undefined") { + testCounter = 0; + } else if (++testCounter == tests.length) { + SimpleTest.finish(); + return; + } + + runTest(tests[testCounter]); + + doNextTest(); +} + +function getLoadContext() { + const Ci = SpecialPowers.Ci; + return SpecialPowers.wrap(window).docShell.QueryInterface(Ci.nsILoadContext); +} + +function runTest(test) { + var elem = document.getElementById(test.id); + if ("isIFrame" in test) { + elem.contentDocument.designMode = "on"; + elem.contentWindow.focus(); + } else { + elem.focus(); + } + + var trans = SpecialPowers.Cc["@mozilla.org/widget/transferable;1"] + .createInstance(SpecialPowers.Ci.nsITransferable); + trans.init(getLoadContext()); + var data = SpecialPowers.Cc["@mozilla.org/supports-string;1"] + .createInstance(SpecialPowers.Ci.nsISupportsString); + data.data = test.payload; + trans.addDataFlavor("text/html"); + trans.setTransferData("text/html", data); + + if ("indirectPaste" in test) { + var editor, win; + if ("isIFrame" in test) { + win = elem.contentDocument.defaultView; + } else { + getSelection().collapse(elem, 0); + win = window; + } + editor = SpecialPowers.wrap(win).docShell.editor; + let beforeInputEvent = null; + let inputEvent = null; + let selectionRanges = []; + win.addEventListener("beforeinput", aEvent => { + beforeInputEvent = aEvent; + selectionRanges = []; + let selection = win.getSelection(); + for (let i = 0; i < selection.rangeCount; i++) { + let range = selection.getRangeAt(i); + selectionRanges.push( + new win.StaticRange({startContainer: range.startContainer, + startOffset: range.startOffset, + endContainer: range.endContainer, + endOffset: range.endOffset})); + } + }, {once: true}); + win.addEventListener("input", aEvent => { inputEvent = aEvent; }, {once: true}); + editor.pasteTransferable(trans); + isnot(beforeInputEvent, null, '"beforeinput" event should be fired'); + if (beforeInputEvent) { + is(beforeInputEvent.cancelable, true, '"beforeinput" event for "insertFromPaste" should be cancelable'); + is(beforeInputEvent.inputType, "insertFromPaste", `inputType of "beforeinput" event should be "insertFromPaste"`); + is(beforeInputEvent.data, null, 'data of "beforeinput" event should be null'); + is(beforeInputEvent.dataTransfer.getData("text/html"), test.payload, 'dataTransfer of "beforeinput" event should have the HTML data'); + is(beforeInputEvent.dataTransfer.getData("text/plain"), "", 'dataTransfer of "beforeinput" event should not have have plain text'); + let targetRanges = beforeInputEvent.getTargetRanges(); + is(targetRanges.length, selectionRanges.length, 'getTargetRanges() of "beforeinput" event should return selection ranges'); + if (targetRanges.length === selectionRanges.length) { + for (let i = 0; i < selectionRanges.length; i++) { + is(targetRanges[i].startContainer, selectionRanges[i].startContainer, + `startContainer of getTargetRanges()[${i}] of "beforeinput" event does not match`); + is(targetRanges[i].startOffset, selectionRanges[i].startOffset, + `startOffset of getTargetRanges()[${i}] of "beforeinput" event does not match`); + is(targetRanges[i].endContainer, selectionRanges[i].endContainer, + `endContainer of getTargetRanges()[${i}] of "beforeinput" event does not match`); + is(targetRanges[i].endOffset, selectionRanges[i].endOffset, + `endOffset of getTargetRanges()[${i}] of "beforeinput" event does not match`); + } + } + } + is(inputEvent.type, "input", '"input" event should be fired'); + is(inputEvent.inputType, "insertFromPaste", `inputType of "input" event should be "insertFromPaste"`); + is(inputEvent.data, null, 'data of "input" event should be null'); + is(inputEvent.dataTransfer.getData("text/html"), test.payload, 'dataTransfer of "input" event should have the HTML data'); + is(inputEvent.dataTransfer.getData("text/plain"), "", 'dataTransfer of "input" event should not have have plain text'); + is(inputEvent.getTargetRanges().length, 0, 'getTargetRanges() of "input" event should return empty array'); + } else { + var clipboard = SpecialPowers.Services.clipboard; + + clipboard.setData(trans, null, SpecialPowers.Ci.nsIClipboard.kGlobalClipboard); + + synthesizeKey("V", {accelKey: true}); + } + + if ("checkResult" in test) { + if ("isIFrame" in test) { + test.checkResult(elem.contentDocument.documentElement.innerHTML, + elem.contentDocument.documentElement.textContent); + } else { + test.checkResult(elem.innerHTML, elem.textContent); + } + } else { + var iframes = test.rootElement().querySelectorAll("iframe"); + var expectedIFrameCount = ("iframeCount" in test) ? test.iframeCount : 1; + is(iframes.length, expectedIFrameCount, "Only " + expectedIFrameCount + " iframe should be pasted"); + if (expectedIFrameCount > 0) { + ok(!iframes[0].hasAttribute("src"), "iframe should not have a src attrib"); + } + } +} + +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(function() { + doNextTest(); +}); +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug525389.html b/editor/libeditor/tests/test_bug525389.html new file mode 100644 index 0000000000..500720d92a --- /dev/null +++ b/editor/libeditor/tests/test_bug525389.html @@ -0,0 +1,207 @@ +<!DOCTYPE HTML> +<html><head> +<title>Test for bug 525389</title> +<style src="/tests/SimpleTest/test.css" type="text/css"></style> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> + +<script class="testbody" type="application/javascript"> + + var utils = SpecialPowers.getDOMWindowUtils(window); + var Cc = SpecialPowers.Cc; + var Ci = SpecialPowers.Ci; + +function getLoadContext() { + return SpecialPowers.wrap(window).docShell.QueryInterface(Ci.nsILoadContext); +} + +async function runTest() { + var pasteCount = 0; + var pasteFunc = function(event) { pasteCount++; }; + + function verifyContent(s) { + var e = document.getElementById("i1"); + var doc = e.contentDocument; + if (navigator.platform.includes("Win")) { + // On Windows ignore \n which got left over from the removal of the fragment tags + // <html><body>\n<!--StartFragment--> and <!--EndFragment-->\n</body>\n</html>. + is(doc.body.innerHTML.replace(/\n/g, ""), s, ""); + } else { + is(doc.body.innerHTML, s, ""); + } + } + + function pasteInto(trans, html, target_id) { + var e = document.getElementById("i1"); + var doc = e.contentDocument; + doc.designMode = "on"; + doc.body.innerHTML = html; + doc.defaultView.focus(); + if (target_id) + e = doc.getElementById(target_id); + else + e = doc.body; + var selection = doc.defaultView.getSelection(); + selection.removeAllRanges(); + selection.selectAllChildren(e); + selection.collapseToEnd(); + + pasteCount = 0; + e.addEventListener("paste", pasteFunc); + utils.sendContentCommandEvent("pasteTransferable", trans); + e.removeEventListener("paste", pasteFunc); + + return e; + } + + function getTransferableFromClipboard(asHTML) { + var trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable); + trans.init(getLoadContext()); + if (asHTML) { + trans.addDataFlavor("text/html"); + } else { + trans.addDataFlavor("text/plain"); + } + var clip = SpecialPowers.Services.clipboard; + clip.getData(trans, Ci.nsIClipboard.kGlobalClipboard, SpecialPowers.wrap(window).browsingContext.currentWindowContext); + return trans; + } + + // Commented out as the test for it below is also commented out. + // function makeTransferable(s, asHTML, target_id) { + // var e = document.getElementById("i2"); + // var doc = e.contentDocument; + // if (asHTML) { + // doc.body.innerHTML = s; + // } else { + // var text = doc.createTextNode(s); + // doc.body.appendChild(text); + // } + // doc.designMode = "on"; + // doc.defaultView.focus(); + // var selection = doc.defaultView.getSelection(); + // selection.removeAllRanges(); + // if (!target_id) { + // selection.selectAllChildren(doc.body); + // } else { + // var range = document.createRange(); + // range.selectNode(doc.getElementById(target_id)); + // selection.addRange(range); + // } + // + // // We cannot use plain strings, we have to use nsSupportsString. + // var supportsStringClass = SpecialPowers.Components.classes["@mozilla.org/supports-string;1"]; + // var ssData = supportsStringClass.createInstance(Ci.nsISupportsString); + // + // // Create the transferable. + // var trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable); + // trans.init(getLoadContext()); + // + // // Add the data to the transferable. + // if (asHTML) { + // trans.addDataFlavor("text/html"); + // ssData.data = doc.body.innerHTML; + // trans.setTransferData("text/html", ssData); + // } else { + // trans.addDataFlavor("text/plain"); + // ssData.data = doc.body.innerHTML; + // trans.setTransferData("text/plain", ssData); + // } + // + // return trans; + // } + + async function copyToClipBoard(s, asHTML, target_id) { + var e = document.getElementById("i2"); + var doc = e.contentDocument; + if (asHTML) { + doc.body.innerHTML = s; + } else { + var text = doc.createTextNode(s); + doc.body.appendChild(text); + } + doc.designMode = "on"; + doc.defaultView.focus(); + var selection = doc.defaultView.getSelection(); + selection.removeAllRanges(); + if (!target_id) { + selection.selectAllChildren(doc.body); + } else { + var range = document.createRange(); + range.selectNode(doc.getElementById(target_id)); + selection.addRange(range); + } + + await SimpleTest.promiseClipboardChange(() => true, + () => { SpecialPowers.wrap(doc).execCommand("copy", false, null); }); + + return e; + } + + await copyToClipBoard("<span>Hello</span><span>Kitty</span>", true); + var trans = getTransferableFromClipboard(true); + pasteInto(trans, ""); + verifyContent("<span>Hello</span><span>Kitty</span>"); + is(pasteCount, 1, "paste event was not triggered"); + + // this test is not working out exactly like the clipboard test + // has to do with generating the nsITransferable above + // trans = makeTransferable('<span>Hello</span><span>Kitty</span>', true); + // pasteInto(trans, ''); + // verifyContent('<span>Hello</span><span>Kitty</span>'); + + await copyToClipBoard("<dl><dd>Hello Kitty</dd></dl><span>Hello</span><span>Kitty</span>", true); + trans = getTransferableFromClipboard(true); + pasteInto(trans, '<ol><li id="paste_here">X</li></ol>', "paste_here"); + verifyContent('<ol><li id="paste_here">X<dl><dd>Hello Kitty</dd></dl><span>Hello</span><span>Kitty</span></li></ol>'); + is(pasteCount, 1, "paste event was not triggered"); + +// The following test doesn't do what I expected, because the special handling +// of IsList nodes in nsHTMLEditor::InsertHTMLWithContext simply removes +// non-list/item children. See bug 481177. +// await copyToClipBoard("<ol><li>Hello Kitty</li><span>Hello</span></ol>", true); +// pasteInto('<ol><li id="paste_here">X</li></ol>',"paste_here"); +// verifyContent('<ol><li id="paste_here">X</li><li>Hello Kitty</li><span>Hello</span></ol>'); + + await copyToClipBoard("<pre>Kitty</pre><span>Hello</span>", true); + trans = getTransferableFromClipboard(true); + pasteInto(trans, '<pre id="paste_here">Hello </pre>', "paste_here"); + verifyContent('<pre id="paste_here">Hello Kitty<span>Hello</span></pre>'); + is(pasteCount, 1, "paste event was not triggered"); + + await copyToClipBoard('1<span style="display: contents">2</span>3', true); + trans = getTransferableFromClipboard(true); + pasteInto(trans, '<div id="paste_here"></div>', "paste_here"); + verifyContent('<div id="paste_here">1<span style="display: contents">2</span>3</div>'); + is(pasteCount, 1, "paste event was not triggered"); + + // test that we can preventDefault pastes + pasteFunc = function(event) { event.preventDefault(); return false; }; + await copyToClipBoard("<pre>Kitty</pre><span>Hello</span>", true); + trans = getTransferableFromClipboard(true); + pasteInto(trans, '<pre id="paste_here">Hello </pre>', "paste_here"); + verifyContent('<pre id="paste_here">Hello </pre>'); + is(pasteCount, 0, "paste event was triggered"); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(() => { + add_task(async function test_copy() { + await runTest(); + }); +}); + +</script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=525389">Mozilla Bug 525389</a> +<p id="display"></p> + +<pre id="test"> +</pre> + +<iframe id="i1" width="200" height="100" src="about:blank"></iframe><br> +<iframe id="i2" width="200" height="100" src="about:blank"></iframe><br> + +</body> +</html> diff --git a/editor/libeditor/tests/test_bug537046.html b/editor/libeditor/tests/test_bug537046.html new file mode 100644 index 0000000000..746aba6edc --- /dev/null +++ b/editor/libeditor/tests/test_bug537046.html @@ -0,0 +1,49 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=537046 +--> +<head> + <title>Test for Bug 537046</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=537046">Mozilla Bug 537046</a> +<p id="display"></p> +<div id="content"> + <div id="editor" contenteditable="true"> + Some editable content + </div> + <div id="source" contenteditable="true"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 537046 **/ + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var ed = document.getElementById("editor"); + var src = document.getElementById("source"); + ed.addEventListener("DOMSubtreeModified", function() { + src.textContent = ed.innerHTML; + }); + src.addEventListener("DOMSubtreeModified", function() { + ed.innerHTML = ed.textContent; + }); + + // Simulate pressing Enter twice + ed.focus(); + synthesizeKey("KEY_Enter"); + synthesizeKey("KEY_Enter"); + + ok(true, "Didn't crash!"); + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug549262.html b/editor/libeditor/tests/test_bug549262.html new file mode 100644 index 0000000000..a6848e3a66 --- /dev/null +++ b/editor/libeditor/tests/test_bug549262.html @@ -0,0 +1,160 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=549262 +--> +<head> + <title>Test for Bug 549262</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="/tests/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=549262">Mozilla Bug 549262</a> +<p id="display"></p> +<div id="content"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 549262 **/ + +var smoothScrollPref = "general.smoothScroll"; +SimpleTest.waitForExplicitFinish(); +var win = window.open("file_bug549262.html", "_blank", + "width=600,height=600,scrollbars=yes"); + +function waitForScrollEvent(aWindow) { + return new Promise(resolve => { + aWindow.addEventListener("scroll", () => { SimpleTest.executeSoon(resolve); }, {once: true, capture: true}); + }); +} + +function waitAndCheckNoScrollEvent(aWindow) { + let gotScroll = false; + function recordScroll() { + gotScroll = true; + } + aWindow.addEventListener("scroll", recordScroll, {capture: true}); + return waitToClearOutAnyPotentialScrolls(aWindow).then(function() { + aWindow.removeEventListener("scroll", recordScroll, {capture: true}); + is(gotScroll, false, "check that we didn't get a scroll"); + }); +} + +SimpleTest.waitForFocus(function() { + SpecialPowers.pushPrefEnv({"set": [[smoothScrollPref, false]]}, startTest); +}, win); +async function startTest() { + // Make sure that pressing Space when a contenteditable element is not focused + // will scroll the page. + var ed = win.document.getElementById("editor"); + var sc = win.document.querySelector("a"); + sc.focus(); + await waitToClearOutAnyPotentialScrolls(win); + is(win.scrollY, 0, "Sanity check"); + let waitForScrolling = waitForScrollEvent(win); + synthesizeKey(" ", {}, win); + + await waitForScrolling; + + isnot(win.scrollY, 0, "Page is scrolled down"); + is(ed.textContent, "abc", "The content of the editable element has not changed"); + var oldY = win.scrollY; + waitForScrolling = waitForScrollEvent(win); + synthesizeKey(" ", {shiftKey: true}, win); + + await waitForScrolling; + + ok(win.scrollY < oldY, "Page is scrolled up"); + is(ed.textContent, "abc", "The content of the editable element has not changed"); + + // Make sure that pressing Space when a contenteditable element is focused + // will not scroll the page, and will edit the element. + ed.focus(); + win.getSelection().collapse(ed.firstChild, 1); + await waitToClearOutAnyPotentialScrolls(win); + oldY = win.scrollY; + let waitForNoScroll = waitAndCheckNoScrollEvent(win); + synthesizeKey(" ", {}, win); + + await waitForNoScroll; + + ok(win.scrollY <= oldY, "Page is not scrolled down"); + is(ed.textContent, "a bc", "The content of the editable element has changed"); + sc.focus(); + await waitToClearOutAnyPotentialScrolls(win); + waitForScrolling = waitForScrollEvent(win); + synthesizeKey(" ", {}, win); + + await waitForScrolling; + + isnot(win.scrollY, 0, "Page is scrolled down"); + is(ed.textContent, "a bc", "The content of the editable element has not changed"); + ed.focus(); + win.getSelection().collapse(ed.firstChild, 3); + await waitToClearOutAnyPotentialScrolls(win); + waitForNoScroll = waitAndCheckNoScrollEvent(win); + synthesizeKey(" ", {shiftKey: true}, win); + + await waitForNoScroll; + + isnot(win.scrollY, 0, "Page is not scrolled up"); + is(ed.textContent, "a b c", "The content of the editable element has changed"); + + // Now let's test the down/up keys + sc = document.body; + + ed.blur(); + sc.focus(); + await waitToClearOutAnyPotentialScrolls(win); + oldY = win.scrollY; + waitForScrolling = waitForScrollEvent(win); + synthesizeKey("VK_UP", {}, win); + + await waitForScrolling; + + ok(win.scrollY < oldY, "Page is scrolled up"); + oldY = win.scrollY; + ed.focus(); + win.getSelection().collapse(ed.firstChild, 3); + await waitToClearOutAnyPotentialScrolls(win); + waitForNoScroll = waitAndCheckNoScrollEvent(win); + synthesizeKey("VK_UP", {}, win); + + await waitForNoScroll; + + is(win.scrollY, oldY, "Page is not scrolled up"); + is(win.getSelection().focusNode, ed.firstChild, "Correct element selected"); + is(win.getSelection().focusOffset, 0, "Selection should be moved to the beginning"); + win.getSelection().removeAllRanges(); + await waitToClearOutAnyPotentialScrolls(win); + waitForScrolling = waitForScrollEvent(win); + synthesizeMouse(sc, 300, 300, {}, win); + synthesizeKey("VK_DOWN", {}, win); + + await waitForScrolling; + + ok(win.scrollY > oldY, "Page is scrolled down"); + ed.focus(); + win.getSelection().collapse(ed.firstChild, 3); + await waitToClearOutAnyPotentialScrolls(win); + oldY = win.scrollY; + waitForNoScroll = waitAndCheckNoScrollEvent(win); + synthesizeKey("VK_DOWN", {}, win); + + await waitForNoScroll; + + is(win.scrollY, oldY, "Page is not scrolled down"); + is(win.getSelection().focusNode, ed.firstChild, "Correct element selected"); + is(win.getSelection().focusOffset, ed.textContent.length, "Selection should be moved to the end"); + + win.close(); + + SimpleTest.finish(); +} +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug550434.html b/editor/libeditor/tests/test_bug550434.html new file mode 100644 index 0000000000..2018701f6d --- /dev/null +++ b/editor/libeditor/tests/test_bug550434.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=550434 +--> +<head> + <title>Test for Bug 550434</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=550434">Mozilla Bug 550434</a> +<p id="display"></p> +<div id="content"> + <div id="editor" contenteditable="true" + style="height: 250px; height: 200px; border: 4px solid red; outline: none;"></div> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 550434 **/ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var ed = document.getElementById("editor"); + + // Simulate click twice + synthesizeMouse(ed, 10, 10, {}); + synthesizeMouse(ed, 50, 50, {}); + setTimeout(function() { + sendString("x"); + + is(ed.innerHTML, "x", "Editor should work after being clicked twice"); + SimpleTest.finish(); + }, 0); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug551704.html b/editor/libeditor/tests/test_bug551704.html new file mode 100644 index 0000000000..ad63904f66 --- /dev/null +++ b/editor/libeditor/tests/test_bug551704.html @@ -0,0 +1,126 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=551704 +--> +<head> + <title>Test for Bug 551704</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=551704">Mozilla Bug 551704</a> +<p id="display"></p> +<div id="content"> + <div id="preformatted" style="white-space: pre" contenteditable>a b</div> + <div id="test1" contenteditable><br></div> + <div id="test2" contenteditable>a<br></div> + <div id="test3" contenteditable style="white-space: pre"><br></div> + <div id="test4" contenteditable style="white-space: pre">a<br></div> + <div id="test5" contenteditable></div> + <div id="test6" contenteditable>a</div> + <div id="test7" contenteditable style="white-space: pre"></div> + <div id="test8" contenteditable style="white-space: pre">a</div> +</div> +<pre id="test"> +<script type="application/javascript"> + +function testLineBreak(div, type, expectedText, expectedHTML, callback) { + div.focus(); + getSelection().collapse(div, 0); + type(); + is(div.innerHTML, expectedHTML, "The expected HTML after editing should be correct"); + requestAnimationFrame(function() { + SimpleTest.waitForClipboard(expectedText, + function() { + getSelection().selectAllChildren(div); + synthesizeKey("C", {accelKey: true}); + }, + function() { + var t = document.createElement("textarea"); + document.body.appendChild(t); + t.focus(); + synthesizeKey("V", {accelKey: true}); + is(t.value, expectedText, "The expected text should be copied to the clipboard"); + callback(); + }, + function() { + SimpleTest.finish(); + } + ); + }); +} + +function typeABCDEF() { + sendString("a"); + typeBCDEF_chars(); +} + +function typeBCDEF() { + synthesizeKey("KEY_ArrowRight"); + typeBCDEF_chars(); +} + +function typeBCDEF_chars() { + sendString("bc"); + synthesizeKey("KEY_Enter"); + sendString("def"); +} + +/** Test for Bug 551704 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + document.execCommand("defaultParagraphSeparator", false, "div"); + + var preformatted = document.getElementById("preformatted"); + is(preformatted.innerHTML, "a\nb", "No BR node should be injected for preformatted editable fields"); + + var iframe = document.createElement("iframe"); + iframe.addEventListener("load", function() { + var sel = iframe.contentWindow.getSelection(); + is(sel.rangeCount, 0, "There should be no range in the selection initially"); + iframe.contentDocument.designMode = "on"; + sel = iframe.contentWindow.getSelection(); + is(sel.rangeCount, 1, "There should be a single range in the selection after setting designMode"); + var range = sel.getRangeAt(0); + ok(range.collapsed, "The range should be collapsed"); + is(range.startContainer, iframe.contentDocument.body.firstChild, "The range should start on the text"); + is(range.startOffset, 0, "The start offset should be zero"); + + continueTest(); + }); + iframe.srcdoc = "foo"; + document.getElementById("content").appendChild(iframe); +}); + +function continueTest() { + var divs = []; + for (var i = 0; i < 8; ++i) { + divs[i] = document.getElementById("test" + (i + 1)); + } + var current = 0; + function doNextTest() { + if (current == divs.length) { + SimpleTest.finish(); + return; + } + var div = divs[current++]; + let type; + if (div.textContent == "a") { + type = typeBCDEF; + } else { + type = typeABCDEF; + } + var expectedHTML = "<div>abc</div><div>def<br></div>"; + var expectedText = "abc\ndef"; + testLineBreak(div, type, expectedText, expectedHTML, doNextTest); + } + + doNextTest(); +} + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug552782.html b/editor/libeditor/tests/test_bug552782.html new file mode 100644 index 0000000000..66a70cae72 --- /dev/null +++ b/editor/libeditor/tests/test_bug552782.html @@ -0,0 +1,46 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=552782 +--> +<head> + <title>Test for Bug 552782</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=290026">Mozilla Bug 552782</a> +<p id="display"></p> +<div id="editor" contenteditable></div> + +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 552782 **/ +SimpleTest.waitForExplicitFinish(); + +var original = "<ol><li>Item 1</li><ol><li>Item 2</li><li>Item 3</li><li>Item 4</li></ol></ol>"; +var editor = document.getElementById("editor"); +editor.innerHTML = original; +editor.focus(); + +addLoadEvent(function() { + var sel = window.getSelection(); + sel.removeAllRanges(); + var lis = document.getElementsByTagName("li"); + sel.selectAllChildren(lis[2]); + document.execCommand("outdent", false, false); + var expected = "<ol><li>Item 1</li><ol><li>Item 2</li></ol><li>Item 3</li><ol><li>Item 4</li></ol></ol>"; + is(editor.innerHTML, expected, "outdenting third item in a partially indented numbered list"); + document.execCommand("indent", false, false); + todo_is(editor.innerHTML, original, "re-indenting third item in a partially indented numbered list"); + + // done + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug567213.html b/editor/libeditor/tests/test_bug567213.html new file mode 100644 index 0000000000..cfad69dff7 --- /dev/null +++ b/editor/libeditor/tests/test_bug567213.html @@ -0,0 +1,58 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=567213 +--> + +<head> + <title>Test for Bug 567213</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> + +<body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=567213">Mozilla Bug 567213</a> + <p id="display"></p> + <div id="content"> + <div id="target" contenteditable="true">test</div> + <button id="thief">theif</button> + </div> + + <pre id="test"> + <script type="application/javascript"> + + /** Test for Bug 567213 **/ + + SimpleTest.waitForExplicitFinish(); + + addLoadEvent(function() { + var target = document.getElementById("target"); + var thief = document.getElementById("thief"); + var sel = window.getSelection(); + + // select the contents of the editable area + sel.removeAllRanges(); + sel.selectAllChildren(target); + target.focus(); + + // press some key + sendString("X"); + is(target.textContent, "X", "Text input should work (sanity check)"); + + // select the contents of the editable area again + sel.removeAllRanges(); + sel.selectAllChildren(target); + thief.focus(); + + // press some key with the thief having focus + sendString("Y"); + is(target.textContent, "X", "Text entry should not work with another element focused"); + + SimpleTest.finish(); + }); + + </script> + </pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug569988.html b/editor/libeditor/tests/test_bug569988.html new file mode 100644 index 0000000000..c4d4b040ba --- /dev/null +++ b/editor/libeditor/tests/test_bug569988.html @@ -0,0 +1,103 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=569988 +--> +<head> + <title>Test for Bug 569988</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=569988">Mozilla Bug 569988</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 569988 **/ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTest); + + +function runTest() { + var script = SpecialPowers.loadChromeScript(function() { + /* eslint-env mozilla/chrome-script */ + var gPromptInput = null; + var os = Services.obs; + + os.addObserver(onPromptLoad, "common-dialog-loaded"); + os.addObserver(onPromptLoad, "tabmodal-dialog-loaded"); + + function onPromptLoad(subject, topic, data) { + let ui = subject.Dialog ? subject.Dialog.ui : undefined; + if (!ui) { + // subject is an tab prompt, find the elements ourselves + ui = { + loginTextbox: subject.querySelector(".tabmodalprompt-loginTextbox"), + button0: subject.querySelector(".tabmodalprompt-button0"), + }; + } + sendAsyncMessage("ok", [true, "onPromptLoad is called"]); + gPromptInput = ui.loginTextbox; + gPromptInput.addEventListener("focus", onPromptFocus); + // shift focus to ensure it fires. + ui.button0.focus(); + gPromptInput.focus(); + } + + function onPromptFocus() { + sendAsyncMessage("ok", [true, "onPromptFocus is called"]); + gPromptInput.removeEventListener("focus", onPromptFocus); + + var listenerService = Services.els; + + var listener = { + handleEvent: function _hv(aEvent) { + var isPrevented = aEvent.defaultPrevented; + sendAsyncMessage("ok", [!isPrevented, + "ESC key event is prevented by editor"]); + listenerService.removeSystemEventListener(gPromptInput, "keypress", + listener, false); + }, + }; + listenerService.addSystemEventListener(gPromptInput, "keypress", + listener, false); + + sendAsyncMessage("info", "sending key"); + var EventUtils = {}; + EventUtils.window = {}; + EventUtils._EU_Ci = Ci; + EventUtils._EU_Cc = Cc; + Services.scriptloader + .loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js", + EventUtils); + EventUtils.synthesizeKey("VK_ESCAPE", {}, + gPromptInput.ownerGlobal); + } + + addMessageListener("destroy", function() { + os.removeObserver(onPromptLoad, "tabmodal-dialog-loaded"); + os.removeObserver(onPromptLoad, "common-dialog-loaded"); + }); + }); + script.addMessageListener("ok", ([val, msg]) => ok(val, msg)); + script.addMessageListener("info", msg => info(msg)); + + info("opening prompt..."); + prompt("summary", "text"); + info("prompt is closed"); + + script.sendAsyncMessage("destroy"); + + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug570144.html b/editor/libeditor/tests/test_bug570144.html new file mode 100644 index 0000000000..9ea4cd1d6e --- /dev/null +++ b/editor/libeditor/tests/test_bug570144.html @@ -0,0 +1,123 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=570144 +--> +<head> + <title>Test for Bug 570144</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=570144">Mozilla Bug 570144</a> +<p id="display"></p> +<div id="content"> + <!-- editable paragraphs in list item --> + <section id="test1"> + <ol> + <li><p contenteditable>foo</p></li> + </ol> + <ul> + <li><p contenteditable>foo</p></li> + </ul> + <dl> + <dt>foo</dt> + <dd><p contenteditable>bar</p></dd> + </dl> + </section> + <!-- paragraphs in editable list item --> + <section id="test2"> + <ol> + <li contenteditable><p>foo</p></li> + </ol> + <ul> + <li contenteditable><p>foo</p></li> + </ul> + <dl> + <dt>foo</dt> + <dd contenteditable><p>bar</p></dd> + </dl> + </section> + <!-- paragraphs in editable list --> + <section id="test3"> + <ol contenteditable> + <li><p>foo</p></li> + </ol> + <ul contenteditable> + <li><p>foo</p></li> + </ul> + <dl contenteditable> + <dt>foo</dt> + <dd><p>bar</p></dd> + </dl> + </section> +</div> + +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 570144 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + +function try2split(list) { + var editor = list.hasAttribute("contenteditable") + ? list : list.querySelector("*[contenteditable]"); + editor.focus(); + // put the caret at the end of the paragraph + var selection = window.getSelection(); + if (editor.nodeName.toLowerCase() == "p") + selection.selectAllChildren(editor); + else + selection.selectAllChildren(editor.querySelector("p")); + selection.collapseToEnd(); + // simulate a [Enter] keypress + synthesizeKey("KEY_Enter"); +} + +function testSection(element, context, shouldCreateLI, shouldCreateP) { + var nbLI = shouldCreateLI ? 2 : 1; // number of expected list items + var nbP = shouldCreateP ? 2 : 1; // number of expected paragraphs + + function message(nodeName, dup) { + return context + ":[Return] should " + (dup ? "" : "not ") + + "create another <" + nodeName + ">."; + } + var msgP = message("p", shouldCreateP); + var msgLI = message("li", shouldCreateLI); + var msgDT = message("dt", shouldCreateLI); + var msgDD = message("dd", false); + + const ol = element.querySelector("ol"); + try2split(ol); + is(ol.querySelectorAll("li").length, nbLI, msgLI); + is(ol.querySelectorAll("p").length, nbP, msgP); + + const ul = element.querySelector("ul"); + try2split(ul); + is(ul.querySelectorAll("li").length, nbLI, msgLI); + is(ul.querySelectorAll("p").length, nbP, msgP); + + const dl = element.querySelector("dl"); + try2split(dl); + is(dl.querySelectorAll("dt").length, nbLI, msgDT); + is(dl.querySelectorAll("dd").length, 1, msgDD); + is(dl.querySelectorAll("p").length, nbP, msgP); +} + +function runTests() { + testSection(document.getElementById("test1"), "editable paragraph in list item", false, false); + testSection(document.getElementById("test2"), "paragraph in editable list item", false, true); + testSection(document.getElementById("test3"), "paragraph in editable list", true, false); + /* Note: concerning #test3, it would be preferrable that [Return] creates + * another paragraph in another list item (i.e. last argument = 'true'). + * Currently it just creates an empty list item, which is acceptable. + */ + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug578771.html b/editor/libeditor/tests/test_bug578771.html new file mode 100644 index 0000000000..5cf7b23e52 --- /dev/null +++ b/editor/libeditor/tests/test_bug578771.html @@ -0,0 +1,63 @@ +<!DOCTYPE HTML> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this file, + - You can obtain one at http://mozilla.org/MPL/2.0/. --> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=578771 +--> + +<head> + <title>Test for Bug 578771</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> + +<body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=578771">Mozilla Bug 578771</a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + + <pre id="test"> + <script type="application/javascript"> + + /** Test for Bug 578771 **/ + SimpleTest.waitForExplicitFinish(); + + function testElem(elem, elemTag) { + var ce = document.getElementById("ce"); + ce.focus(); + + synthesizeMouse(elem, 5, 5, {clickCount: 2 }); + ok(elem.selectionStart == 0 && elem.selectionEnd == 7, + " Double-clicking on another " + elemTag + " works correctly"); + + ce.focus(); + synthesizeMouse(elem, 5, 5, {clickCount: 3 }); + ok(elem.selectionStart == 0 && elem.selectionEnd == 14, + "Triple-clicking on another " + elemTag + " works correctly"); + } + // Avoid platform selection differences + SimpleTest.waitForFocus(function() { + SpecialPowers.pushPrefEnv({"set": [["layout.word_select.eat_space_to_next_word", false]]}, startTest); + }); + + function startTest() { + var input = document.getElementById("ip"); + testElem(input, "input"); + + var textarea = document.getElementById("ta"); + testElem(textarea, "textarea"); + + SimpleTest.finish(); + } + </script> + </pre> + + <input id="ip" type="text" value="Mozilla editor" /> + <textarea id="ta">Mozilla editor</textarea> + <div id="ce" contenteditable="true">Contenteditable div that could interfere with focus</div> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug586662.html b/editor/libeditor/tests/test_bug586662.html new file mode 100644 index 0000000000..c7280b4ac2 --- /dev/null +++ b/editor/libeditor/tests/test_bug586662.html @@ -0,0 +1,62 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=586662 +--> + +<head> + <title>Test for Bug 586662</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> + +<body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=586662">Mozilla Bug 586662</a> + <p id="display"><textarea onkeypress="this.style.overflow = 'hidden'"></textarea></p> + <div id="content" style="display: none"> + </div> + + <pre id="test"> + <script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var t = document.querySelector("textarea"); + t.focus(); + sendString("a"); + is(getComputedStyle(t, null).overflow, "hidden", "The event handler should be executed"); + is(t.value, "a", "The key entry should result in a character being added to the field"); + + var win = window.open("file_bug586662.html", "_blank", + "width=600,height=600,scrollbars=yes"); + SimpleTest.waitForFocus(function() { + // Make sure that focusing the textarea will cause the page to scroll + var ed = win.document.getElementById("editor"); + ed.focus(); + setTimeout(function() { + isnot(win.scrollY, 0, "Page is scrolled down"); + // Scroll back up + win.scrollTo(0, 0); + setTimeout(function() { + is(win.scrollY, 0, "Page is scrolled back up"); + // Make sure that typing something into the textarea will cause the + // page to scroll down + synthesizeKey("a", {}, win); + requestAnimationFrame(function() { + requestAnimationFrame(function() { + isnot(win.scrollY, 0, "Page is scrolled down again"); + + win.close(); + SimpleTest.finish(); + }); + }); + }, 0); + }, 0); + }, win); +}); + + </script> + </pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug590554.html b/editor/libeditor/tests/test_bug590554.html new file mode 100644 index 0000000000..ea48b4c08f --- /dev/null +++ b/editor/libeditor/tests/test_bug590554.html @@ -0,0 +1,36 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=590554 +--> + +<head> + <title>Test for Bug 590554</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> + +<body> + + <script type="application/javascript"> + + /** Test for Bug 590554 **/ + + SimpleTest.waitForExplicitFinish(); + + SimpleTest.waitForFocus(function() { + var t = document.querySelector("textarea"); + t.focus(); + synthesizeKey("KEY_Enter"); + is(t.value, "\n", "Pressing enter should work the first time"); + synthesizeKey("KEY_Enter"); + is(t.value, "\n", "Pressing enter should not work the second time"); + SimpleTest.finish(); + }); + + </script> + + <textarea maxlength="1"></textarea> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug592592.html b/editor/libeditor/tests/test_bug592592.html new file mode 100644 index 0000000000..c1a74e9893 --- /dev/null +++ b/editor/libeditor/tests/test_bug592592.html @@ -0,0 +1,72 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=592592 +--> +<head> + <title>Test for Bug 592592</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=592592">Mozilla Bug 592592</a> +<p id="display"></p> +<div id="content"> + <div id="editor" contenteditable="true" style="white-space:pre-wrap">a b</div> + <div id="editor2" contenteditable="true" style="white-space:pre-wrap">a b</div> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 592592 **/ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var ed = document.getElementById("editor"); + + // Put the selection right after "a" + ed.focus(); + window.getSelection().collapse(ed.firstChild, 1); + + // Press space + sendString(" "); + + // Make sure we haven't added an nbsp + is(ed.innerHTML, "a b", "We should not be adding an for preformatted text"); + + // Remove the preformatted style + ed.removeAttribute("style"); + + // Reset the DOM + ed.innerHTML = "a b"; + + // Reset the selection + ed.focus(); + window.getSelection().collapse(ed.firstChild, 1); + + // Press space + sendString(" "); + + // Make sure that we have added an nbsp + is(ed.innerHTML, "a b", "We should add an for non-preformatted text"); + + ed = document.getElementById("editor2"); + + // Put the selection after the second space in the second editable field + ed.focus(); + window.getSelection().collapse(ed.firstChild, 3); + + // Press the back-space key + synthesizeKey("KEY_Backspace"); + + // Make sure that we've only deleted a single space + is(ed.innerHTML, "a b", "We should only be deleting a single space"); + + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug596001.html b/editor/libeditor/tests/test_bug596001.html new file mode 100644 index 0000000000..451b1f9c28 --- /dev/null +++ b/editor/libeditor/tests/test_bug596001.html @@ -0,0 +1,57 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=596001 +--> +<head> + <title>Test for Bug 596001</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=596001">Mozilla Bug 596001</a> +<p id="display"></p> +<div id="content"> +<textarea id="src">a	b</textarea> +<textarea id="dst"></textarea> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 596001 **/ + +function testTab(prefix, callback) { + var src = document.getElementById("src"); + var dst = document.getElementById("dst"); + dst.value = prefix; + src.focus(); + src.select(); + SimpleTest.waitForClipboard("a\tb", + function() { + synthesizeKey("c", {accelKey: true}); + }, + function() { + dst.focus(); + var inputReceived = false; + dst.addEventListener("input", function() { inputReceived = true; }); + synthesizeKey("v", {accelKey: true}); + ok(inputReceived, "An input event should be raised"); + is(dst.value, prefix + src.value, "The value should be pasted verbatim"); + callback(); + }, + callback + ); +} + +SimpleTest.waitForExplicitFinish(); +testTab("", function() { + testTab("foo", function() { + SimpleTest.finish(); + }); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug596506.html b/editor/libeditor/tests/test_bug596506.html new file mode 100644 index 0000000000..3ecf5e2363 --- /dev/null +++ b/editor/libeditor/tests/test_bug596506.html @@ -0,0 +1,55 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=596506 +--> +<head> + <title>Test for Bug 596506</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=596506">Mozilla Bug 596506</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 596506 **/ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTest); + +const kIsMac = navigator.platform.includes("Mac"); + + +function runTest() { + var edit = document.getElementById("edit"); + edit.focus(); + + sendString("First"); + synthesizeKey("KEY_Enter"); + sendString("Second"); + synthesizeKey("KEY_ArrowUp"); + synthesizeKey("KEY_ArrowUp"); + if (kIsMac) { + synthesizeKey("KEY_ArrowRight", { accelKey: true }); + } else { + synthesizeKey("KEY_End"); + } + sendString("ly"); + is(edit.value, "Firstly\nSecond", + "Pressing end should position the cursor before the terminating newline"); + SimpleTest.finish(); +} + +</script> +</pre> + +<textarea id="edit"></textarea> + +</body> +</html> diff --git a/editor/libeditor/tests/test_bug597331.html b/editor/libeditor/tests/test_bug597331.html new file mode 100644 index 0000000000..d165ff78a5 --- /dev/null +++ b/editor/libeditor/tests/test_bug597331.html @@ -0,0 +1,73 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=597331 +--> +<head> + <title>Test for Bug 597331</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/WindowSnapshot.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style> + textarea { border-color: white; } + </style> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=597331">Mozilla Bug 597331</a> +<p id="display"></p> +<div id="content"> +<textarea>line1 +line2 +line3 +</textarea> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 597331 **/ + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + SimpleTest.executeSoon(function() { + var t = document.querySelector("textarea"); + t.focus(); + t.selectionStart = 4; + t.selectionEnd = 4; + SimpleTest.executeSoon(function() { + t.getBoundingClientRect(); // flush layout + var before = snapshotWindow(window, true); + t.selectionStart = 5; + t.selectionEnd = 5; + t.addEventListener("keydown", function() { + SimpleTest.executeSoon(function() { + t.style.display = "block"; + document.body.offsetWidth; + t.style.display = ""; + document.body.offsetWidth; + + is(t.selectionStart, 4, "Cursor should be moved correctly"); + is(t.selectionEnd, 4, "Cursor should be moved correctly"); + + var after = snapshotWindow(window, true); + + var result = compareSnapshots(before, after, true); + var msg = "The caret should be displayed correctly after reframing"; + if (!result[0]) { + msg += "\nRESULT:\n" + result[2]; + msg += "\nREFERENCE:\n" + result[1]; + } + ok(result[0], msg); + + SimpleTest.finish(); + }); + }, {once: true}); + synthesizeKey("KEY_ArrowLeft"); + }); + }); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug597784.html b/editor/libeditor/tests/test_bug597784.html new file mode 100644 index 0000000000..ef0348a0a4 --- /dev/null +++ b/editor/libeditor/tests/test_bug597784.html @@ -0,0 +1,37 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=597784 +--> +<head> + <title>Test for Bug 597784</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=597784">Mozilla Bug 597784</a> +<p id="display"></p> +<div id="content"></div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 597784 **/ + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + document.designMode = "on"; + var content = document.getElementById("content"); + getSelection().collapse(content, 0); + var html = "<test:tag>test:tag</test:tag>" + + "<a href=\"http://mozilla.org/\" test:attr=\"test:attr\" custom=\"value\">link</a>"; + document.execCommand("insertHTML", false, html); + is(content.innerHTML, html, + "The custom tags and attributes should be inserted into the document using the insertHTML command"); + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug599322.html b/editor/libeditor/tests/test_bug599322.html new file mode 100644 index 0000000000..57c15cf4c7 --- /dev/null +++ b/editor/libeditor/tests/test_bug599322.html @@ -0,0 +1,58 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=599322.patch +--> +<head> + <title>Test for Bug 599322.patch</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=599322.patch">Mozilla Bug 599322.patch</a> +<p id="display"></p> +<div id="content"> +<div id="src">src<img src="/tests/editor/libeditor/tests/green.png"></div> +<iframe id="dst" src="javascript:;"></iframe> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 599322.patch **/ + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var src = document.getElementById("src"); + var dst = document.getElementById("dst"); + var doc = dst.contentDocument; + doc.open(); + doc.write("<html><head><base href='http://mochi.test:8888/'></head><body></body></html>"); + doc.close(); + SimpleTest.waitForFocus(function() { + getSelection().selectAllChildren(src); + SimpleTest.waitForClipboard("src", + function() { + synthesizeKey("c", {accelKey: true}); + }, + function() { + dst.contentDocument.designMode = "on"; + dst.focus(); + dst.contentDocument.body.focus(); + synthesizeKey("v", {accelKey: true}); + is(dst.contentDocument.querySelector("img").src, + document.querySelector("img").src, + "The source should be correctly set based on the base URI"); + SimpleTest.finish(); + }, + function() { + SimpleTest.finish(); + } + ); + }); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug599983.html b/editor/libeditor/tests/test_bug599983.html new file mode 100644 index 0000000000..08fc9a228a --- /dev/null +++ b/editor/libeditor/tests/test_bug599983.html @@ -0,0 +1,16 @@ +<!doctype html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=599983 +--> +<title>Test for Bug 599983</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=599983">Mozilla Bug 599983</a> +<div contenteditable>foo</div> +<script> +getSelection().selectAllChildren(document.querySelector("div")); +document.execCommand("bold"); +is(document.querySelector("[_moz_dirty]"), null, + "No _moz_dirty allowed in webpages"); +</script> diff --git a/editor/libeditor/tests/test_bug599983.xhtml b/editor/libeditor/tests/test_bug599983.xhtml new file mode 100644 index 0000000000..6e943929d4 --- /dev/null +++ b/editor/libeditor/tests/test_bug599983.xhtml @@ -0,0 +1,67 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" + type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=599983 +--> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Mozilla Bug 599983" onload="runTest()"> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=599983" + target="_blank">Mozilla Bug 599983</a> + <editor xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + id="editor" + editortype="html" + src="about:blank" /> + </body> + <script type="application/javascript"> + <![CDATA[ + + SimpleTest.waitForExplicitFinish(); + + const kAllowInteraction = Ci.nsIEditor.eEditorAllowInteraction; + const kMailMask = Ci.nsIEditor.eEditorMailMask; + + function runTest() { + testEditor(false, false); + testEditor(false, true); + testEditor(true, false); + testEditor(true, true); + + SimpleTest.finish(); + } + + function testEditor(setAllowInteraction, setMailMask) { + var desc = " with " + (setAllowInteraction ? "" : "no ") + + "eEditorAllowInteraction and " + + (setMailMask ? "" : "no ") + "eEditorMailMask"; + + var editorElem = document.getElementById("editor"); + + var editorObj = editorElem.getEditor(editorElem.contentWindow); + editorObj.flags = (setAllowInteraction ? kAllowInteraction : 0) | + (setMailMask ? kMailMask : 0); + + var editorDoc = editorElem.contentDocument; + editorDoc.body.innerHTML = "<p>foo<p>bar"; + editorDoc.getSelection().selectAllChildren(editorDoc.body.firstChild); + editorDoc.execCommand("bold"); + + var createsDirty = !setAllowInteraction || setMailMask; + + (createsDirty ? isnot : is)(editorDoc.querySelector("[_moz_dirty]"), null, + "Elements with _moz_dirty" + desc); + + // Even if we do create _moz_dirty, we should strip it for innerHTML. + is(editorDoc.body.innerHTML, "<p><b>foo</b></p><p>bar</p>", + "innerHTML" + desc); + } + + ]]> + </script> +</window> diff --git a/editor/libeditor/tests/test_bug600570.html b/editor/libeditor/tests/test_bug600570.html new file mode 100644 index 0000000000..f4ef84199c --- /dev/null +++ b/editor/libeditor/tests/test_bug600570.html @@ -0,0 +1,81 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=600570 +--> +<head> + <title>Test for Bug 600570</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/WindowSnapshot.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style> + textarea { border-color: white; } + </style> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=600570">Mozilla Bug 600570</a> +<p id="display"></p> +<div id="content"> +<textarea spellcheck="false"> +aaa +[bbb]</textarea> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 600570 **/ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var t = document.querySelector("textarea"); + t.value = "[aaa\nbbb]"; + t.focus(); + synthesizeKey("A", {accelKey: true}); + + SimpleTest.executeSoon(function() { + t.getBoundingClientRect(); // flush layout + var afterSetValue = snapshotWindow(window); + + t.value = t.defaultValue; + + t.selectionStart = 0; + t.selectionEnd = 4; + SimpleTest.waitForClipboard("aaa\n", + function() { + synthesizeKey("X", {accelKey: true}); + }, + function() { + t.addEventListener("input", function() { + setTimeout(function() { // Avoid the assertion in bug 649797 + is(t.value, "[aaa\nbbb]", "The value of the textarea should be correct"); + synthesizeKey("A", {accelKey: true}); + is(t.selectionStart, 0, "Select all should set the selection start to the beginning of textarea"); + is(t.selectionEnd, 9, "Select all should set the selection end to the end of textarea"); + + var afterPaste = snapshotWindow(window); + + var res = compareSnapshots(afterSetValue, afterPaste, true); + var msg = "Pasting and setting the value directly should result in the same rendering"; + if (!res[0]) { + msg += "\nRESULT:\n" + res[2] + "\nREFERENCE:\n" + res[1]; + } + ok(res[0], msg); + + SimpleTest.finish(); + }, 0); + }, {once: true}); + synthesizeKey("KEY_ArrowRight"); + synthesizeKey("V", {accelKey: true}); + }, + function() { + SimpleTest.finish(); + } + ); + }); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug603556.html b/editor/libeditor/tests/test_bug603556.html new file mode 100644 index 0000000000..1e27d824e3 --- /dev/null +++ b/editor/libeditor/tests/test_bug603556.html @@ -0,0 +1,53 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=603556 +--> +<head> + <title>Test for Bug 603556</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=603556">Mozilla Bug 603556</a> +<p id="display"></p> +<div id="content"> + <div id="src">testing</div> + <input maxlength="4"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 603556 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var i = document.querySelector("input"); + var src = document.getElementById("src"); + SimpleTest.waitForClipboard(src.textContent, + function() { + getSelection().selectAllChildren(src); + synthesizeKey("C", {accelKey: true}); + }, + function() { + i.focus(); + synthesizeKey("V", {accelKey: true}); + if (!SpecialPowers.getBoolPref("editor.truncate_user_pastes")) { + is(i.value, src.textContent, + "Pasting should paste the clipboard contents regardless of maxlength"); + } else { + is(i.value, src.textContent.substr(0, i.maxLength), + "Pasting should paste maxlength chars worth of the clipboard contents"); + } + SimpleTest.finish(); + }, + function() { + SimpleTest.finish(); + } + ); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug604532.html b/editor/libeditor/tests/test_bug604532.html new file mode 100644 index 0000000000..cd790a58b1 --- /dev/null +++ b/editor/libeditor/tests/test_bug604532.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=604532 +--> +<head> + <title>Test for Bug 604532</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=604532">Mozilla Bug 604532</a> +<p id="display"></p> +<div id="content"> +<input> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 604532 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var i = document.querySelector("input"); + i.focus(); + i.value = "foo"; + synthesizeKey("A", {accelKey: true}); + is(i.selectionStart, 0, "Selection should start at 0 before appending"); + is(i.selectionEnd, 3, "Selection should end at 3 before appending"); + synthesizeKey("KEY_ArrowRight"); + sendString("x"); + is(i.value, "foox", "The text should be appended correctly"); + synthesizeKey("A", {accelKey: true}); + is(i.selectionStart, 0, "Selection should start at 0 after appending"); + is(i.selectionEnd, 4, "Selection should end at 4 after appending"); + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug607584.html b/editor/libeditor/tests/test_bug607584.html new file mode 100644 index 0000000000..c2595af108 --- /dev/null +++ b/editor/libeditor/tests/test_bug607584.html @@ -0,0 +1,41 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=607584 +--> +<head> + <title>Test for Bug 607584</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=607584">Mozilla Bug 607584</a> +<p id="display"></p> +<div id="content" contenteditable> +<p id="foo">Hello world</p> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 607584 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var content = document.getElementById("content"); + content.focus(); + var sel = getSelection(); + sel.collapse(document.getElementById("foo").firstChild, 5); + synthesizeKey("KEY_Enter"); + var paragraphs = content.querySelectorAll("p"); + is(paragraphs.length, 2, "The paragraph should be split in two"); + is(paragraphs[0].textContent, "Hello", "The first paragraph should have the correct content"); + is(paragraphs[1].textContent, "\u00A0world", "The second paragraph should have the correct content"); + is(paragraphs[0].getAttribute("id"), "foo", "The id of the first paragraph should be retained"); + is(paragraphs[1].hasAttribute("id"), false, "The second paragraph shouldn't have an ID"); + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug607584.xhtml b/editor/libeditor/tests/test_bug607584.xhtml new file mode 100644 index 0000000000..f610de544b --- /dev/null +++ b/editor/libeditor/tests/test_bug607584.xhtml @@ -0,0 +1,112 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" + type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=607584 +--> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Mozilla Bug 607584" onload="runTest();"> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=607584" + target="_blank">Mozilla Bug 607584</a> + <p/> + <editor xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + id="editor" + type="content" + primary="true" + editortype="html" + style="width: 400px; height: 100px; border: thin solid black"/> + <p/> + <pre id="test"> + </pre> + </body> + <script class="testbody" type="application/javascript"> + <![CDATA[ + + SimpleTest.waitForExplicitFinish(); + + function EditorContentListener(aEditor) + { + this.init(aEditor); + } + + EditorContentListener.prototype = { + init(aEditor) + { + this.mEditor = aEditor; + }, + + QueryInterface: ChromeUtils.generateQI(["nsIWebProgressListener", + "nsISupportsWeakReference"]), + + onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) + { + if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) + { + var editor = this.mEditor.getEditor(this.mEditor.contentWindow); + if (editor) { + this.mEditor.focus(); + editor instanceof Ci.nsIHTMLEditor; + editor.returnInParagraphCreatesNewParagraph = true; + editor.insertHTML("<p id='foo'>this is a paragraph carrying id 'foo'</p>"); + var p = editor.document.getElementById('foo') + editor.beginningOfDocument(); + sendKey("return"); + var firstP = p.parentNode.firstElementChild; + var lastP = p.parentNode.lastElementChild; + var isOk = firstP.nodeName.toLowerCase() == "p" && + firstP.id == "foo" && + lastP.id == ""; + ok(isOk, "CR in a paragraph with an ID should not create two paragraphs of same ID"); + progress.removeProgressListener(this); + SimpleTest.finish(); + } + } + + }, + + + onProgressChange(aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, + aCurTotalProgress, aMaxTotalProgress) + { + }, + + onLocationChange(aWebProgress, aRequest, aLocation, aFlags) + { + }, + + onStatusChange(aWebProgress, aRequest, aStatus, aMessage) + { + }, + + onSecurityChange(aWebProgress, aRequest, aState) + { + }, + + onContentBlockingEvent(aWebProgress, aRequest, aEvent) + { + }, + + mEditor: null + }; + + var progress, progressListener; + + function runTest() { + var newEditorElement = document.getElementById("editor"); + newEditorElement.makeEditable("html", true); + var docShell = newEditorElement.docShell; + progress = docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebProgress); + progressListener = new EditorContentListener(newEditorElement); + progress.addProgressListener(progressListener, Ci.nsIWebProgress.NOTIFY_ALL); + newEditorElement.setAttribute("src", "data:text/html,"); + } +]]> +</script> +</window> diff --git a/editor/libeditor/tests/test_bug611182.html b/editor/libeditor/tests/test_bug611182.html new file mode 100644 index 0000000000..7843156fda --- /dev/null +++ b/editor/libeditor/tests/test_bug611182.html @@ -0,0 +1,105 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=611182 +--> +<head> + <title>Test for Bug 611182</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/WindowSnapshot.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=611182">Mozilla Bug 611182</a> +<p id="display"></p> +<div id="content"> + <iframe></iframe> + <iframe id="ref" src="./file_bug611182.html"></iframe> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 611182 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var iframe = document.querySelector("iframe"); + var refElem = document.querySelector("#ref"); + var ref = snapshotWindow(refElem.contentWindow, false); + + function findTextNode(doc) { + var body = doc.documentElement; + var result = findTextNodeWorker(body); + ok(result, "Failed to find the text node"); + return result; + } + + function findTextNodeWorker(root) { + if (root.isContentEditable) { + root.focus(); + } + for (var i = 0; i < root.childNodes.length; ++i) { + var node = root.childNodes[i]; + if (node.nodeType == node.TEXT_NODE && + node.nodeValue == "fooz bar") { + return node; + } + if (node.nodeType == node.ELEMENT_NODE) { + node = findTextNodeWorker(node); + if (node) { + return node; + } + } + } + return null; + } + + function testBackspace(src, callback) { + ok(true, "Testing " + src); + iframe.addEventListener("load", function() { + var doc = iframe.contentDocument; + var win = iframe.contentWindow; + doc.body.setAttribute("spellcheck", "false"); + + iframe.focus(); + var textNode = findTextNode(doc); + var sel = win.getSelection(); + sel.collapse(textNode, 4); + synthesizeKey("KEY_Backspace"); + is(textNode.textContent, "foo bar", "Backspace should work correctly"); + + var snapshot = snapshotWindow(win, false); + ok(compareSnapshots(snapshot, ref, true)[0], + "No padding <br> element should exist in the document"); + + callback(); + }, {once: true}); + iframe.src = src; + } + + var totalTests = 0; + var currentTest = 0; + function runAllTests() { + if (currentTest == totalTests) { + SimpleTest.finish(); + return; + } + testBackspace("file_bug611182.sjs?" + currentTest, runAllTests); + currentTest++; + } + + // query total number of tests to be run from the server and + // start running all tests. + var myXHR = new XMLHttpRequest(); + myXHR.open("GET", "file_bug611182.sjs?queryTotalTests"); + myXHR.onload = function(e) { + totalTests = myXHR.responseText; + runAllTests(); + }; + myXHR.send(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug612128.html b/editor/libeditor/tests/test_bug612128.html new file mode 100644 index 0000000000..3d6d7ed34e --- /dev/null +++ b/editor/libeditor/tests/test_bug612128.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=612128 +--> +<head> + <title>Test for Bug 612128</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=612128">Mozilla Bug 612128</a> +<p id="display"></p> +<div id="content"> +<input> +<div contenteditable></div> +</div> +<pre id="test"> +<script type="application/javascript"> + +/* eslint-disable no-useless-concat */ + +/** Test for Bug 612128 **/ +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + document.querySelector("input").focus(); + try { + is(document.execCommand("inserthtml", null, "<span>f" + "oo</span>"), + false, "The insertHTML command should return false"); + } catch (e) { + ok(false, "insertHTML should not throw here"); + } + is(document.querySelectorAll("span").length, 0, "No span element should be injected inside the page"); + is(document.body.innerHTML.indexOf("f" + "oo"), -1, "No text should be injected inside the page"); + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug612447.html b/editor/libeditor/tests/test_bug612447.html new file mode 100644 index 0000000000..fdb3b38f25 --- /dev/null +++ b/editor/libeditor/tests/test_bug612447.html @@ -0,0 +1,74 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=612447 +--> +<head> + <title>Test for Bug 612447</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/WindowSnapshot.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=612447">Mozilla Bug 612447</a> +<p id="display"></p> +<div id="content"> +<iframe></iframe> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 612447 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + function editorCommandsEnabled() { + var caught = false; + try { + doc.execCommand("justifyfull", false, null); + } catch (e) { + caught = true; + } + return !caught; + } + + var i = document.querySelector("iframe"); + var doc = i.contentDocument; + var win = i.contentWindow; + var b = doc.body; + doc.designMode = "on"; + i.focus(); + b.focus(); + var beforeA = snapshotWindow(win, true); + sendString("X"); + var beforeB = snapshotWindow(win, true); + is(b.textContent, "X", "Typing should work"); + while (b.firstChild) { + b.firstChild.remove(); + } + ok(editorCommandsEnabled(), "The editor commands should work"); + + i.style.display = "block"; + document.clientWidth; + + i.focus(); + b.focus(); + var afterA = snapshotWindow(win, true); + sendString("X"); + var afterB = snapshotWindow(win, true); + is(b.textContent, "X", "Typing should work"); + while (b.firstChild) { + b.firstChild.remove(); + } + ok(editorCommandsEnabled(), "The editor commands should work"); + + ok(compareSnapshots(beforeA, afterA, true)[0], "The iframes should look the same before typing"); + ok(compareSnapshots(beforeB, afterB, true)[0], "The iframes should look the same after typing"); + + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug616590.xhtml b/editor/libeditor/tests/test_bug616590.xhtml new file mode 100644 index 0000000000..1f6cb3d0f8 --- /dev/null +++ b/editor/libeditor/tests/test_bug616590.xhtml @@ -0,0 +1,101 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" + type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=616590 +--> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Mozilla Bug 616590" onload="runTest();"> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=616590" + target="_blank">Mozilla Bug 616590</a> + <p/> + <editor xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + id="editor" + type="content" + editortype="htmlmail" + style="width: 400px; height: 100px;"/> + <p/> + <pre id="test"> + </pre> + </body> + <script class="testbody" type="application/javascript"> + <![CDATA[ + + SimpleTest.waitForExplicitFinish(); + + function EditorContentListener(aEditor) + { + this.init(aEditor); + } + + EditorContentListener.prototype = { + init(aEditor) + { + this.mEditor = aEditor; + }, + + QueryInterface: ChromeUtils.generateQI(["nsIWebProgressListener", + "nsISupportsWeakReference"]), + + onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) + { + if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) + { + var editor = this.mEditor.getEditor(this.mEditor.contentWindow); + if (editor) { + editor.QueryInterface(Ci.nsIEditorMailSupport); + editor.insertAsCitedQuotation("<html><body><div contenteditable>foo</div></body></html>", "", true); + document.documentElement.clientWidth; + progress.removeProgressListener(this); + ok(true, "Test complete"); + SimpleTest.finish(); + } + } + }, + + + onProgressChange(aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, + aCurTotalProgress, aMaxTotalProgress) + { + }, + + onLocationChange(aWebProgress, aRequest, aLocation, aFlags) + { + }, + + onStatusChange(aWebProgress, aRequest, aStatus, aMessage) + { + }, + + onSecurityChange(aWebProgress, aRequest, aState) + { + }, + + onContentBlockingEvent(aWebProgress, aRequest, aEvent) + { + }, + + mEditor: null + }; + + var progress, progressListener; + + function runTest() { + var editorElement = document.getElementById("editor"); + editorElement.makeEditable("htmlmail", true); + var docShell = editorElement.docShell; + progress = docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebProgress); + progressListener = new EditorContentListener(editorElement); + progress.addProgressListener(progressListener, Ci.nsIWebProgress.NOTIFY_ALL); + editorElement.setAttribute("src", "data:text/html,"); + } +]]> +</script> +</window> diff --git a/editor/libeditor/tests/test_bug622371.html b/editor/libeditor/tests/test_bug622371.html new file mode 100644 index 0000000000..de52efb618 --- /dev/null +++ b/editor/libeditor/tests/test_bug622371.html @@ -0,0 +1,44 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=622371 +--> +<head> + <title>Test for Bug 622371</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=622371">Mozilla Bug 622371</a> +<p id="display"></p> +<div id="content"> + <iframe srcdoc="<body contenteditable>abc</body>"></iframe> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 622371 **/ +// This test is ported to WPT: +// html/editing/editing-0/contenteditable/selection-in-contentEditable-at-turning-designMode-on-off.tentative.html +// But unfortunately, the random orange reported as bug 1601585 with it. +// Therefore, this test is not removed. +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var i = document.querySelector("iframe"); + var sel = i.contentWindow.getSelection(); + var doc = i.contentDocument; + var body = doc.body; + i.focus(); + sel.collapse(body, 1); + doc.designMode = "on"; + doc.designMode = "off"; + is(sel.getRangeAt(0).startOffset, 1, "The start offset of the selection shouldn't change"); + is(sel.getRangeAt(0).endOffset, 1, "The end offset of the selection shouldn't change"); + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug625452.html b/editor/libeditor/tests/test_bug625452.html new file mode 100644 index 0000000000..68ff16342f --- /dev/null +++ b/editor/libeditor/tests/test_bug625452.html @@ -0,0 +1,66 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=625452 +--> +<head> + <title>Test for Bug 625452</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=625452">Mozilla Bug 625452</a> +<p id="display"></p> +<div id="content"> +<input> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 625452 **/ +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var i = document.querySelector("input"); + var inputCount = 0; + i.addEventListener("input", function() { inputCount++; }); + + // test cut + i.focus(); + i.value = "foo bar"; + i.selectionStart = 0; + i.selectionEnd = 4; + synthesizeKey("X", {accelKey: true}); + is(i.value, "bar", "Cut should work correctly"); + is(inputCount, 1, "input event should be raised correctly"); + + // test undo + synthesizeKey("Z", {accelKey: true}); + is(i.value, "foo bar", "Undo should work correctly"); + is(inputCount, 2, "input event should be raised correctly"); + + // test redo + synthesizeKey("Z", {accelKey: true, shiftKey: true}); + is(i.value, "bar", "Redo should work correctly"); + is(inputCount, 3, "input event should be raised correctly"); + + // test delete + i.selectionStart = 0; + i.selectionEnd = 2; + synthesizeKey("KEY_Delete"); + is(i.value, "r", "Delete should work correctly"); + is(inputCount, 4, "input event should be raised correctly"); + + // test DeleteSelection(eNone) + i.value = "retest"; // the "r" common prefix is crucial here + is(inputCount, 4, "input event should not have been raised"); + + // paste is tested in test_bug596001.html + + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug629172.html b/editor/libeditor/tests/test_bug629172.html new file mode 100644 index 0000000000..55d1ed89b6 --- /dev/null +++ b/editor/libeditor/tests/test_bug629172.html @@ -0,0 +1,105 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=629172 +--> +<head> + <title>Test for Bug 629172</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/WindowSnapshot.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=629172">Mozilla Bug 629172</a> +<p id="display"></p> +<div id="content"> +<textarea id="ltr-ref">test.</textarea> +<textarea id="rtl-ref" style="display: none; direction: rtl">test.</textarea> +</div> +<pre id="test"> +<script> + +/** Test for Bug 629172 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(async function() { + await SpecialPowers.pushPrefEnv({ + set: [["test.events.async.enabled", true]], + }); + + let LTRRef = document.getElementById("ltr-ref"); + let RTLRef = document.getElementById("rtl-ref"); + let ReferenceScreenshots = {}; + + // generate the reference screenshots + document.body.clientWidth; + ReferenceScreenshots.ltr = snapshotWindow(window); + LTRRef.remove(); + RTLRef.style.display = ""; + document.body.clientWidth; + ReferenceScreenshots.rtl = snapshotWindow(window); + RTLRef.remove(); + + async function testDirection(initialDir) { + function revertDir(aDir) { + return aDir == "rtl" ? "ltr" : "rtl"; + } + + function promiseFormatSetBlockTextDirectionInputEvent(aElement) { + return new Promise(resolve => { + function handler(aEvent) { + if (aEvent.inputType !== "formatSetBlockTextDirection") { + ok(false, `Unexpected input event received: inputType="${aEvent.inputType}"`); + } else { + aElement.removeEventListener("input", handler, true); + SimpleTest.executeSoon(resolve); + } + } + aElement.addEventListener("input", handler, true); + }); + } + + let textarea = document.createElement("textarea"); + textarea.setAttribute("dir", initialDir); + textarea.value = "test."; + document.getElementById("content").appendChild(textarea); + document.body.clientWidth; + assertSnapshots(snapshotWindow(window), ReferenceScreenshots[initialDir], + /* expectEqual = */ true, /* fuzz = */ null, + `<textarea dir="${initialDir}"> before Accel+Shift+X`, + `<textarea dir="${initialDir}">`); + textarea.focus(); + let waitForInputEvent = promiseFormatSetBlockTextDirectionInputEvent(textarea); + synthesizeKey("X", {accelKey: true, shiftKey: true}); + await waitForInputEvent; + is(textarea.getAttribute("dir"), revertDir(initialDir), + "The dir attribute must be correctly updated with first Accel+Shift+X"); + textarea.blur(); + assertSnapshots(snapshotWindow(window), ReferenceScreenshots[revertDir(initialDir)], + /* expectEqual = */ true, /* fuzz = */ null, + `<textarea dir="${initialDir}"> after first Accel+Shift+X`, + `<textarea dir="${revertDir(initialDir)}">`); + textarea.focus(); + waitForInputEvent = promiseFormatSetBlockTextDirectionInputEvent(textarea); + synthesizeKey("X", {accelKey: true, shiftKey: true}); + await waitForInputEvent; + is(textarea.getAttribute("dir"), initialDir, + "The dir attribute must be correctly recovered with second Accel+Shift+X"); + textarea.blur(); + assertSnapshots(snapshotWindow(window), ReferenceScreenshots[initialDir], + /* expectEqual = */ true, /* fuzz = */ null, + `<textarea dir="${initialDir}"> after second Accel+Shift+X`, + `<textarea dir="${initialDir}">`); + textarea.remove(); + } + + await testDirection("ltr"); + await testDirection("rtl"); + + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug629845.html b/editor/libeditor/tests/test_bug629845.html new file mode 100644 index 0000000000..e7c4bd5019 --- /dev/null +++ b/editor/libeditor/tests/test_bug629845.html @@ -0,0 +1,58 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=629845 +--> +<head> + <title>Test for Bug 629845</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=629845">Mozilla Bug 629845</a> +<p id="display"></p> + +<script> +function initFrame(frame) { + frame.contentWindow.document.designMode = "on"; + frame.contentWindow.document.writeln("<body></body>"); + + document.getElementsByTagName("button")[0].click(); +} + +function command(aName) { + var frame = document.getElementsByTagName("iframe")[0]; + + is(frame.contentDocument.designMode, "on", "design mode should be on!"); + var caught = false; + try { + frame.contentDocument.execCommand(aName, false, null); + } catch (e) { + ok(false, "exception " + e + " was thrown"); + caught = true; + } + + ok(!caught, "No exception should have been thrown."); + + // Stop the document load before finishing, just to be clean. + document.getElementsByTagName("iframe")[0].contentWindow.document.close(); + SimpleTest.finish(); +} +</script> + +<div id="content"> + <button type="button" onclick="command('bold');">Bold</button> + <iframe onload="initFrame(this);"></iframe> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 629845 **/ + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug635636.html b/editor/libeditor/tests/test_bug635636.html new file mode 100644 index 0000000000..3da04e18f2 --- /dev/null +++ b/editor/libeditor/tests/test_bug635636.html @@ -0,0 +1,63 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=635636 +--> +<head> + <title>Test for Bug 635636</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=635636">Mozilla Bug 635636</a> +<p id="display"></p> +<div id="content"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 635636 **/ +SimpleTest.waitForExplicitFinish(); +addLoadEvent(async function() { + function openNewWindow(aURL) { + return new Promise(resolve => { + let contentWindow = window.open(aURL); + contentWindow.addEventListener("load", () => { + ok(true, aURL + " is loaded"); + resolve(contentWindow); + }, { once: true }); + }); + } + + function unloadWindow(aWindow) { + return new Promise(resolve => { + aWindow.addEventListener("unload", () => { + ok(true, "The window has been unloaded"); + SimpleTest.executeSoon(() => { resolve(0); }); + }, { once: true }); + aWindow.location = "file_bug635636_2.html"; + }); + } + + let contentWindow = await openNewWindow("file_bug635636.xhtml"); + + contentWindow.addEventListener("load", () => { ok(true, "load"); }); + contentWindow.addEventListener("pageshow", () => { ok(true, "pageshow"); }); + + let div = contentWindow.document.getElementsByTagName("div")[0]; + contentWindow.document.designMode = "on"; + + await unloadWindow(contentWindow); + + div.remove(); + ok(true, "Should not crash"); + // Not needed for the crash + contentWindow.close(); + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug638596.html b/editor/libeditor/tests/test_bug638596.html new file mode 100644 index 0000000000..976ccb32ab --- /dev/null +++ b/editor/libeditor/tests/test_bug638596.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=638596 +--> +<head> + <title>Test for Bug 638596</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=638596">Mozilla Bug 638596</a> +<p id="display"></p> +<div id="content"> + <input type="password" style="font-size: 0"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 638596 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var i = document.querySelector("input"); + i.focus(); + sendString("test"); + is(i.value, "test", "The correct value should be stored in the field"); + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug641466.html b/editor/libeditor/tests/test_bug641466.html new file mode 100644 index 0000000000..5004df2d8a --- /dev/null +++ b/editor/libeditor/tests/test_bug641466.html @@ -0,0 +1,48 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=641466 +--> +<head> + <title>Test for Bug 641466</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=641466">Mozilla Bug 641466</a> +<p id="display"></p> +<div id="content"> +<input value="𐑑𐑧𐑕𐑑"> +<textarea>𐑑𐑧𐑕𐑑</textarea> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 641466 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + function doTest(element) { + element.focus(); + element.selectionStart = 4; + element.selectionEnd = 4; + synthesizeKey("KEY_Backspace", {repeat: 4}); + + // XXX: Switched to from ok() to todo_is() in Bug 1467712. Follow up in 1500964 + // This test is not working for several reasons: + // - race conditions between each event, we should wait before sending the next backspace + // - race conditions between the two tests + // - the value has an initial length of 8, not 4 + todo_is(element.value, "", "4 backspaces should delete all of the characters in the " + element.localName); + } + + doTest(document.querySelector("input")); + doTest(document.querySelector("textarea")); + + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug645914.html b/editor/libeditor/tests/test_bug645914.html new file mode 100644 index 0000000000..8f1a8cbfda --- /dev/null +++ b/editor/libeditor/tests/test_bug645914.html @@ -0,0 +1,63 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=645914 +--> +<head> + <title>Test for Bug 645914</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=645914">Mozilla Bug 645914</a> +<p id="display"></p> +<div id="content"> +<textarea>foo +bar</textarea> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 645914 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + SpecialPowers.pushPrefEnv({"set": [["layout.word_select.eat_space_to_next_word", true], + ["browser.triple_click_selects_paragraph", false]]}, startTest); +}); +function startTest() { + var textarea = document.querySelector("textarea"); + textarea.selectionStart = textarea.selectionEnd = 0; + + // Simulate a double click on foo + synthesizeMouse(textarea, 5, 5, {clickCount: 2}); + + ok(true, "Testing word selection"); + is(textarea.selectionStart, 0, "The start of the selection should be at the beginning of the text"); + is(textarea.selectionEnd, 3, "The end of the selection should not include a newline character"); + + textarea.selectionStart = textarea.selectionEnd = 0; + + // Simulate a triple click on foo + synthesizeMouse(textarea, 5, 5, {clickCount: 3}); + + ok(true, "Testing line selection"); + is(textarea.selectionStart, 0, "The start of the selection should be at the beginning of the text"); + is(textarea.selectionEnd, 3, "The end of the selection should not include a newline character"); + + textarea.selectionStart = textarea.selectionEnd = 0; + textarea.value = "Very very long value which would eventually overflow the visible section of the textarea"; + + // Simulate a quadruple click on Very + synthesizeMouse(textarea, 5, 5, {clickCount: 4}); + + ok(true, "Testing paragraph selection"); + is(textarea.selectionStart, 0, "The start of the selection should be at the beginning of the text"); + is(textarea.selectionEnd, textarea.value.length, "The end of the selection should be the end of the paragraph"); + + SimpleTest.finish(); +} +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug646194.html b/editor/libeditor/tests/test_bug646194.html new file mode 100644 index 0000000000..5131e9cab0 --- /dev/null +++ b/editor/libeditor/tests/test_bug646194.html @@ -0,0 +1,36 @@ +<!doctype html> +<title>Mozilla Bug 646194</title> +<link rel=stylesheet href="/tests/SimpleTest/test.css"> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=646194" + target="_blank">Mozilla Bug 646194</a> +<iframe id="i" srcdoc="<div contenteditable=true id=t>test me now</div>"></iframe> +<script> +function runTest() { + var i = document.getElementById("i"); + i.focus(); + var win = i.contentWindow; + var doc = i.contentDocument; + var t = doc.getElementById("t"); + t.focus(); + // put the caret at the end + win.getSelection().collapse(t.firstChild, 11); + + // Simulate pression Option+Delete on Mac + // We do things this way because not every platform can invoke this + // command using the available key bindings. + SpecialPowers.doCommand(window, "cmd_wordPrevious"); + SpecialPowers.doCommand(window, "cmd_wordPrevious"); + SpecialPowers.doCommand(window, "cmd_deleteWordBackward"); + SpecialPowers.doCommand(window, "cmd_deleteWordBackward"); + + // If we reach here, we haven't crashed. Phew! + // But let's check the value too, now that we're here. + is(t.textContent, "me now", "The command has worked correctly"); + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTest); +</script> diff --git a/editor/libeditor/tests/test_bug668599.html b/editor/libeditor/tests/test_bug668599.html new file mode 100644 index 0000000000..1bf9a64075 --- /dev/null +++ b/editor/libeditor/tests/test_bug668599.html @@ -0,0 +1,73 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=668599 +--> +<head> + <title>Test for Bug 668599</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=668599">Mozilla Bug 668599</a> +<p id="display"></p> +<div id="content"> + <div id="test1"> + block <span contenteditable>type here</span> block + </div> + <div id="test2"> + <p contenteditable> + block <span>type here</span> block + </p> + </div> +</div> + +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 668599 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + +function select(element) { + // select the element text content + var userSelection = window.getSelection(); + window.getSelection().removeAllRanges(); + var range = document.createRange(); + range.setStart(element.firstChild, 0); + range.setEnd(element.firstChild, element.textContent.length); + userSelection.addRange(range); +} + +function runTests() { + var span = document.querySelector("#test1 span"); + + // editable <span> => the <span> *content* should be deleted + select(span); + span.focus(); + sendString("x"); + is(span.textContent, "x", "The <span> content should have been replaced by 'x'."); + + // same thing, but using [Del] instead of typing some text + document.execCommand("Undo", false, null); + select(span); + span.focus(); + synthesizeKey("KEY_Delete"); + is(span.textContent, "", "The <span> content should have been deleted."); + + // <span> in editable block => the <span> *element* should be deleted + select(document.querySelector("#test2 span")); + document.querySelector("#test2 [contenteditable]").focus(); + synthesizeKey("KEY_Delete"); + is(document.querySelector("#test2 span"), null, + "The <span> element should have been deleted."); + + // done + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug674770-1.html b/editor/libeditor/tests/test_bug674770-1.html new file mode 100644 index 0000000000..0b6089e0ef --- /dev/null +++ b/editor/libeditor/tests/test_bug674770-1.html @@ -0,0 +1,85 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=674770 +--> +<head> + <title>Test for Bug 674770</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=674770">Mozilla Bug 674770</a> +<p id="display"></p> +<div id="content"> +<a href="file_bug674770-1.html" id="link1">test</a> +<div contenteditable> +<a href="file_bug674770-1.html" id="link2">test</a> +</div> +</div> +<pre id="test"> +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + SpecialPowers.pushPrefEnv({"set": [["middlemouse.paste", true]]}, startTests); +}); + +function startTests() { + var tests = [ + { description: "Testing link in <div>: ", + target() { return document.querySelector("#link1"); }, + linkShouldWork: true }, + { description: "Testing link in <div contenteditable>: ", + target() { return document.querySelector("#link2"); }, + linkShouldWork: false }, + ]; + var currentTest; + function runNextTest() { + localStorage.removeItem("clicked"); + currentTest = tests.shift(); + if (!currentTest) { + SimpleTest.finish(); + return; + } + ok(true, currentTest.description + "Starting to test..."); + synthesizeMouseAtCenter(currentTest.target(), { button: 1 }); + } + + + addEventListener("storage", function(e) { + is(e.key, "clicked", currentTest.description + "Key should always be 'clicked'"); + is(e.newValue, "true", currentTest.description + "Value should always be 'true'"); + ok(currentTest.linkShouldWork, currentTest.description + "The click operation on the link " + (currentTest.linkShouldWork ? "should work" : "shouldn't work")); + SimpleTest.executeSoon(runNextTest); + }, false); + + SpecialPowers.addSystemEventListener(window, "auxclick", function(aEvent) { + // When the click event should cause default action, e.g., opening the link, + // the event shouldn't have been consumed except the link handler. + // However, in e10s mode, it's not consumed during propagating the event but + // in non-e10s mode, it's consumed during the propagation. Therefore, + // we cannot check defaultPrevented value when the link should work as is + // if there is no way to detect if it's running in e10s mode or not. + // So, let's skip checking Event.defaultPrevented value when the link should + // work. In such case, we should receive "storage" event later. + if (currentTest.linkShouldWork) { + return; + } + + ok(SpecialPowers.defaultPreventedInAnyGroup(aEvent), + currentTest.description + "The default action should be consumed because the link should work as is"); + if (SpecialPowers.defaultPreventedInAnyGroup(aEvent)) { + // In this case, "storage" event won't be fired. + SimpleTest.executeSoon(runNextTest); + } + }, false); + + SimpleTest.executeSoon(runNextTest); +} + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug674770-2.html b/editor/libeditor/tests/test_bug674770-2.html new file mode 100644 index 0000000000..9b05277052 --- /dev/null +++ b/editor/libeditor/tests/test_bug674770-2.html @@ -0,0 +1,374 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=674770 +--> +<head> + <title>Test for Bug 674770</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=674770">Mozilla Bug 674770</a> +<p id="display"></p> +<div id="content"> +<iframe id="iframe" style="display: block; height: 14em;" + srcdoc="<input id='text' value='pasted'><input id='input' style='display: block;'><p id='editor1' contenteditable>editor1:</p><p id='editor2' contenteditable>editor2:</p>"></iframe> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 674770 **/ +SimpleTest.waitForExplicitFinish(); + +var iframe = document.getElementById("iframe"); +var frameWindow, frameDocument; + +var gClicked = false; +var gClicking = null; +var gDoPreventDefault1 = null; +var gDoPreventDefault2 = null; + +function clickEventHandler(aEvent) { + if (aEvent.button == 1 && aEvent.target == gClicking) { + gClicked = true; + } + if (gDoPreventDefault1 == aEvent.target) { + aEvent.preventDefault(); + } + if (gDoPreventDefault2 == aEvent.target) { + aEvent.preventDefault(); + } +} + +// NOTE: tests need to check the result *after* the content is actually +// modified. Sometimes, the modification is delayed. Therefore, there +// are a lot of functions and SimpleTest.executeSoon()s. +SimpleTest.waitForFocus(function() { + SpecialPowers.pushPrefEnv({"set": [["middlemouse.contentLoadURL", false], + ["middlemouse.paste", true]]}, startTest); +}); +function startTest() { + frameWindow = iframe.contentWindow; + frameDocument = iframe.contentDocument; + + frameDocument.getElementById("input").addEventListener("click", clickEventHandler); + frameDocument.getElementById("editor1").addEventListener("click", clickEventHandler); + frameDocument.getElementById("editor2").addEventListener("click", clickEventHandler); + + var text = frameDocument.getElementById("text"); + + text.focus(); + synthesizeKey("a", { accelKey: true }, frameWindow); + // Windows and Mac don't have primary selection, we should copy the text to + // the global clipboard. + if (!SpecialPowers.supportsSelectionClipboard()) { + SimpleTest.waitForClipboard("pasted", + function() { synthesizeKey("c", { accelKey: true }, frameWindow); }, + function() { SimpleTest.executeSoon(runInputTests1); }, + cleanup); + } else { + // Otherwise, don't call waitForClipboard since it breaks primary + // selection. + runInputTests1(); + } +} + +function runInputTests1() { + var input = frameDocument.getElementById("input"); + + // first, copy text. + + // when middle clicked in focused input element, text should be pasted. + input.value = ""; + input.focus(); + + SimpleTest.executeSoon(function() { + gClicked = false; + gClicking = input; + gDoPreventDefault1 = null; + gDoPreventDefault2 = null; + + synthesizeMouseAtCenter(input, {button: 1}, frameWindow); + + SimpleTest.executeSoon(function() { + todo(gClicked, "click event hasn't been fired for runInputTests1"); + is(input.value, "pasted", "failed to paste in input element"); + + SimpleTest.executeSoon(runInputTests2); + }); + }); +} + +function runInputTests2() { + var input = frameDocument.getElementById("input"); + + // even if the input element hasn't had focus, middle click should set focus + // to it and paste the text. + input.value = ""; + input.blur(); + + SimpleTest.executeSoon(function() { + gClicked = false; + gClicking = input; + gDoPreventDefault1 = null; + gDoPreventDefault2 = null; + + synthesizeMouseAtCenter(input, {button: 1}, frameWindow); + + SimpleTest.executeSoon(function() { + todo(gClicked, "click event hasn't been fired for runInputTests2"); + is(input.value, "pasted", + "failed to paste in input element when it hasn't had focus yet"); + + SimpleTest.executeSoon(runInputTests3); + }); + }); +} + +function runInputTests3() { + var input = frameDocument.getElementById("input"); + var editor1 = frameDocument.getElementById("editor1"); + + // preventDefault() of HTML editor's click event handler shouldn't prevent + // middle click pasting in input element. + input.value = ""; + input.focus(); + + SimpleTest.executeSoon(function() { + gClicked = false; + gClicking = input; + gDoPreventDefault1 = editor1; + gDoPreventDefault2 = null; + + synthesizeMouseAtCenter(input, {button: 1}, frameWindow); + + SimpleTest.executeSoon(function() { + todo(gClicked, "click event hasn't been fired for runInputTests3"); + is(input.value, "pasted", + "failed to paste in input element when editor1 does preventDefault()"); + + SimpleTest.executeSoon(runInputTests4); + }); + }); +} + +function runInputTests4() { + var input = frameDocument.getElementById("input"); + + // preventDefault() of input element's click event handler should prevent + // middle click pasting in it. + input.value = ""; + input.focus(); + + SimpleTest.executeSoon(function() { + gClicked = false; + gClicking = input; + gDoPreventDefault1 = input; + gDoPreventDefault2 = null; + + synthesizeMouseAtCenter(input, {button: 1}, frameWindow); + + SimpleTest.executeSoon(function() { + todo(gClicked, "click event hasn't been fired for runInputTests4"); + todo_is(input.value, "", + "pasted in input element when it does preventDefault()"); + + SimpleTest.executeSoon(runContentEditableTests1); + }); + }); +} + +function runContentEditableTests1() { + var editor1 = frameDocument.getElementById("editor1"); + + // when middle clicked in focused contentediable editor, text should be + // pasted. + editor1.innerHTML = "editor1:"; + editor1.focus(); + + SimpleTest.executeSoon(function() { + gClicked = false; + gClicking = editor1; + gDoPreventDefault1 = null; + gDoPreventDefault2 = null; + + synthesizeMouseAtCenter(editor1, {button: 1}, frameWindow); + + SimpleTest.executeSoon(function() { + todo(gClicked, "click event hasn't been fired for runContentEditableTests1"); + is(editor1.innerHTML, "editor1:pasted", + "failed to paste text in contenteditable editor"); + SimpleTest.executeSoon(runContentEditableTests2); + }); + }); +} + +function runContentEditableTests2() { + var editor1 = frameDocument.getElementById("editor1"); + + // even if the contenteditable editor hasn't had focus, middle click should + // set focus to it and paste the text. + editor1.innerHTML = "editor1:"; + editor1.blur(); + + SimpleTest.executeSoon(function() { + gClicked = false; + gClicking = editor1; + gDoPreventDefault1 = null; + gDoPreventDefault2 = null; + + synthesizeMouseAtCenter(editor1, {button: 1}, frameWindow); + + SimpleTest.executeSoon(function() { + todo(gClicked, "click event hasn't been fired for runContentEditableTests2"); + is(editor1.innerHTML, "editor1:pasted", + "failed to paste in contenteditable editor #1 when it hasn't had focus yet"); + SimpleTest.executeSoon(runContentEditableTests3); + }); + }); +} + +function runContentEditableTests3() { + var editor1 = frameDocument.getElementById("editor1"); + var editor2 = frameDocument.getElementById("editor2"); + + // When editor1 has focus but editor2 is middle clicked, should be pasted + // in the editor2. + editor1.innerHTML = "editor1:"; + editor2.innerHTML = "editor2:"; + editor1.focus(); + + SimpleTest.executeSoon(function() { + gClicked = false; + gClicking = editor2; + gDoPreventDefault1 = null; + gDoPreventDefault2 = null; + + synthesizeMouseAtCenter(editor2, {button: 1}, frameWindow); + + SimpleTest.executeSoon(function() { + todo(gClicked, "click event hasn't been fired for runContentEditableTests3"); + is(editor1.innerHTML, "editor1:", + "pasted in contenteditable editor #1 when editor2 is clicked"); + is(editor2.innerHTML, "editor2:pasted", + "failed to paste in contenteditable editor #2 when editor2 is clicked"); + SimpleTest.executeSoon(runContentEditableTests4); + }); + }); +} + +function runContentEditableTests4() { + var editor1 = frameDocument.getElementById("editor1"); + + // preventDefault() of editor1's click event handler should prevent + // middle click pasting in it. + editor1.innerHTML = "editor1:"; + editor1.focus(); + + SimpleTest.executeSoon(function() { + gClicked = false; + gClicking = editor1; + gDoPreventDefault1 = editor1; + gDoPreventDefault2 = null; + + synthesizeMouseAtCenter(editor1, {button: 1}, frameWindow); + + SimpleTest.executeSoon(function() { + todo(gClicked, "click event hasn't been fired for runContentEditableTests4"); + todo_is(editor1.innerHTML, "editor1:", + "pasted in contenteditable editor #1 when it does preventDefault()"); + SimpleTest.executeSoon(runContentEditableTests5); + }); + }); +} + +function runContentEditableTests5() { + var editor1 = frameDocument.getElementById("editor1"); + var editor2 = frameDocument.getElementById("editor2"); + + // preventDefault() of editor1's click event handler shouldn't prevent + // middle click pasting in editor2. + editor1.innerHTML = "editor1:"; + editor2.innerHTML = "editor2:"; + editor2.focus(); + + SimpleTest.executeSoon(function() { + gClicked = false; + gClicking = editor2; + gDoPreventDefault1 = editor1; + gDoPreventDefault2 = null; + + synthesizeMouseAtCenter(editor2, {button: 1}, frameWindow); + + SimpleTest.executeSoon(function() { + todo(gClicked, "click event hasn't been fired for runContentEditableTests5"); + is(editor1.innerHTML, "editor1:", + "pasted in contenteditable editor #1?"); + is(editor2.innerHTML, "editor2:pasted", + "failed to paste in contenteditable editor #2"); + + SimpleTest.executeSoon(initForBodyEditableDocumentTests); + }); + }); +} + +function initForBodyEditableDocumentTests() { + frameDocument.getElementById("input").removeEventListener("click", clickEventHandler); + frameDocument.getElementById("editor1").removeEventListener("click", clickEventHandler); + frameDocument.getElementById("editor2").removeEventListener("click", clickEventHandler); + + iframe.onload = + function(aEvent) { SimpleTest.executeSoon(runBodyEditableDocumentTests1); }; + iframe.srcdoc = "<body contenteditable>body:</body>"; +} + +function runBodyEditableDocumentTests1() { + frameWindow = iframe.contentWindow; + frameDocument = iframe.contentDocument; + + var body = frameDocument.body; + + is(body.innerHTML, "body:", + "failed to initialize at runBodyEditableDocumentTests1"); + + // middle click on html element should cause pasting text in its body. + synthesizeMouseAtCenter(frameDocument.documentElement, {button: 1}, frameWindow); + + SimpleTest.executeSoon(function() { + is(body.innerHTML, + "body:pasted", + "failed to paste in editable body element when clicked on html element"); + + SimpleTest.executeSoon(runBodyEditableDocumentTests2); + }); +} + +function runBodyEditableDocumentTests2() { + frameDocument.body.innerHTML = "body:<span id='span' contenteditable='false'>non-editable</span>"; + + var body = frameDocument.body; + + is(body.innerHTML, "body:<span id=\"span\" contenteditable=\"false\">non-editable</span>", + "failed to initialize at runBodyEditableDocumentTests2"); + + synthesizeMouseAtCenter(frameDocument.getElementById("span"), {button: 1}, frameWindow); + + SimpleTest.executeSoon(function() { + is(body.innerHTML, + "body:<span id=\"span\" contenteditable=\"false\">non-editable</span>", + "pasted when middle clicked in non-editable element"); + + SimpleTest.executeSoon(cleanup); + }); +} + +function cleanup() { + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug674861.html b/editor/libeditor/tests/test_bug674861.html new file mode 100644 index 0000000000..a103bb3c40 --- /dev/null +++ b/editor/libeditor/tests/test_bug674861.html @@ -0,0 +1,194 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=674861 +--> +<head> + <title>Test for Bug 674861</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=674861">Mozilla Bug 674861</a> +<p id="display"></p> +<div id="content"> + <section id="test1"> + <h2> Editable Bullet List </h2> + <ul contenteditable> + <li> item A </li> + <li> item B </li> + <li> item C </li> + </ul> + + <h2> Editable Ordered List </h2> + <ol contenteditable> + <li> item A </li> + <li> item B </li> + <li> item C </li> + </ol> + + <h2> Editable Definition List </h2> + <dl contenteditable> + <dt> term A </dt> + <dd> definition A </dd> + <dt> term B </dt> + <dd> definition B </dd> + <dt> term C </dt> + <dd> definition C </dd> + </dl> + </section> + + <section id="test2" contenteditable> + <h2> Bullet List In Editable Section </h2> + <ul> + <li> item A </li> + <li> item B </li> + <li> item C </li> + </ul> + + <h2> Ordered List In Editable Section </h2> + <ol> + <li> item A </li> + <li> item B </li> + <li> item C </li> + </ol> + + <h2> Definition List In Editable Section </h2> + <dl> + <dt> term A </dt> + <dd> definition A </dd> + <dt> term B </dt> + <dd> definition B </dd> + <dt> term C </dt> + <dd> definition C </dd> + </dl> + </section> +</div> + +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 674861 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + +const CARET_BEGIN = 0; +const CARET_MIDDLE = 1; +const CARET_END = 2; + +function try2split(element, caretPos) { + // compute the requested position + var len = element.textContent.length; + var pos = -1; + switch (caretPos) { + case CARET_BEGIN: + pos = 0; + break; + case CARET_MIDDLE: + pos = Math.floor(len / 2); + break; + case CARET_END: + pos = len; + break; + } + + // put the caret on the requested position + var sel = window.getSelection(); + for (var i = 0; i < sel.rangeCount; i++) { + var range = sel.getRangeAt(i); + sel.removeRange(range); + } + range = document.createRange(); + range.setStart(element.firstChild, pos); + range.setEnd(element.firstChild, pos); + sel.addRange(range); + + // simulates two [Return] keypresses + synthesizeKey("KEY_Enter"); + synthesizeKey("KEY_Enter"); +} + +function runTests() { + const test1 = document.getElementById("test1"); + const test2 = document.getElementById("test2"); + + // ----------------------------------------------------------------------- + // #test1: editable lists should NOT be splittable + // ----------------------------------------------------------------------- + const ul = test1.querySelector("ul"); + const ol = test1.querySelector("ol"); + const dl = test1.querySelector("dl"); + + // bullet list + ul.focus(); + try2split(ul.querySelector("li"), CARET_END); + is(test1.querySelectorAll("ul").length, 1, + "The <ul contenteditable> list should not be splittable."); + is(ul.querySelectorAll("li").length, 5, + "Two new <li> elements should have been created."); + + // ordered list + ol.focus(); + try2split(ol.querySelector("li"), CARET_END); + is(test1.querySelectorAll("ol").length, 1, + "The <ol contenteditable> list should not be splittable."); + is(ol.querySelectorAll("li").length, 5, + "Two new <li> elements should have been created."); + + // definition list + dl.focus(); + try2split(dl.querySelector("dd"), CARET_END); + is(test1.querySelectorAll("dl").length, 1, + "The <dl contenteditable> list should not be splittable."); + is(dl.querySelectorAll("dt").length, 5, + "Two new <dt> elements should have been created."); + + // ----------------------------------------------------------------------- + // #test2: lists in editable blocks should be splittable + // ----------------------------------------------------------------------- + test2.focus(); + + function testNewParagraph(expected) { + // bullet list + try2split(test2.querySelector("ul li"), CARET_END); + is(test2.querySelectorAll("ul").length, 2, + "The <ul> list should have been splitted."); + is(test2.querySelectorAll("ul li").length, 3, + "No new <li> element should have been created."); + is(test2.querySelectorAll("ul+" + expected).length, 1, + "A new " + expected + " should have been created in the <ul>."); + + // ordered list + try2split(test2.querySelector("ol li"), CARET_END); + is(test2.querySelectorAll("ol").length, 2, + "The <ol> list should have been splitted."); + is(test2.querySelectorAll("ol li").length, 3, + "No new <li> element should have been created."); + is(test2.querySelectorAll("ol+" + expected).length, 1, + "A new " + expected + " should have been created in the <ol>."); + + // definition list + try2split(test2.querySelector("dl dd"), CARET_END); + is(test2.querySelectorAll("dl").length, 2, + "The <dl> list should have been splitted."); + is(test2.querySelectorAll("dt").length, 3, + "No new <dt> element should have been created."); + is(test2.querySelectorAll("dl+" + expected).length, 1, + "A new " + expected + " should have been created in the <dl>."); + } + + document.execCommand("defaultParagraphSeparator", false, "div"); + testNewParagraph("div"); + document.execCommand("defaultParagraphSeparator", false, "p"); + testNewParagraph("p"); + document.execCommand("defaultParagraphSeparator", false, "br"); + testNewParagraph("p"); + + // done + SimpleTest.finish(); +} +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug676401.html b/editor/libeditor/tests/test_bug676401.html new file mode 100644 index 0000000000..35653461ae --- /dev/null +++ b/editor/libeditor/tests/test_bug676401.html @@ -0,0 +1,129 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=676401 +--> +<head> + <title>Test for Bug 676401</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=676401">Mozilla Bug 676401</a> +<p id="display"></p> +<div id="content"> + <!-- we need a blockquote to test the "outdent" command --> + <section> + <blockquote> not editable </blockquote> + </section> + <section contenteditable> + <blockquote> editable </blockquote> + </section> +</div> + +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 676401 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + +var gBlock1, gBlock2; + +var alwaysEnabledCommands = [ + "contentReadOnly", + "copy", + "cut", + "enableInlineTableEditing", + "enableObjectResizing", + "insertBrOnReturn", + "selectAll", + "styleWithCSS", +]; + +function ensureNobodyHasFocus() { + document.activeElement.blur(); +} + +function IsCommandEnabled(command) { + ensureNobodyHasFocus(); + + // non-editable div: should return false unless alwaysEnabled + window.getSelection().selectAllChildren(gBlock1); + is( + document.queryCommandEnabled(command), + alwaysEnabledCommands.includes(command) && document.queryCommandSupported(command), + "'" + command + "' should not be enabled on a non-editable block." + ); + + // editable div: should return true if it's supported + window.getSelection().selectAllChildren(gBlock2); + is( + document.queryCommandEnabled(command), + document.queryCommandSupported(command), + "'" + command + "' should be enabled on an editable block." + ); +} + +function runTests() { + var i, commands; + gBlock1 = document.querySelector("#content section blockquote"); + gBlock2 = document.querySelector("#content [contenteditable] blockquote"); + + // common commands: test with and without "styleWithCSS" + commands = [ + "bold", "italic", "underline", "strikeThrough", + "subscript", "superscript", "foreColor", "backColor", "hiliteColor", + "fontName", "fontSize", + "justifyLeft", "justifyCenter", "justifyRight", "justifyFull", + "indent", "outdent", + "insertOrderedList", "insertUnorderedList", "insertParagraph", + "heading", "formatBlock", + "contentReadOnly", "createLink", + "decreaseFontSize", "increaseFontSize", + "insertHTML", "insertHorizontalRule", "insertImage", + "removeFormat", "selectAll", "styleWithCSS", + ]; + document.execCommand("styleWithCSS", false, false); + for (i = 0; i < commands.length; i++) + IsCommandEnabled(commands[i]); + document.execCommand("styleWithCSS", false, true); + for (i = 0; i < commands.length; i++) + IsCommandEnabled(commands[i]); + + // Mozilla-specific stuff + commands = ["enableInlineTableEditing", "enableObjectResizing", "insertBrOnReturn"]; + for (i = 0; i < commands.length; i++) + IsCommandEnabled(commands[i]); + + // These are privileged, and available only to chrome. + ensureNobodyHasFocus(); + window.getSelection().selectAllChildren(gBlock2); + commands = ["paste"]; + for (i = 0; i < commands.length; i++) { + is(document.queryCommandEnabled(commands[i]), false, + "Command should not be enabled for non-privileged code"); + is(SpecialPowers.wrap(document).queryCommandEnabled(commands[i]), true, + "Command should be enabled for privileged code"); + is(document.execCommand(commands[i], false, false), false, "Should return false: " + commands[i]); + is(SpecialPowers.wrap(document).execCommand(commands[i], false, false), true, "Should return true: " + commands[i]); + } + + // delete/undo/redo -- we have to execute this commands because: + // * there's nothing to undo if we haven't modified the selection first + // * there's nothing to redo if we haven't undone something first + commands = ["delete", "undo", "redo"]; + for (i = 0; i < commands.length; i++) { + IsCommandEnabled(commands[i]); + document.execCommand(commands[i], false, false); + } + + // done + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug677752.html b/editor/libeditor/tests/test_bug677752.html new file mode 100644 index 0000000000..cadbbf4c0d --- /dev/null +++ b/editor/libeditor/tests/test_bug677752.html @@ -0,0 +1,107 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=677752 +--> +<head> + <title>Test for Bug 677752</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=677752">Mozilla Bug 677752</a> +<p id="display"></p> +<div id="content"> + <section contenteditable> foo bar </section> + <div contenteditable> foo bar </div> + <p contenteditable> foo bar </p> +</div> + +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 677752 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + +function selectEditor(aEditor) { + aEditor.focus(); + var selection = window.getSelection(); + selection.selectAllChildren(aEditor); + selection.collapseToStart(); +} + +function runTests() { + var editor, node, initialHTML; + document.execCommand("styleWithCSS", false, true); + + // editable <section> + editor = document.querySelector("section[contenteditable]"); + initialHTML = editor.innerHTML; + selectEditor(editor); + // editable <section>: justify + document.execCommand("justifyright", false, null); + node = editor.querySelector("*"); + is(node.nodeName.toLowerCase(), "div", "'justifyright' should create a <div> in the editable <section>."); + is(node.style.textAlign, "right", "'justifyright' should create a 'text-align: right' CSS rule."); + document.execCommand("undo", false, null); + // editable <section>: indent + document.execCommand("indent", false, null); + node = editor.querySelector("*"); + is(node.nodeName.toLowerCase(), "div", "'indent' should create a <div> in the editable <section>."); + is(node.style.marginLeft, "40px", "'indent' should create a 'margin-left: 40px' CSS rule."); + // editable <section>: undo with outdent + // this should remove the whole <div> but only removing the CSS rule would be acceptable, too + document.execCommand("outdent", false, null); + is(editor.innerHTML, initialHTML, "'outdent' should undo the 'indent' action."); + // editable <section>: outdent again + document.execCommand("outdent", false, null); + is(editor.innerHTML, initialHTML, "another 'outdent' should not modify the <section> element."); + + // editable <div> + editor = document.querySelector("div[contenteditable]"); + initialHTML = editor.innerHTML; + selectEditor(editor); + // editable <div>: justify + document.execCommand("justifyright", false, null); + node = editor.querySelector("*"); + is(node.nodeName.toLowerCase(), "div", "'justifyright' should create a <div> in the editable <div>."); + is(node.style.textAlign, "right", "'justifyright' should create a 'text-align: right' CSS rule."); + document.execCommand("undo", false, null); + // editable <div>: indent + document.execCommand("indent", false, null); + node = editor.querySelector("*"); + is(node.nodeName.toLowerCase(), "div", "'indent' should create a <div> in the editable <div>."); + is(node.style.marginLeft, "40px", "'indent' should create a 'margin-left: 40px' CSS rule."); + // editable <div>: undo with outdent + // this should remove the whole <div> but only removing the CSS rule would be acceptable, too + document.execCommand("outdent", false, null); + is(editor.innerHTML, initialHTML, "'outdent' should undo the 'indent' action."); + // editable <div>: outdent again + document.execCommand("outdent", false, null); + is(editor.innerHTML, initialHTML, "another 'outdent' should not modify the <div> element."); + + // editable <p> + // all block-level commands should be ignored (<p><div/></p> is not valid) + editor = document.querySelector("p[contenteditable]"); + initialHTML = editor.innerHTML; + selectEditor(editor); + // editable <p>: justify + document.execCommand("justifyright", false, null); + is(editor.innerHTML, initialHTML, "'justifyright' should have no effect on <p>."); + // editable <p>: indent + document.execCommand("indent", false, null); + is(editor.innerHTML, initialHTML, "'indent' should have no effect on <p>."); + // editable <p>: outdent + document.execCommand("outdent", false, null); + is(editor.innerHTML, initialHTML, "'outdent' should have no effect on <p>."); + + // done + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug681229.html b/editor/libeditor/tests/test_bug681229.html new file mode 100644 index 0000000000..f63c37329c --- /dev/null +++ b/editor/libeditor/tests/test_bug681229.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=681229 +--> +<head> + <title>Test for Bug 681229</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/WindowSnapshot.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=681229">Mozilla Bug 681229</a> +<p id="display"></p> +<div id="content"> +<textarea spellcheck="false"></textarea> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 681229 **/ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var t = document.querySelector("textarea"); + t.focus(); + + const kValue = "a\r\nb"; + + SimpleTest.waitForClipboard( + kValue.replace(/\r\n?/g, "\n"), + function() { + SpecialPowers.clipboardCopyString(kValue); + }, + function() { + synthesizeKey("V", {accelKey: true}); + is(t.value, "a\nb", "The carriage return has been correctly sanitized"); + SimpleTest.finish(); + }, + function() { + SimpleTest.finish(); + } + ); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug686203.html b/editor/libeditor/tests/test_bug686203.html new file mode 100644 index 0000000000..d27c3555fc --- /dev/null +++ b/editor/libeditor/tests/test_bug686203.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this file, + - You can obtain one at http://mozilla.org/MPL/2.0/. --> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=686203 +--> + +<head> + <title>Test for Bug 686203</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> + +<body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=686203">Mozilla Bug 686203</a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + + <pre id="test"> + <script type="application/javascript"> + + /** Test for Bug 686203 **/ + SimpleTest.waitForExplicitFinish(); + SimpleTest.waitForFocus(function() { + var ce = document.getElementById("ce"); + var input = document.getElementById("input"); + ce.focus(); + + var eventDetails = { button: 2 }; + synthesizeMouseAtCenter(input, eventDetails); + + sendString("Z"); + + /* check values */ + is(input.value, "Z", "input correctly focused after right-click"); + is(ce.textContent, "abc", "contenteditable correctly blurred after right-click on input"); + + SimpleTest.finish(); + }); + </script> + </pre> + + <input type="text" value="" id="input" /> + <div id="ce" contenteditable="true">abc</div> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug692520.html b/editor/libeditor/tests/test_bug692520.html new file mode 100644 index 0000000000..68c7d5d11c --- /dev/null +++ b/editor/libeditor/tests/test_bug692520.html @@ -0,0 +1,41 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=692520 +--> +<head> + <title>Test for Bug 692520</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=692520">Mozilla Bug 692520</a> +<p id="display"></p> +<div id="content"> +<textarea></textarea> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 692520 **/ +function test(prop, value) { + var t = document.querySelector("textarea"); + t.value = "testing"; + t.selectionStart = 1; + t.selectionEnd = 3; + t.selectionDirection = "backward"; + t.style.display = ""; + document.body.clientWidth; + t.style.display = "none"; + is(t[prop], value, "Correct value for the " + prop + " property"); +} + +test("selectionStart", 1); +test("selectionEnd", 3); +test("selectionDirection", "backward"); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug697842.html b/editor/libeditor/tests/test_bug697842.html new file mode 100644 index 0000000000..5b39abbace --- /dev/null +++ b/editor/libeditor/tests/test_bug697842.html @@ -0,0 +1,114 @@ +<!DOCTYPE> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=697842 +--> +<head> + <title>Test for Bug 697842</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> +</head> +<body> +<div id="display"> + <p id="editor" contenteditable style="min-height: 1.5em;"></p> +</div> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + +/** Test for Bug 697842 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + +function runTests() { + var editor = document.getElementById("editor"); + editor.focus(); + + SimpleTest.executeSoon(function() { + var composingString = ""; + + function handler(aEvent) { + switch (aEvent.type) { + case "compositionstart": + // Selected string at starting composition must be empty in this test. + is(aEvent.data, "", "mismatch selected string"); + break; + case "compositionupdate": + case "compositionend": + is(aEvent.data, composingString, "mismatch composition string"); + break; + default: + break; + } + aEvent.stopPropagation(); + aEvent.preventDefault(); + } + + editor.addEventListener("compositionstart", handler, true); + editor.addEventListener("compositionend", handler, true); + editor.addEventListener("compositionupdate", handler, true); + + // input first character + composingString = "\u306B"; + synthesizeCompositionChange( + { "composition": + { "string": composingString, + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }, + ], + }, + "caret": { "start": 1, "length": 0 }, + }); + + // input second character + composingString = "\u306B\u3085"; + synthesizeCompositionChange( + { "composition": + { "string": composingString, + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE }, + ], + }, + "caret": { "start": 2, "length": 0 }, + }); + + // convert them + synthesizeCompositionChange( + { "composition": + { "string": composingString, + "clauses": + [ + { "length": 2, + "attr": COMPOSITION_ATTR_SELECTED_CLAUSE }, + ], + }, + "caret": { "start": 2, "length": 0 }, + }); + + synthesizeComposition({ type: "compositioncommitasis" }); + + is(editor.innerHTML, composingString, + "editor has unexpected result"); + + editor.removeEventListener("compositionstart", handler, true); + editor.removeEventListener("compositionend", handler, true); + editor.removeEventListener("compositionupdate", handler, true); + editor.removeEventListener("text", handler, true); + + SimpleTest.finish(); + }); +} + + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_bug725069.html b/editor/libeditor/tests/test_bug725069.html new file mode 100644 index 0000000000..dbbca5a13a --- /dev/null +++ b/editor/libeditor/tests/test_bug725069.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=725069 +--> +<head> + <title>Test for Bug 725069</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body contenteditable>abc<!-- XXX -->def<span></span> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=725069">Mozilla Bug 725069</a> +<p id="display"></p> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 725069 **/ +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var body = document.querySelector("body"); + is(body.firstChild.nodeType, body.TEXT_NODE, "The first node is a text node"); + is(body.firstChild.nodeValue, "abc", "The first text node is there"); + is(body.firstChild.nextSibling.nodeType, body.COMMENT_NODE, "The second node is a comment node"); + is(body.firstChild.nextSibling.nodeValue, " XXX ", "The value of the comment node is not changed"); + is(body.firstChild.nextSibling.nextSibling.nodeType, body.TEXT_NODE, "The last text node is a text node"); + is(body.firstChild.nextSibling.nextSibling.nodeValue, "def", "The last next node is there"); + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug735059.html b/editor/libeditor/tests/test_bug735059.html new file mode 100644 index 0000000000..3b81ce48ba --- /dev/null +++ b/editor/libeditor/tests/test_bug735059.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=735059 +--> +<title>Test for Bug 735059</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=735059">Mozilla Bug 735059</a> +<div id="display" contenteditable>foo</div> +<pre id="test"> +<script> +/** Test for Bug 735059 **/ + +// Value defaults to the empty string, which evaluates to true, so this +// disables CSS styling +document.execCommand("usecss"); +getSelection().selectAllChildren(document.getElementById("display")); +document.execCommand("bold"); +is(document.getElementById("display").innerHTML, "<b>foo</b>", + "execCommand() needs to work with only one parameter"); +</script> +</pre> diff --git a/editor/libeditor/tests/test_bug738366.html b/editor/libeditor/tests/test_bug738366.html new file mode 100644 index 0000000000..a54aec7a2f --- /dev/null +++ b/editor/libeditor/tests/test_bug738366.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=738366 +--> +<title>Test for Bug 738366</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=738366">Mozilla Bug 738366</a> +<div id="display" contenteditable>foobarbaz</div> +<script> +/** Test for Bug 738366 **/ + +getSelection().collapse(document.getElementById("display").firstChild, 3); +getSelection().extend(document.getElementById("display").firstChild, 6); +document.execCommand("bold"); +is(document.getElementById("display").innerHTML, "foo<b>bar</b>baz", + "styleWithCSS must default to false"); +document.execCommand("stylewithcss", false, "true"); +document.execCommand("bold"); +document.execCommand("bold"); +is(document.getElementById("display").innerHTML, + 'foo<span style="font-weight: bold;">bar</span>baz', + "styleWithCSS must be settable to true"); +</script> diff --git a/editor/libeditor/tests/test_bug740784.html b/editor/libeditor/tests/test_bug740784.html new file mode 100644 index 0000000000..74e8374385 --- /dev/null +++ b/editor/libeditor/tests/test_bug740784.html @@ -0,0 +1,46 @@ +<!DOCTYPE HTML> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this file, + - You can obtain one at http://mozilla.org/MPL/2.0/. --> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=740784 +--> + +<head> + <title>Test for Bug 740784</title> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> +</head> + +<body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=740784">Mozilla Bug 740784</a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + + <pre id="test"> + <script type="application/javascript"> + + /** Test for Bug 740784 **/ + + SimpleTest.waitForExplicitFinish(); + SimpleTest.waitForFocus(function() { + var t1 = $("t1"); + + t1.focus(); + synthesizeKey("KEY_End"); + synthesizeKey("KEY_Backspace"); + synthesizeKey("z", {accelKey: true}); + + is(t1.value, "a", "trailing <br> correctly ignored"); + + SimpleTest.finish(); + }); + </script> + </pre> + + <textarea id="t1" rows="2" columns="80">a</textarea> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug742261.html b/editor/libeditor/tests/test_bug742261.html new file mode 100644 index 0000000000..9ad41dd52b --- /dev/null +++ b/editor/libeditor/tests/test_bug742261.html @@ -0,0 +1,14 @@ +<!doctype html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=742261 +--> +<title>Test for Bug 742261</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +<body> +<script> +is(document.execCommandShowHelp, undefined, + "execCommandShowHelp shouldn't exist"); +is(document.queryCommandText, undefined, + "queryCommandText shouldn't exist"); +</script> diff --git a/editor/libeditor/tests/test_bug757371.html b/editor/libeditor/tests/test_bug757371.html new file mode 100644 index 0000000000..5ca41a5951 --- /dev/null +++ b/editor/libeditor/tests/test_bug757371.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=757371 +--> +<title>Test for Bug 757371</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=757371">Mozilla Bug 757371</a> +<div contenteditable></div> +<script> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var div = document.querySelector("div"); + div.focus(); + getSelection().collapse(div, 0); + document.execCommand("bold"); + sendString("ab"); + sendKey("BACK_SPACE"); + sendChar("b"); + + is(div.innerHTML, "<b>ab</b>"); + + SimpleTest.finish(); +}); +</script> diff --git a/editor/libeditor/tests/test_bug757771.html b/editor/libeditor/tests/test_bug757771.html new file mode 100644 index 0000000000..9ef980b662 --- /dev/null +++ b/editor/libeditor/tests/test_bug757771.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=757771 +--> +<title>Test for Bug 757771</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=757771">Mozilla Bug 757771</a> +<input value=foo maxlength=4> +<input type=password value=password> +<script> +/** Test for Bug 757771 **/ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var textInput = document.querySelector("input"); + textInput.focus(); + textInput.select(); + sendString("abcde"); + + var passwordInput = document.querySelector("input + input"); + passwordInput.focus(); + passwordInput.select(); + sendString("hunter2"); + + ok(true, "No real tests, just crashes/asserts"); + + SimpleTest.finish(); +}); +</script> diff --git a/editor/libeditor/tests/test_bug772796.html b/editor/libeditor/tests/test_bug772796.html new file mode 100644 index 0000000000..fbfab347cf --- /dev/null +++ b/editor/libeditor/tests/test_bug772796.html @@ -0,0 +1,487 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=772796 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 772796</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style> .pre { white-space: pre } </style> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=772796">Mozilla Bug 772796</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> + +<div id="editable" contenteditable></div> + +<pre id="test"> + +<script type="application/javascript"> + var tests = [ +// FYI: Those tests were ported to join-pre-and-other-block.html +/* 00*/[ + "<div>test</div><pre>foobar\nbaz</pre>", + "<div>testfoobar</div><pre>baz</pre>", // expected +], +/* 01*/[ + "<div>test</div><pre><b>foobar\nbaz</b></pre>", + "<div>test<b>foobar</b></div><pre><b>baz</b></pre>", // expected +], +/* 02*/[ + "<div>test</div><pre><b>foo</b>bar\nbaz</pre>", + "<div>test<b>foo</b>bar</div><pre>baz</pre>", // expected +], +/* 03*/[ + "<div>test</div><pre><b>foo</b>\nbar</pre>", + "<div>test<b>foo</b></div><pre>bar</pre>", // expected +], +/* 04*/[ + "<div>test</div><pre><b>foo\n</b>bar\nbaz</pre>", + "<div>test<b>foo</b></div><pre>bar\nbaz</pre>", // expected +], + +// The <br> after the foobar is unfortunate but is behaviour that hasn't changed in bug 772796. +// FYI: Those tests were ported to join-pre-and-other-block.html +/* 05*/[ + "<div>test</div><pre>foobar<br>baz</pre>", + "<div>testfoobar</div><pre>baz</pre>", // expected +], +/* 06*/[ + "<div>test</div><pre><b>foobar<br>baz</b></pre>", + "<div>test<b>foobar</b></div><pre><b>baz</b></pre>", // expected +], + +// Some tests with block elements. +// FYI: Those tests were ported to join-pre-and-other-block.html +/* 07*/[ + "<div>test</div><pre><div>foobar</div>baz</pre>", + "<div>testfoobar</div><pre>baz</pre>", // expected +], +/* 08*/[ + "<div>test</div><pre>foobar<div>baz</div></pre>", + "<div>testfoobar</div><pre><div>baz</div></pre>", // expected +], +/* 09*/[ + "<div>test</div><pre><div>foobar</div>baz\nfred</pre>", + "<div>testfoobar</div><pre>baz\nfred</pre>", // expected +], +/* 10*/[ + "<div>test</div><pre>foobar<div>baz</div>\nfred</pre>", + "<div>testfoobar</div><pre><div>baz</div>\nfred</pre>", // expected +], +/* 11*/[ + "<div>test</div><pre><div>foo\nbar</div>baz\nfred</pre>", + "<div>testfoo</div><pre><div>bar</div>baz\nfred</pre>", // expected +], +/* 12*/[ + "<div>test</div><pre>foo<div>bar</div>baz\nfred</pre>", + "<div>testfoo</div><pre><div>bar</div>baz\nfred</pre>", // expected +], + +// Repeating all tests above with the <pre> on a new line. +// We know that backspace doesn't work (bug 1190161). Third argument shows the current outcome. +/* 13-00*/[ + "<div>test</div>\n<pre>foobar\nbaz</pre>", + "<div>testfoobar</div><pre>baz</pre>", // expected +], +/* 14-01*/[ + "<div>test</div>\n<pre><b>foobar\nbaz</b></pre>", + "<div>test<b>foobar</b></div><pre><b>baz</b></pre>", // expected +], +/* 15-02*/[ + "<div>test</div>\n<pre><b>foo</b>bar\nbaz</pre>", + "<div>test<b>foo</b>bar</div><pre>baz</pre>", // expected +], +/* 16-03*/[ + "<div>test</div>\n<pre><b>foo</b>\nbar</pre>", + "<div>test<b>foo</b></div><pre>bar</pre>", // expected +], +/* 17-04*/[ + "<div>test</div>\n<pre><b>foo\n</b>bar\nbaz</pre>", + "<div>test<b>foo</b></div><pre>bar\nbaz</pre>", // expected +], +/* 18-05*/[ + "<div>test</div>\n<pre>foobar<br>baz</pre>", + "<div>testfoobar</div><pre>baz</pre>", // expected +], +/* 19-06*/[ + "<div>test</div>\n<pre><b>foobar<br>baz</b></pre>", + "<div>test<b>foobar</b></div><pre><b>baz</b></pre>", // expected +], +/* 20-07*/[ + "<div>test</div>\n<pre><div>foobar</div>baz</pre>", + "<div>testfoobar</div><pre>baz</pre>", // expected +], +/* 21-08*/[ + "<div>test</div>\n<pre>foobar<div>baz</div></pre>", + "<div>testfoobar</div><pre><div>baz</div></pre>", // expected +], +/* 22-09*/[ + "<div>test</div>\n<pre><div>foobar</div>baz\nfred</pre>", + "<div>testfoobar</div><pre>baz\nfred</pre>", // expected +], +/* 23-10*/[ + "<div>test</div>\n<pre>foobar<div>baz</div>\nfred</pre>", + "<div>testfoobar</div><pre><div>baz</div>\nfred</pre>", // expected +], +/* 24-11*/[ + "<div>test</div>\n<pre><div>foo\nbar</div>baz\nfred</pre>", + "<div>testfoo</div><pre><div>bar</div>baz\nfred</pre>", // expected +], +/* 25-12*/[ + "<div>test</div>\n<pre>foo<div>bar</div>baz\nfred</pre>", + "<div>testfoo</div><pre><div>bar</div>baz\nfred</pre>", // expected +], + +// Some tests without <div>. These exercise the MoveBlock "right in left" +/* 26-00*/[ + "test<pre>foobar\nbaz</pre>", + "testfoobar<pre>baz</pre>", // expected +], +/* 27-01*/[ + "test<pre><b>foobar\nbaz</b></pre>", + "test<b>foobar</b><pre><b>baz</b></pre>", // expected +], +/* 28-02*/[ + "test<pre><b>foo</b>bar\nbaz</pre>", + "test<b>foo</b>bar<pre>baz</pre>", // expected +], +/* 29-03*/[ + "test<pre><b>foo</b>\nbar</pre>", + "test<b>foo</b><pre>bar</pre>", // expected +], +/* 30-04*/[ + "test<pre><b>foo\n</b>bar\nbaz</pre>", + "test<b>foo</b><pre>bar\nbaz</pre>", // expected +], +/* 31-05*/[ + "test<pre>foobar<br>baz</pre>", + "testfoobar<pre>baz</pre>", // expected +], +/* 32-06*/[ + "test<pre><b>foobar<br>baz</b></pre>", + "test<b>foobar</b><pre><b>baz</b></pre>", // expected +], +/* 33-07*/[ + "test<pre><div>foobar</div>baz</pre>", + "testfoobar<pre>baz</pre>", // expected +], +/* 34-08*/[ + "test<pre>foobar<div>baz</div></pre>", + "testfoobar<pre><div>baz</div></pre>", // expected +], +/* 35-09*/[ + "test<pre><div>foobar</div>baz\nfred</pre>", + "testfoobar<pre>baz\nfred</pre>", // expected +], +/* 36-10*/[ + "test<pre>foobar<div>baz</div>\nfred</pre>", + "testfoobar<pre><div>baz</div>\nfred</pre>", // expected +], +/* 37-11*/[ + "test<pre><div>foo\nbar</div>baz\nfred</pre>", + "testfoo<pre><div>bar</div>baz\nfred</pre>", // expected +], +/* 38-12*/[ + "test<pre>foo<div>bar</div>baz\nfred</pre>", + "testfoo<pre><div>bar</div>baz\nfred</pre>", // expected +], + +// Some tests with <span class="pre"> +// All these exercise MoveBlock "left in right". The "right" is the surrounding "contenteditable" div. +// FYI: Those tests except the cases <span> having <div> were ported to +// join-different-white-space-style-left-paragraph-and-right-line.html +/* 39-00*/[ + "<div>test</div><span class=\"pre\">foobar\nbaz</span>", + "<div>test<span class=\"pre\" style=\"white-space: pre;\">foobar</span></div><span class=\"pre\">baz</span>", // expected +], +/* 40-01*/[ + "<div>test</div><span class=\"pre\"><b>foobar\nbaz</b></span>", + "<div>test<span class=\"pre\" style=\"white-space: pre;\"><b>foobar</b></span></div><span class=\"pre\"><b>baz</b></span>", // expected +], +/* 41-02*/[ + "<div>test</div><span class=\"pre\"><b>foo</b>bar\nbaz</span>", + "<div>test<span class=\"pre\" style=\"white-space: pre;\"><b>foo</b>bar</span></div><span class=\"pre\">baz</span>", // expected +], +/* 42-03*/[ + "<div>test</div><span class=\"pre\"><b>foo</b>\nbar</span>", + "<div>test<span class=\"pre\" style=\"white-space: pre;\"><b>foo</b></span></div><span class=\"pre\">bar</span>", // expected +], +/* 43-04*/[ + "<div>test</div><span class=\"pre\"><b>foo\n</b>bar\nbaz</span>", + "<div>test<span class=\"pre\" style=\"white-space: pre;\"><b>foo</b></span></div><span class=\"pre\">bar\nbaz</span>", // expected +], +/* 44-05*/[ + "<div>test</div><span class=\"pre\">foobar<br>baz</span>", + "<div>test<span class=\"pre\" style=\"white-space: pre;\">foobar</span></div><span class=\"pre\">baz</span>", // expected +], +/* 45-06*/[ + "<div>test</div><span class=\"pre\"><b>foobar<br>baz</b></span>", + "<div>test<span class=\"pre\" style=\"white-space: pre;\"><b>foobar</b></span></div><span class=\"pre\"><b>baz</b></span>", // expected +], +/* 46-07*/[ + "<div>test</div><span class=\"pre\"><div>foobar</div>baz</span>", + "<div>test<span class=\"pre\" style=\"white-space: pre;\"><div>foobar</div></span></div><span class=\"pre\">baz</span>", // expected +], +/* 47-08*/[ + "<div>test</div><span class=\"pre\">foobar<div>baz</div></span>", + "<div>test<span class=\"pre\" style=\"white-space: pre;\">foobar</span></div><span class=\"pre\"><div>baz</div></span>", // expected +], +/* 48-09*/[ + "<div>test</div><span class=\"pre\"><div>foobar</div>baz\nfred</span>", + "<div>test<span class=\"pre\" style=\"white-space: pre;\"><div>foobar</div></span></div><span class=\"pre\">baz\nfred</span>", // expected +], +/* 49-10*/[ + "<div>test</div><span class=\"pre\">foobar<div>baz</div>\nfred</span>", + "<div>test<span class=\"pre\" style=\"white-space: pre;\">foobar</span></div><span class=\"pre\"><div>baz</div>\nfred</span>", // expected +], +/* 50-11*/[ + "<div>test</div><span class=\"pre\"><div>foo\nbar</div>baz\nfred</span>", + "<div>test<span style=\"white-space: pre;\">foo</span></div><span class=\"pre\"><div>bar</div>baz\nfred</span>", // expected +], +/* 51-12*/[ + "<div>test</div><span class=\"pre\">foo<div>bar</div>baz\nfred</span>", + "<div>test<span class=\"pre\" style=\"white-space: pre;\">foo</span></div><span class=\"pre\"><div>bar</div>baz\nfred</span>", // expected +], + +// Some tests with <div class="pre">. +// FYI: Those tests were ported to join-different-white-space-style-paragraphs.html +/* 52-00*/[ + "<div>test</div><div class=\"pre\">foobar\nbaz</div>", + "<div>test<span style=\"white-space: pre;\">foobar</span></div><div class=\"pre\">baz</div>", // expected +], +/* 53-01*/[ + "<div>test</div><div class=\"pre\"><b>foobar\nbaz</b></div>", + "<div>test<b style=\"white-space: pre;\">foobar</b></div><div class=\"pre\"><b>baz</b></div>", // expected +], +/* 54-02*/[ + "<div>test</div><div class=\"pre\"><b>foo</b>bar\nbaz</div>", + "<div>test<b style=\"white-space: pre;\">foo</b><span style=\"white-space: pre;\">bar</span></div><div class=\"pre\">baz</div>", // expected +], +/* 55-03*/[ + "<div>test</div><div class=\"pre\"><b>foo</b>\nbar</div>", + "<div>test<b style=\"white-space: pre;\">foo</b></div><div class=\"pre\">bar</div>", // expected +], +/* 56-04*/[ + "<div>test</div><div class=\"pre\"><b>foo\n</b>bar\nbaz</div>", + "<div>test<b style=\"white-space: pre;\">foo</b></div><div class=\"pre\">bar\nbaz</div>", // expected +], +/* 57-05*/[ + "<div>test</div><div class=\"pre\">foobar<br>baz</div>", + "<div>test<span style=\"white-space: pre;\">foobar</span></div><div class=\"pre\">baz</div>", // expected +], +/* 58-06*/[ + "<div>test</div><div class=\"pre\"><b>foobar<br>baz</b></div>", + "<div>test<b style=\"white-space: pre;\">foobar</b></div><div class=\"pre\"><b>baz</b></div>", // expected +], +/* 59-07*/[ + "<div>test</div><div class=\"pre\"><div>foobar</div>baz</div>", + "<div>test<span style=\"white-space: pre;\">foobar</span></div><div class=\"pre\">baz</div>", // expected +], +/* 60-08*/[ + "<div>test</div><div class=\"pre\">foobar<div>baz</div></div>", + "<div>test<span style=\"white-space: pre;\">foobar</span></div><div class=\"pre\"><div>baz</div></div>", // expected +], +/* 61-09*/[ + "<div>test</div><div class=\"pre\"><div>foobar</div>baz\nfred</div>", + "<div>test<span style=\"white-space: pre;\">foobar</span></div><div class=\"pre\">baz\nfred</div>", // expected +], +/* 62-10*/[ + "<div>test</div><div class=\"pre\">foobar<div>baz</div>\nfred</div>", + "<div>test<span style=\"white-space: pre;\">foobar</span></div><div class=\"pre\"><div>baz</div>\nfred</div>", // expected +], +/* 63-11*/[ + "<div>test</div><div class=\"pre\"><div>foo\nbar</div>baz\nfred</div>", + "<div>test<span style=\"white-space: pre;\">foo</span></div><div class=\"pre\"><div>bar</div>baz\nfred</div>", // expected +], +/* 64-12*/[ + "<div>test</div><div class=\"pre\">foo<div>bar</div>baz\nfred</div>", + "<div>test<span style=\"white-space: pre;\">foo</span></div><div class=\"pre\"><div>bar</div>baz\nfred</div>", // expected +], + +// Some tests with lists. These exercise the MoveBlock "left in right". +/* 65*/[ + "<ul><pre><li>test</li>foobar\nbaz</pre></ul>", + "<ul><pre><li>testfoobar</li>baz</pre></ul>", // expected +], +/* 66*/[ + "<ul><pre><li>test</li><b>foobar\nbaz</b></pre></ul>", + "<ul><pre><li>test<b>foobar</b></li><b>baz</b></pre></ul>", // expected +], +/* 67*/[ + "<ul><pre><li>test</li><b>foo</b>bar\nbaz</pre></ul>", + "<ul><pre><li>test<b>foo</b>bar</li>baz</pre></ul>", // expected +], +/* 68*/[ + "<ul><pre><li>test</li><b>foo</b>\nbar</pre></ul>", + "<ul><pre><li>test<b>foo</b></li>bar</pre></ul>", // expected +], +/* 69*/[ + "<ul><pre><li>test</li><b>foo\n</b>bar\nbaz</pre></ul>", + "<ul><pre><li>test<b>foo</b></li>bar\nbaz</pre></ul>", // expected +], + +// Last not least, some simple edge case tests. +// FYI: Those tests were ported to join-pre-and-other-block.html +/* 70*/[ + "<div>test</div><pre>foobar\n</pre>baz", + "<div>testfoobar</div>baz", // expected +], +/* 71*/[ + "<div>test</div><pre>\nfoo\nbar</pre>", + "<div>testfoo</div><pre>bar</pre>", // expected +], +/* 72*/[ + "<div>test</div><pre>\n\nfoo\nbar</pre>", + "<div>test</div><pre>foo\nbar</pre>", // expected +], +]; + + /** Test for Bug 772796 **/ + + SimpleTest.waitForExplicitFinish(); + SimpleTest.waitForFocus(function() { + var sel = window.getSelection(); + var theEdit = document.getElementById("editable"); + var testName; + var theDiv; + + for (let i = 0; i < tests.length; i++) { + testName = "test" + i.toString(); + + /* Set up the selection. */ + theEdit.innerHTML = "<div id=\"" + testName + "\">" + tests[i][0] + "</div>"; + theDiv = document.getElementById(testName); + theDiv.focus(); + sel.collapse(theDiv, 0); + synthesizeMouse(theDiv, 100, 2, {}); /* click behind and down */ + + function normalizeStyeAttributeValues(aElement) { + for (const element of Array.from( + aElement.querySelectorAll("[style]") + )) { + element.setAttribute( + "style", + element + .getAttribute("style") + // Random spacing differences + .replace(/$/, ";") + .replace(/;;$/, ";") + // Gecko likes "transparent" + .replace(/transparent/g, "rgba(0, 0, 0, 0)") + // WebKit likes to look overly precise + .replace(/, 0.496094\)/g, ", 0.5)") + // Gecko converts anything with full alpha to "transparent" which + // then becomes "rgba(0, 0, 0, 0)", so we have to make other + // browsers match + .replace(/rgba\([0-9]+, [0-9]+, [0-9]+, 0\)/g, "rgba(0, 0, 0, 0)") + ); + } + } + + let todoCount = 0; + /** First round: Forward delete. **/ + synthesizeKey("KEY_Delete"); + normalizeStyeAttributeValues(theDiv); + if (tests[i].length == 2 || theDiv.innerHTML == tests[i][1]) { + is(theDiv.innerHTML, tests[i][1], "delete(collapsed): inner HTML for " + testName); + } else { + todoCount++; + todo_is(theDiv.innerHTML, tests[i][1], "delete(should be): inner HTML for " + testName); + is(theDiv.innerHTML, tests[i][2], "delete(currently is): inner HTML for " + testName); + } + + /* Set up the selection. */ + theEdit.innerHTML = "<div id=\"" + testName + "\">" + tests[i][0] + "</div>"; + theDiv = document.getElementById(testName); + theDiv.focus(); + sel.collapse(theDiv, 0); + synthesizeMouse(theDiv, 100, 2, {}); /* click behind and down */ + + /** Second round: Backspace. **/ + synthesizeKey("KEY_ArrowRight"); + synthesizeKey("KEY_Backspace"); + normalizeStyeAttributeValues(theDiv); + if (tests[i].length == 2 || theDiv.innerHTML == tests[i][1]) { + is(theDiv.innerHTML, tests[i][1], "backspace: inner HTML for " + testName); + } else { + todoCount++; + todo_is(theDiv.innerHTML, tests[i][1], "backspace(should be): inner HTML for " + testName); + is(theDiv.innerHTML, tests[i][2], "backspace(currently is): inner HTML for " + testName); + } + + /* Set up the selection. */ + theEdit.innerHTML = "<div id=\"" + testName + "\">" + tests[i][0] + "</div>"; + theDiv = document.getElementById(testName); + theDiv.focus(); + sel.collapse(theDiv, 0); + synthesizeMouse(theDiv, 100, 2, {}); /* click behind and down */ + + /** Third round: Delete with non-collapsed selection. **/ + if (i == 72) { + if (tests[i].length == 3) { + ok(!!todoCount, `All tests unexpectedly passed in ${testName}`); + } + // This test doesn't work, since we can't select only one newline using the right arrow key. + continue; + } + synthesizeKey("KEY_ArrowLeft"); + /* Strangely enough we need to hit "right arrow" three times to select two characters. */ + synthesizeKey("KEY_ArrowRight", {shiftKey: true}); + synthesizeKey("KEY_ArrowRight", {shiftKey: true}); + synthesizeKey("KEY_ArrowRight", {shiftKey: true}); + synthesizeKey("KEY_Delete"); + normalizeStyeAttributeValues(theDiv); + + /* We always expect to the delete the "tf" in "testfoo". */ + function makeNonCollapsedExpectation(aExpected) { + return aExpected + .replace("testfoo", + "tesoo") + .replace("test<b>foo", + "tes<b>oo") + .replace("test<b style=\"white-space: pre;\">foo", + "tes<b style=\"white-space: pre;\">oo") + .replace("test<span style=\"white-space: pre;\">foo", + "tes<span style=\"white-space: pre;\">oo") + .replace("test<span style=\"white-space: pre;\"><b>foo", + "tes<span style=\"white-space: pre;\"><b>oo") + .replace("test<span style=\"white-space: pre;\"><div>foo", + "tes<span style=\"white-space: pre;\"><div>oo") + .replace("test<span class=\"pre\" style=\"white-space: pre;\">foo", + "tes<span class=\"pre\" style=\"white-space: pre;\">oo") + .replace("test<span class=\"pre\" style=\"white-space: pre;\"><b>foo", + "tes<span class=\"pre\" style=\"white-space: pre;\"><b>oo") + .replace("test<span class=\"pre\" style=\"white-space: pre;\"><div>foo", + "tes<span class=\"pre\" style=\"white-space: pre;\"><div>oo"); + } + const expected = makeNonCollapsedExpectation(tests[i][1]); + if (tests[i].length == 2 || theDiv.innerHTML == expected) { + is(theDiv.innerHTML, expected, "delete(non-collapsed): inner HTML for " + testName); + } else { + todoCount++; + todo_is(theDiv.innerHTML, expected, "delete(non-collapsed, should be): inner HTML for " + testName); + is( + theDiv.innerHTML, + makeNonCollapsedExpectation(tests[i][2]), + "delete(non-collapsed, currently is): inner HTML for " + testName + ); + } + if (tests[i].length == 3) { + ok(!!todoCount, `All tests unexpectedly passed in ${testName}`); + } + } + + SimpleTest.finish(); + }); + + </script> + +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug773262.html b/editor/libeditor/tests/test_bug773262.html new file mode 100644 index 0000000000..b0dc827559 --- /dev/null +++ b/editor/libeditor/tests/test_bug773262.html @@ -0,0 +1,63 @@ +<!doctype html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=773262 +--> +<title>Test for Bug 773262</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +<p><a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=773262">Mozilla Bug 773262</a></p> +<iframe></iframe> +<script> +function runTest(doc, desc) { + is(doc.queryCommandEnabled("undo"), false, + desc + ": Undo shouldn't be enabled yet"); + is(doc.queryCommandEnabled("redo"), false, + desc + ": Redo shouldn't be enabled yet"); + is(doc.body.innerHTML, "<p>Hello</p>", desc + ": Wrong initial innerHTML"); + + doc.getSelection().selectAllChildren(doc.body.firstChild); + doc.execCommand("bold"); + is(doc.queryCommandEnabled("undo"), true, + desc + ": Undo should be enabled after bold"); + is(doc.queryCommandEnabled("redo"), false, + desc + ": Redo still shouldn't be enabled"); + is(doc.body.innerHTML, "<p><b>Hello</b></p>", + desc + ": Wrong innerHTML after bold"); + + doc.execCommand("undo"); + is(doc.queryCommandEnabled("undo"), false, + desc + ": Undo should be disabled again"); + is(doc.queryCommandEnabled("redo"), true, + desc + ": Redo should be enabled now"); + is(doc.body.innerHTML, "<p>Hello</p>", + desc + ": Wrong innerHTML after undo"); + + doc.execCommand("redo"); + is(doc.queryCommandEnabled("undo"), true, + desc + ": Undo should be enabled after redo"); + is(doc.queryCommandEnabled("redo"), false, + desc + ": Redo should be disabled again"); + is(doc.body.innerHTML, "<p><b>Hello</b></p>", + desc + ": Wrong innerHTML after redo"); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var doc = document.querySelector("iframe").contentDocument; + + // First turn on designMode and run the test like that, as a sanity check. + doc.body.innerHTML = "<p>Hello</p>"; + doc.designMode = "on"; + runTest(doc, "1"); + + // Now to test the actual bug: repeat all the above, but with designMode + // toggled. This should clear the undo history, so everything should be + // exactly as before. + doc.designMode = "off"; + doc.body.innerHTML = "<p>Hello</p>"; + doc.designMode = "on"; + runTest(doc, "2"); + + SimpleTest.finish(); +}); +</script> diff --git a/editor/libeditor/tests/test_bug780035.html b/editor/libeditor/tests/test_bug780035.html new file mode 100644 index 0000000000..9992314888 --- /dev/null +++ b/editor/libeditor/tests/test_bug780035.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=780035 +--> +<title>Test for Bug 780035</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=780035">Mozilla Bug 780035</a> +<div contenteditable style="font-size: 13.3333px"></div> +<script> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + document.querySelector("div").focus(); + document.execCommand("stylewithcss", false, true); + document.execCommand("defaultParagraphSeparator", false, "div"); + sendKey("RETURN"); + sendChar("x"); + is(document.querySelector("div").innerHTML, + "<div><br></div><div>x<br></div>", "No <font> tag should be generated"); + SimpleTest.finish(); +}); +</script> diff --git a/editor/libeditor/tests/test_bug780908.xhtml b/editor/libeditor/tests/test_bug780908.xhtml new file mode 100644 index 0000000000..590316ef46 --- /dev/null +++ b/editor/libeditor/tests/test_bug780908.xhtml @@ -0,0 +1,110 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" + type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=780908 + +adapted from test_bug607584.xhtml by Kent James <kent@caspia.com> +--> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Mozilla Bug 780908" onload="runTest();"> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=780908" + target="_blank">Mozilla Bug 780908</a> + <p/> + <editor xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + id="editor" + type="content" + primary="true" + editortype="html" + style="width: 400px; height: 100px; border: thin solid black"/> + <p/> + <pre id="test"> + </pre> + </body> + <script class="testbody" type="application/javascript"> + <![CDATA[ + + SimpleTest.waitForExplicitFinish(); + + function EditorContentListener(aEditor) + { + this.init(aEditor); + } + + EditorContentListener.prototype = { + init(aEditor) + { + this.mEditor = aEditor; + }, + + QueryInterface: ChromeUtils.generateQI(["nsIWebProgressListener", + "nsISupportsWeakReference"]), + + onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) + { + if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) + { + var editor = this.mEditor.getEditor(this.mEditor.contentWindow); + if (editor) { + this.mEditor.focus(); + editor instanceof Ci.nsIHTMLEditor; + editor.returnInParagraphCreatesNewParagraph = true; + let source = "<html><body><table><head></table></body></html>"; + editor.rebuildDocumentFromSource(source); + ok(true, "Don't crash when head appears after body"); + source = "<html></head><head><body></body></html>"; + editor.rebuildDocumentFromSource(source); + ok(true, "Don't crash when /head appears before head"); + SimpleTest.finish(); + progress.removeProgressListener(this); + } + } + + }, + + + onProgressChange(aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, + aCurTotalProgress, aMaxTotalProgress) + { + }, + + onLocationChange(aWebProgress, aRequest, aLocation, aFlags) + { + }, + + onStatusChange(aWebProgress, aRequest, aStatus, aMessage) + { + }, + + onSecurityChange(aWebProgress, aRequest, aState) + { + }, + + onContentBlockingEvent(aWebProgress, aRequest, aEvent) + { + }, + + mEditor: null + }; + + var progress, progressListener; + + function runTest() { + var newEditorElement = document.getElementById("editor"); + newEditorElement.makeEditable("html", true); + var docShell = newEditorElement.docShell; + progress = docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebProgress); + progressListener = new EditorContentListener(newEditorElement); + progress.addProgressListener(progressListener, Ci.nsIWebProgress.NOTIFY_ALL); + newEditorElement.setAttribute("src", "data:text/html,"); + } +]]> +</script> +</window> diff --git a/editor/libeditor/tests/test_bug787432.html b/editor/libeditor/tests/test_bug787432.html new file mode 100644 index 0000000000..c73bb3c7ea --- /dev/null +++ b/editor/libeditor/tests/test_bug787432.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=787432 +--> +<title>Test for Bug 787432</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=787432">Mozilla Bug 787432</a> +<div id="test" contenteditable><span class="insert">%</span><br></div> +<script> +var div = document.getElementById("test"); +getSelection().collapse(div.firstChild, 0); +getSelection().extend(div.firstChild, 1); +document.execCommand("inserttext", false, "x"); +is(div.innerHTML, '<span class="insert">x</span><br>', + "Empty <span> needs to not be removed"); +</script> diff --git a/editor/libeditor/tests/test_bug790475.html b/editor/libeditor/tests/test_bug790475.html new file mode 100644 index 0000000000..d30b14b312 --- /dev/null +++ b/editor/libeditor/tests/test_bug790475.html @@ -0,0 +1,90 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=790475 +--> +<head> + <title>Test for Bug 790475</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=790475">Mozilla Bug 790475</a> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +<script type="application/javascript"> + +/** + * Test for Bug 790475 + * + * Tests that inline spell checking works properly through adjacent text nodes. + */ + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTest); + +var gMisspeltWords; + +function getEditor() { + var editingSession = SpecialPowers.wrap(window).docShell.editingSession; + return editingSession.getEditorForWindow(window); +} + +function getSpellCheckSelection() { + var editor = getEditor(); + var selcon = editor.selectionController; + return selcon.getSelection(selcon.SELECTION_SPELLCHECK); +} + +function runTest() { + gMisspeltWords = []; + var edit = document.getElementById("edit"); + edit.focus(); + + SimpleTest.executeSoon(function() { + gMisspeltWords = []; + is(isSpellingCheckOk(), true, "Should not find any misspellings yet."); + + var newTextNode = document.createTextNode("ing string"); + edit.appendChild(newTextNode); + var editor = getEditor(); + var sel = editor.selection; + sel.collapse(newTextNode, newTextNode.textContent.length); + sendString("!"); + + edit.blur(); + + SimpleTest.executeSoon(function() { + is(isSpellingCheckOk(), true, "Should not have found any misspellings. "); + SimpleTest.finish(); + }); + }); +} + +function isSpellingCheckOk() { + var sel = getSpellCheckSelection(); + var numWords = sel.rangeCount; + + is(numWords, gMisspeltWords.length, "Correct number of misspellings and words."); + + if (numWords != gMisspeltWords.length) + return false; + + for (var i = 0; i < numWords; i++) { + var word = sel.getRangeAt(i); + is(word, gMisspeltWords[i], "Misspelling is what we think it is."); + if (word != gMisspeltWords[i]) + return false; + } + return true; +} + +</script> +</pre> + +<div id="edit" contenteditable="true">This is a test</div> + +</body> +</html> diff --git a/editor/libeditor/tests/test_bug795418-2.html b/editor/libeditor/tests/test_bug795418-2.html new file mode 100644 index 0000000000..66330b4f79 --- /dev/null +++ b/editor/libeditor/tests/test_bug795418-2.html @@ -0,0 +1,87 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=795418 +--> +<head> + <meta charset="utf-8"> + <title>Test #2 for Bug 772796</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=772796">Mozilla Bug 795418</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> + +<div id="copySource">Copy this</div> +<!-- load content of type application/xhtml+xml using an *.sjs file --> +<iframe src="./file_bug795418-2.sjs"></iframe> + +<pre id="test"> + +<script type="application/javascript"> + +/** Test for Bug 795418 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + const div = document.getElementById("copySource"); + getSelection().setBaseAndExtent(div.firstChild, 0, div.firstChild, "Copy this".length); + info(`Selected test: "${getSelection().getRangeAt(0).toString()}"`); + + function checkResult() { + var iframe = document.querySelector("iframe"); + var theEdit = iframe.contentDocument.firstChild; + theEdit.offsetHeight; + is(theEdit.innerHTML, + "<blockquote xmlns=\"http://www.w3.org/1999/xhtml\" type=\"cite\">Copy this</blockquote><span xmlns=\"http://www.w3.org/1999/xhtml\">AB</span>", + "unexpected HTML for test"); + SimpleTest.finish(); + } + + function pasteQuote() { + var iframe = document.querySelector("iframe"); + var iframeWindow = iframe.contentWindow; + var theEdit = iframe.contentDocument.firstChild; + theEdit.offsetHeight; + iframeWindow.focus(); + SimpleTest.waitForFocus(function() { + var iframeSel = iframeWindow.getSelection(); + iframeSel.removeAllRanges(); + let span = iframe.contentDocument.querySelector("span"); + iframeSel.collapse(span, 1); + + SpecialPowers.doCommand(iframeWindow, "cmd_pasteQuote"); + setTimeout(checkResult, 0); + }, iframeWindow); + } + + SimpleTest.waitForClipboard( + aData => { + if (aData.includes(`${getSelection().getRangeAt(0)?.toString()}`)) { + return true; + } + info(`Text in the clipboard: "${aData}"`); + return false; + }, + function setup() { + synthesizeKey("c", {accelKey: true}); + }, + function onSuccess() { + SimpleTest.executeSoon(pasteQuote); + }, + function onFailure() { + SimpleTest.finish(); + }, + // TODO: bug 1686012 + SpecialPowers.isHeadless ? "text/plain" : "text/html" + ); +}); + +</script> + +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug795418-3.html b/editor/libeditor/tests/test_bug795418-3.html new file mode 100644 index 0000000000..aae076069f --- /dev/null +++ b/editor/libeditor/tests/test_bug795418-3.html @@ -0,0 +1,89 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=795418 +--> +<head> + <meta charset="utf-8"> + <title>Test #3 for Bug 772796</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=772796">Mozilla Bug 795418</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> + +<div id="copySource">Copy this</div> +<iframe srcdoc="<html><body><span>AB</span>"></iframe> + +<pre id="test"> + +<script type="application/javascript"> + +/** Test for Bug 795418 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var div = document.getElementById("copySource"); + var sel = window.getSelection(); + sel.removeAllRanges(); + + // Select the text from the text node in div. + var r = document.createRange(); + r.setStart(div.firstChild, 0); + r.setEnd(div.firstChild, 9); + sel.addRange(r); + + function checkResult() { + var iframe = document.querySelector("iframe"); + var theEdit = iframe.contentDocument.body; + theEdit.offsetHeight; + is(theEdit.innerHTML, + "<span>AB<blockquote type=\"cite\">Copy this</blockquote></span>", + "unexpected HTML for test"); + SimpleTest.finish(); + } + + function pasteQuote() { + var iframe = document.querySelector("iframe"); + var iframeWindow = iframe.contentWindow; + var theEdit = iframe.contentDocument.body; + iframe.contentDocument.designMode = "on"; + iframe.contentDocument.body.offsetHeight; + iframeWindow.focus(); + SimpleTest.waitForFocus(function() { + var iframeSel = iframeWindow.getSelection(); + iframeSel.removeAllRanges(); + iframeSel.collapse(theEdit.firstChild, 1); + + SpecialPowers.doCommand(iframeWindow, "cmd_pasteQuote"); + setTimeout(checkResult, 0); + }, iframeWindow); + } + + SimpleTest.waitForClipboard( + aData => { + // XXX Oddly, specifying `r.toString()` causes timeout in headless mode. + info(`copied text: "${aData}"`); + return true; + }, + function setup() { + synthesizeKey("C", {accelKey: true}); + }, + function onSuccess() { + setTimeout(pasteQuote, 0); + }, + function onFailure() { + SimpleTest.finish(); + }, + "text/html" + ); +}); + +</script> + +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug795418-4.html b/editor/libeditor/tests/test_bug795418-4.html new file mode 100644 index 0000000000..1c71ee5894 --- /dev/null +++ b/editor/libeditor/tests/test_bug795418-4.html @@ -0,0 +1,70 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=795418 +--> +<head> + <meta charset="utf-8"> + <title>Test #4 for Bug 795418</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=795418">Mozilla Bug 795418</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> + +<div id="copySource">Copy this</div> +<div id="editable" contenteditable style="display:grid">AB</div> + +<pre id="test"> + +<script type="application/javascript"> + +/** Test for Bug 795418 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var div = document.getElementById("copySource"); + var sel = window.getSelection(); + sel.removeAllRanges(); + + // Select the text from the text node in div. + var r = document.createRange(); + r.setStart(div.firstChild, 0); + r.setEnd(div.firstChild, 9); + sel.addRange(r); + + SimpleTest.waitForClipboard( + aData => { + // XXX Oddly, specifying `r.toString()` causes timeout in headless mode. + info(`copied text: "${aData}"`); + return true; + }, + function setup() { + synthesizeKey("C", {accelKey: true}); + }, + function onSuccess() { + var theEdit = document.getElementById("editable"); + sel.collapse(theEdit.firstChild, 2); + + SpecialPowers.doCommand(window, "cmd_paste"); + is(theEdit.innerHTML, + "ABCopy this", + "unexpected HTML for test"); + + SimpleTest.finish(); + }, + function onFailure() { + SimpleTest.finish(); + }, + "text/html" + ); +}); + +</script> + +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug795418-5.html b/editor/libeditor/tests/test_bug795418-5.html new file mode 100644 index 0000000000..562a225f3e --- /dev/null +++ b/editor/libeditor/tests/test_bug795418-5.html @@ -0,0 +1,69 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=795418 +--> +<head> + <meta charset="utf-8"> + <title>Test #5 for Bug 795418</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=795418">Mozilla Bug 795418</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> + +<div id="copySource">Copy this</div> +<div id="editable" contenteditable style="display:ruby">AB</div> + +<pre id="test"> + +<script type="application/javascript"> + +/** Test for Bug 795418 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var div = document.getElementById("copySource"); + var sel = window.getSelection(); + sel.removeAllRanges(); + + // Select the text from the text node in div. + var r = document.createRange(); + r.setStart(div.firstChild, 0); + r.setEnd(div.firstChild, 9); + sel.addRange(r); + + SimpleTest.waitForClipboard( + aData => { + // XXX Oddly, specifying `r.toString()` causes timeout in headless mode. + info(`copied text: "${aData}"`); + return true; + }, + function setup() { + synthesizeKey("C", {accelKey: true}); + }, + function onSuccess() { + var theEdit = document.getElementById("editable"); + sel.collapse(theEdit.firstChild, 2); + + SpecialPowers.doCommand(window, "cmd_paste"); + is(theEdit.innerHTML, + "ABCopy this", + "unexpected HTML for test"); + SimpleTest.finish(); + }, + function onFailure() { + SimpleTest.finish(); + }, + "text/html" + ); +}); + +</script> + +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug795418-6.html b/editor/libeditor/tests/test_bug795418-6.html new file mode 100644 index 0000000000..ea6a62612b --- /dev/null +++ b/editor/libeditor/tests/test_bug795418-6.html @@ -0,0 +1,69 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=795418 +--> +<head> + <meta charset="utf-8"> + <title>Test #5 for Bug 795418</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=795418">Mozilla Bug 795418</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> + +<div id="copySource">Copy this</div> +<div id="editable" contenteditable style="display:table">AB</div> + +<pre id="test"> + +<script type="application/javascript"> + +/** Test for Bug 795418 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var div = document.getElementById("copySource"); + var sel = window.getSelection(); + sel.removeAllRanges(); + + // Select the text from the text node in div. + var r = document.createRange(); + r.setStart(div.firstChild, 0); + r.setEnd(div.firstChild, 9); + sel.addRange(r); + + SimpleTest.waitForClipboard( + aData => { + // XXX Oddly, specifying `r.toString()` causes timeout in headless mode. + info(`copied text: "${aData}"`); + return true; + }, + function setup() { + synthesizeKey("C", {accelKey: true}); + }, + function onSuccess() { + var theEdit = document.getElementById("editable"); + sel.collapse(theEdit.firstChild, 2); + + SpecialPowers.doCommand(window, "cmd_pasteQuote"); + is(theEdit.innerHTML, + "AB<blockquote type=\"cite\">Copy this</blockquote>", + "unexpected HTML for test"); + SimpleTest.finish(); + }, + function onFailure() { + SimpleTest.finish(); + }, + "text/html" + ); +}); + +</script> + +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug795418.html b/editor/libeditor/tests/test_bug795418.html new file mode 100644 index 0000000000..8e02d0b49f --- /dev/null +++ b/editor/libeditor/tests/test_bug795418.html @@ -0,0 +1,70 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=795418 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 795418</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=795418">Mozilla Bug 795418</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> + +<div id="copySource">Copy this</div> +<div id="editable" contenteditable><span>AB</span></div> + +<pre id="test"> + +<script type="application/javascript"> + +/** Test for Bug 795418 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var div = document.getElementById("copySource"); + var sel = window.getSelection(); + sel.removeAllRanges(); + + // Select the text from the text node in div. + var r = document.createRange(); + r.setStart(div.firstChild, 0); + r.setEnd(div.firstChild, 9); + sel.addRange(r); + + SimpleTest.waitForClipboard( + aData => { + // XXX Oddly, specifying `r.toString()` causes timeout in headless mode. + info(`copied text: "${aData}"`); + return true; + }, + function setup() { + synthesizeKey("C", {accelKey: true}); + }, + function onSuccess() { + var theEdit = document.getElementById("editable"); + sel.collapse(theEdit.firstChild, 1); + + SpecialPowers.doCommand(window, "cmd_pasteQuote"); + is(theEdit.innerHTML, + "<span>AB<blockquote type=\"cite\">Copy this</blockquote></span>", + "unexpected HTML for test"); + + SimpleTest.finish(); + }, + function onFailure() { + SimpleTest.finish(); + }, + "text/html" + ); +}); + +</script> + +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug795785.html b/editor/libeditor/tests/test_bug795785.html new file mode 100644 index 0000000000..3e56207b63 --- /dev/null +++ b/editor/libeditor/tests/test_bug795785.html @@ -0,0 +1,139 @@ +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=795785 +--> +<head> + <title>Test for Bug 795785</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=795785">Mozilla Bug 795785</a> +<div id="display"> + <textarea style="overflow: hidden; height: 3em; width: 5em; word-wrap: normal;"></textarea> + <div contenteditable style="overflow: hidden; height: 3em; width: 5em;"></div> +</div> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<script> +"use strict"; + +function waitForScroll() { + return waitToClearOutAnyPotentialScrolls(window); +} + +async function synthesizeKeyInEachEventLoop(aKey, aEvent, aCount) { + for (let i = 0; i < aCount; i++) { + synthesizeKey(aKey, aEvent); + await new Promise(resolve => SimpleTest.executeSoon(resolve)); + } +} + +async function sendStringOneByOne(aString) { + for (const ch of aString) { + sendString(ch); + await new Promise(resolve => SimpleTest.executeSoon(resolve)); + } +} + +async function doKeyEventTest(aSelector) { + const element = document.querySelector(aSelector); + if (element.value !== undefined) { + element.value = ""; + } else { + element.innerHTML = ""; + } + element.focus(); + element.scrollTop = 0; + await waitForScroll(); + is(element.scrollTop, 0, `${aSelector}.scrollTop should be 0`); + await synthesizeKeyInEachEventLoop("KEY_Enter", {shiftKey: true}, 6); + await waitForScroll(); + isnot(element.scrollTop, 0, `${aSelector} should be scrolled by inserting line breaks`); + const oldScrollTop = element.scrollTop; + await synthesizeKeyInEachEventLoop("KEY_ArrowUp", {}, 5); + await waitForScroll(); + isnot(element.scrollTop, oldScrollTop, `${aSelector} should be scrolled by up key events`); + await synthesizeKeyInEachEventLoop("KEY_ArrowDown", {}, 5); + await waitForScroll(); + is(element.scrollTop, oldScrollTop, `${aSelector} should be scrolled by down key events`); + const longWord = "aaaaaaaaaaaaaaaaaaaa"; + await sendStringOneByOne(longWord); + await waitForScroll(); + isnot(element.scrollLeft, 0, `${aSelector} should be scrolled by typing long word`); + const oldScrollLeft = element.scrollLeft; + await synthesizeKeyInEachEventLoop("KEY_ArrowLeft", {}, longWord.length); + await waitForScroll(); + isnot(element.scrollLeft, oldScrollLeft, `${aSelector} should be scrolled by left key events`); + await synthesizeKeyInEachEventLoop("KEY_ArrowRight", {}, longWord.length); + await waitForScroll(); + is(element.scrollLeft, oldScrollLeft, `${aSelector} should be scrolled by right key events`); +} + +async function doCompositionTest(aSelector) { + const element = document.querySelector(aSelector); + if (element.value !== undefined) { + element.value = ""; + } else { + element.innerHTML = ""; + } + element.focus(); + element.scrollTop = 0; + await waitForScroll(); + is(element.scrollTop, 0, `${aSelector}.scrollTop should be 0`); + const longCompositionString = + "Web \u958b\u767a\u8005\u306e\u7686\u3055\u3093\u306f\u3001" + + "Firefox \u306b\u5b9f\u88c5\u3055\u308c\u3066\u3044\u308b HTML5" + + " \u3084 CSS \u306e\u65b0\u6a5f\u80fd\u3092\u6d3b\u7528\u3059" + + "\u308b\u3053\u3068\u3067\u3001\u9b45\u529b\u3042\u308b Web " + + "\u30b5\u30a4\u30c8\u3084\u9769\u65b0\u7684\u306a Web \u30a2" + + "\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u3088\u308a" + + "\u77ed\u6642\u9593\u3067\u7c21\u5358\u306b\u4f5c\u6210\u3067" + + "\u304d\u307e\u3059\u3002"; + synthesizeCompositionChange( + { + composition: { + string: longCompositionString, + clauses: [ + { + length: longCompositionString.length, + attr: COMPOSITION_ATTR_RAW_CLAUSE, + }, + ], + }, + caret: { + start: longCompositionString.length, + length: 0, + }, + } + ); + await waitForScroll(); + isnot(element.scrollTop, 0, `${aSelector} should be scrolled by composition`); + synthesizeComposition({ type: "compositioncommit", data: "" }); + await waitForScroll(); + is( + element.scrollTop, + 0, + `${aSelector} should be scrolled back to the top by canceling composition` + ); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(async () => { + await doKeyEventTest("textarea"); + await doKeyEventTest("div[contenteditable]"); + await doCompositionTest("textarea"); + await doCompositionTest("div[contenteditable]"); + SimpleTest.finish(); +}); +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_bug796839.html b/editor/libeditor/tests/test_bug796839.html new file mode 100644 index 0000000000..8fe1c36d56 --- /dev/null +++ b/editor/libeditor/tests/test_bug796839.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=796839 +--> +<title>Test for Bug 796839</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=796839">Mozilla Bug 796839</a> +<div id="test" contenteditable><br></div> +<script> +var div = document.getElementById("test"); +var text = document.createTextNode(""); +div.insertBefore(text, div.firstChild); +getSelection().collapse(text, 0); +document.execCommand("inserthtml", false, "x"); +is(div.textContent, "x", "Empty textnodes should be editable"); +</script> diff --git a/editor/libeditor/tests/test_bug830600.html b/editor/libeditor/tests/test_bug830600.html new file mode 100644 index 0000000000..166ac187a2 --- /dev/null +++ b/editor/libeditor/tests/test_bug830600.html @@ -0,0 +1,97 @@ +<!DOCTYPE HTML> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=830600 +--> +<head> + <title>Test for Bug 830600</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> + +<body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=830600">Mozilla Bug 830600</a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <input type="text" id="t1" /> + <pre id="test"> + <script type="application/javascript"> + + /** Test for Bug 830600 **/ + SimpleTest.waitForExplicitFinish(); + SimpleTest.waitForFocus(function() { + const Ci = SpecialPowers.Ci; + function test(str, expected, callback) { + var t = document.getElementById("t1"); + t.focus(); + t.value = ""; + var editor = SpecialPowers.wrap(t).editor; + editor.newlineHandling = Ci.nsIEditor.eNewlinesStripSurroundingWhitespace; + SimpleTest.waitForClipboard(str, + function() { + SpecialPowers.Cc["@mozilla.org/widget/clipboardhelper;1"] + .getService(Ci.nsIClipboardHelper) + .copyString(str); + }, + function() { + synthesizeKey("V", {accelKey: true}); + is(t.value, expected, "New line handling works correctly"); + t.value = ""; + callback(); + }, + function() { + ok(false, "Failed to copy the string"); + SimpleTest.finish(); + } + ); + } + + function runNextTest() { + if (tests.length) { + var currentTest = tests.shift(); + test(currentTest[0], currentTest[1], runNextTest); + } else { + SimpleTest.finish(); + } + } + + var tests = [ + ["abc", "abc"], + ["\n", ""], + [" \n", ""], + ["\n ", ""], + [" \n ", ""], + [" a", " a"], + ["a ", "a "], + [" a ", " a "], + [" \nabc", "abc"], + ["\n abc", "abc"], + [" \n abc", "abc"], + [" \nabc ", "abc "], + ["\n abc ", "abc "], + [" \n abc ", "abc "], + ["abc\n ", "abc"], + ["abc \n", "abc"], + ["abc \n ", "abc"], + [" abc\n ", " abc"], + [" abc \n", " abc"], + [" abc \n ", " abc"], + [" abc \n def \n ", " abcdef"], + ["\n abc \n def \n ", "abcdef"], + [" \n abc \n def ", "abcdef "], + [" abc\n\ndef ", " abcdef "], + [" abc \n\n def ", " abcdef "], + ]; + + runNextTest(); + }); + + </script> + </pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug832025.html b/editor/libeditor/tests/test_bug832025.html new file mode 100644 index 0000000000..181adda423 --- /dev/null +++ b/editor/libeditor/tests/test_bug832025.html @@ -0,0 +1,43 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=832025 +--> +<head> + <title>Test for Bug 832025</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=832025">Mozilla Bug 832025</a> +<div id="test" contenteditable="true">header1</div> +<script type="application/javascript"> + +/** + * Test for Bug 832025 + * + */ + +document.execCommand("stylewithcss", false, "true"); +document.execCommand("defaultParagraphSeparator", false, "div"); +var test = document.getElementById("test"); +test.focus(); + +// place caret at end of editable area +var sel = getSelection(); +sel.collapse(test, test.childNodes.length); + +// make it a H1 +document.execCommand("formatBlock", false, "H1"); +// simulate a CR key +sendKey("return"); +// insert some text +document.execCommand("insertText", false, "abc"); + +is(test.innerHTML, "<h1>header1</h1><div>abc<br></div>", + "A paragraph automatically created after a CR at the end of an H1 should not be bold"); + +</script> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug850043.html b/editor/libeditor/tests/test_bug850043.html new file mode 100644 index 0000000000..681a5cb5fe --- /dev/null +++ b/editor/libeditor/tests/test_bug850043.html @@ -0,0 +1,59 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=850043 +--> +<head> + <title>Test for Bug 850043</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=850043">Mozilla Bug 850043</a> +<div id="display"> +<textarea id="textarea">b邀󠄏辺󠄁</textarea> +<div contenteditable id="edit">b邀󠄏辺󠄁</div> +</div> +<div id="content" style="display: none"> +</div> + +<pre id="test"> +</pre> +<script> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + let fm = SpecialPowers.Services.focus; + + let element = document.getElementById("textarea"); + element.setSelectionRange(element.value.length, element.value.length); + element.focus(); + is(SpecialPowers.unwrap(fm.focusedElement), element, "failed to move focus"); + + synthesizeKey("KEY_End"); + sendString("a"); + is(element.value, "b\u{9080}\u{e010f}\u{8fba}\u{e0101}a", "a isn't last character"); + + synthesizeKey("KEY_Backspace", {repeat: 3}); + is(element.value, "b", "cannot remove all IVS characters"); + + element = document.getElementById("edit"); + element.focus(); + is(SpecialPowers.unwrap(fm.focusedElement), element, "failed to move focus"); + + let sel = window.getSelection(); + sel.collapse(element.childNodes[0], element.textContent.length); + + sendString("a"); + is(element.textContent, "b\u{9080}\u{e010f}\u{8fba}\u{e0101}a", "a isn't last character"); + + synthesizeKey("KEY_Backspace", {repeat: 3}); + is(element.textContent, "b", "cannot remove all IVS characters"); + + SimpleTest.finish(); +}); +</script> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug857487.html b/editor/libeditor/tests/test_bug857487.html new file mode 100644 index 0000000000..e9cec16ece --- /dev/null +++ b/editor/libeditor/tests/test_bug857487.html @@ -0,0 +1,68 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=857487 +--> +<head> + <title>Test for Bug 857487</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=857487">Mozilla Bug 857487</a> +<div id="edit" contenteditable="true"> + <table id="table" border="1" width="100%"> + <tbody> + <tr> + <td>a</td> + <td>b</td> + <td>c</td> + </tr> + <tr> + <td>d</td> + <td id="cell">e</td> + <td>f</td> + </tr> + <tr> + <td>g</td> + <td>h</td> + <td>i</td> + </tr> + </tbody> + </table> +</div> +<script type="application/javascript"> + +/** + * Test for Bug 857487 + * + * Tests that removing a table row through nsIHTMLEditor works + */ + +function getEditor() { + const Ci = SpecialPowers.Ci; + var editingSession = SpecialPowers.wrap(window).docShell.editingSession; + return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsITableEditor); +} + +var cell = document.getElementById("cell"); +cell.focus(); + +// place caret at end of center cell +var sel = getSelection(); +sel.collapse(cell, cell.childNodes.length); + +var editor = getEditor(); +editor.deleteTableRow(1); + +var table = document.getElementById("table"); + +is(table.innerHTML == "\n <tbody>\n <tr>\n <td>a</td>\n <td>b</td>\n <td>c</td>\n </tr>\n \n <tr>\n <td>g</td>\n <td>h</td>\n <td>i</td>\n </tr>\n </tbody>\n ", + true, "editor.deleteTableRow(1) should delete the row containing the selection"); + +</script> + + +</body> +</html> diff --git a/editor/libeditor/tests/test_bug858918.html b/editor/libeditor/tests/test_bug858918.html new file mode 100644 index 0000000000..46f841bbce --- /dev/null +++ b/editor/libeditor/tests/test_bug858918.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=858918 +--> +<title>Test for Bug 858918</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=858918">Mozilla Bug 858918</a> +<span contenteditable style="display:block;min-height:1em"></span> +<script> +var span = document.querySelector("span"); +getSelection().collapse(span, 0); +document.execCommand("inserthtml", false, "<div>doesn't go in span</div>"); +is(span.innerHTML, "<div>doesn't go in span</div>"); +</script> diff --git a/editor/libeditor/tests/test_bug915962.html b/editor/libeditor/tests/test_bug915962.html new file mode 100644 index 0000000000..42cf4e3d67 --- /dev/null +++ b/editor/libeditor/tests/test_bug915962.html @@ -0,0 +1,120 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=915962 +--> +<head> + <title>Test for Bug 915962</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="/tests/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=915962">Mozilla Bug 915962</a> +<p id="display"></p> +<div id="content"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 915962 **/ + +var smoothScrollPref = "general.smoothScroll"; +SimpleTest.waitForExplicitFinish(); +var win = window.open("file_bug915962.html", "_blank", + "width=600,height=600,scrollbars=yes"); + + +function waitForScrollEvent(aWindow) { + return new Promise(resolve => { + aWindow.addEventListener("scroll", () => { SimpleTest.executeSoon(resolve); }, {once: true, capture: true}); + }); +} + +function waitAndCheckNoScrollEvent(aWindow) { + let gotScroll = false; + function recordScroll() { + gotScroll = true; + } + aWindow.addEventListener("scroll", recordScroll, {capture: true}); + return waitToClearOutAnyPotentialScrolls(aWindow).then(function() { + aWindow.removeEventListener("scroll", recordScroll, {capture: true}); + is(gotScroll, false, "check that we didn't get a scroll"); + }); +} + +SimpleTest.waitForFocus(function() { + SpecialPowers.pushPrefEnv({"set": [[smoothScrollPref, false]]}, startTest); +}, win); +async function startTest() { + // Make sure that pressing Space when a tabindex=-1 element is focused + // will scroll the page. + var button = win.document.querySelector("button"); + var sc = win.document.querySelector("div"); + sc.focus(); + await waitToClearOutAnyPotentialScrolls(win); + is(win.scrollY, 0, "Sanity check"); + let waitForScrolling = waitForScrollEvent(win); + synthesizeKey(" ", {}, win); + + await waitForScrolling; + + isnot(win.scrollY, 0, "Page is scrolled down"); + var oldY = win.scrollY; + waitForScrolling = waitForScrollEvent(win); + synthesizeKey(" ", {shiftKey: true}, win); + + await waitForScrolling; + + ok(win.scrollY < oldY, "Page is scrolled up"); + + // Make sure that pressing Space when a tabindex=-1 element is focused + // will not scroll the page, and will activate the element. + button.focus(); + await waitToClearOutAnyPotentialScrolls(win); + var clicked = false; + button.onclick = () => clicked = true; + oldY = win.scrollY; + let waitForNoScroll = waitAndCheckNoScrollEvent(win); + synthesizeKey(" ", {}, win); + + await waitForNoScroll; + + ok(win.scrollY <= oldY, "Page is not scrolled down"); + ok(clicked, "The button should be clicked"); + synthesizeKey("VK_TAB", {}, win); + + await waitToClearOutAnyPotentialScrolls(win); + + oldY = win.scrollY; + waitForScrolling = waitForScrollEvent(win); + synthesizeKey(" ", {}, win); + + await waitForScrolling; + + ok(win.scrollY >= oldY, "Page is scrolled down"); + + win.close(); + + win = window.open("file_bug915962.html", "_blank", + "width=600,height=600,scrollbars=yes"); + + SimpleTest.waitForFocus(async function() { + is(win.scrollY, 0, "Sanity check"); + waitForScrolling = waitForScrollEvent(win); + synthesizeKey(" ", {}, win); + + await waitForScrolling; + + isnot(win.scrollY, 0, "Page is scrolled down without crashing"); + + win.close(); + + SimpleTest.finish(); + }, win); +} +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug966155.html b/editor/libeditor/tests/test_bug966155.html new file mode 100644 index 0000000000..193d383b96 --- /dev/null +++ b/editor/libeditor/tests/test_bug966155.html @@ -0,0 +1,54 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=966155 +--> +<head> + <title>Test for Bug 966155</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=966155">Mozilla Bug 966155</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +var testWindow = window.open("file_bug966155.html", "", "test-966155"); +testWindow.addEventListener("load", function() { + runTest(testWindow); +}, {once: true}); + +function runTest(win) { + SimpleTest.waitForFocus(function() { + var doc = win.document; + var iframe = doc.querySelector("iframe"); + var iframeDoc = iframe.contentDocument; + var input = doc.querySelector("input"); + iframe.focus(); + iframeDoc.body.focus(); + // Type some text + "test".split("").forEach(function(letter) { + synthesizeKey(letter, {}, win); + }); + is(iframeDoc.body.textContent.trim(), "test", "entered the text"); + // focus the input box + input.focus(); + // press tab + synthesizeKey("VK_TAB", {}, win); + // Now press Ctrl+Backspace + synthesizeKey("VK_BACK_SPACE", {ctrlKey: true}, win); + is(iframeDoc.body.textContent.trim(), "", "deleted the text"); + win.close(); + SimpleTest.finish(); + }, win); +} + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug966552.html b/editor/libeditor/tests/test_bug966552.html new file mode 100644 index 0000000000..f27ceb92d7 --- /dev/null +++ b/editor/libeditor/tests/test_bug966552.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=966552 +--> +<head> + <title>Test for Bug 966552</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=966552">Mozilla Bug 966552</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +var testWindow = window.open("file_bug966552.html", "", "test-966552"); +testWindow.addEventListener("load", function() { + runTest(testWindow); +}, {once: true}); + +function runTest(win) { + SimpleTest.waitForFocus(function() { + var doc = win.document; + var sel = win.getSelection(); + doc.body.focus(); + sel.collapse(doc.body.firstChild, 2); + synthesizeKey("VK_BACK_SPACE", {ctrlKey: true}, win); + is(doc.body.textContent.trim(), "st"); + win.close(); + SimpleTest.finish(); + }, win); +} + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_bug974309.html b/editor/libeditor/tests/test_bug974309.html new file mode 100644 index 0000000000..67f720f2e4 --- /dev/null +++ b/editor/libeditor/tests/test_bug974309.html @@ -0,0 +1,74 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=974309 +--> +<head> + <title>Test for Bug 974309</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=974309">Mozilla Bug 974309</a> +<div id="edit_not_table_parent" contenteditable="true"></div> +<div> + <table id="table" border="1" width="100%"> + <tbody> + <tr> + <td>a</td> + <td>b</td> + <td>c</td> + </tr> + <tr> + <td>d</td> + <td id="cell">e</td> + <td>f</td> + </tr> + <tr> + <td>g</td> + <td>h</td> + <td>i</td> + </tr> + </tbody> + </table> +</div> +<script type="application/javascript"> + +/** + * Test for Bug 974309 + * + * Tests that editing a table row fails when the table or row is _not_ a child of a contenteditable node. + * See bug 857487 for tests that cover when the table or row _is_ a child of a contenteditable node. + */ + +function getEditor() { + const Ci = SpecialPowers.Ci; + var editingSession = SpecialPowers.wrap(window).docShell.editingSession; + return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsITableEditor); +} + +var cell = document.getElementById("cell"); +cell.focus(); + +// place caret at end of center cell +var sel = getSelection(); +sel.collapse(cell, cell.childNodes.length); + +var table = document.getElementById("table"); + +var tableHTML = table.innerHTML; + +var editor = getEditor(); +editor.deleteTableRow(1); + +is(table.innerHTML == tableHTML, true, "editor should not modify non-editable table" ); + +isnot(table.innerHTML == "\n <tbody>\n <tr>\n <td>a</td>\n <td>b</td>\n <td>c</td>\n </tr>\n \n <tr>\n <td>g</td>\n <td>h</td>\n <td>i</td>\n </tr>\n </tbody>\n ", + true, "editor.deleteTableRow(1) should not delete a non-editable row containing the selection"); + +</script> + + +</body> +</html> diff --git a/editor/libeditor/tests/test_bug998188.html b/editor/libeditor/tests/test_bug998188.html new file mode 100644 index 0000000000..8038d2ef64 --- /dev/null +++ b/editor/libeditor/tests/test_bug998188.html @@ -0,0 +1,51 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=565392 +--> +<head> + <title>Test for Bug 998188</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=998188">Mozilla Bug 998188</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<div id="editor" contenteditable>abc</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 998188 **/ + +SimpleTest.waitForExplicitFinish(); + +function runTests() { + var editor = document.getElementById("editor"); + editor.focus(); + + var textNode1 = document.createTextNode("def"); + var textNode2 = document.createTextNode("ghi"); + + editor.appendChild(textNode1); + editor.appendChild(textNode2); + + window.getSelection().collapse(textNode2, 3); + + for (var i = 0; i < 9; i++) { + var caretRect = synthesizeQueryCaretRect(i); + ok(caretRect.succeeded, "QueryCaretRect should succeeded (" + i + ")"); + } + + SimpleTest.finish(); +} + +SimpleTest.waitForFocus(runTests); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_can_undo_after_setting_value.xhtml b/editor/libeditor/tests/test_can_undo_after_setting_value.xhtml new file mode 100644 index 0000000000..03204f8500 --- /dev/null +++ b/editor/libeditor/tests/test_can_undo_after_setting_value.xhtml @@ -0,0 +1,38 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" + type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1386222 +--> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Mozilla Bug 1386222" onload="runTest();"> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <input xmlns="http://www.w3.org/1999/xhtml" id="t"/> + <script class="testbody" type="application/javascript"> + <![CDATA[ + +function runTest() { + var t = document.getElementById("t"); + is( + t.editor.canUndo, + false, + "Editor shouldn't have undo transaction at start" + ); + t.value = "foo"; + is(t.value, "foo", "value setter worked"); + is( + t.editor.canUndo, + true, + "Editor should have undo transaction after setting value" + ); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +]]> +</script> +</window> diff --git a/editor/libeditor/tests/test_cannot_undo_after_reinitializing_editor.html b/editor/libeditor/tests/test_cannot_undo_after_reinitializing_editor.html new file mode 100644 index 0000000000..b4f8239e20 --- /dev/null +++ b/editor/libeditor/tests/test_cannot_undo_after_reinitializing_editor.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=602130 +--> +<head> + <title>Test for Bug 602130</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=602130">Mozilla Bug 602130</a> +<p id="display"></p> +<div id="content"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 602130 **/ + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var i = document.createElement("input"); + document.body.appendChild(i); + i.select(); + i.focus(); + is( + SpecialPowers.wrap(i).editor.canUndo, + false, + "Editor shouldn't have undo transactions at start" + ); + i.style.display = "none"; + document.offsetWidth; + i.style.display = ""; + document.offsetWidth; + i.select(); + i.focus(); + is( + SpecialPowers.wrap(i).editor.canUndo, + false, + "Editor shouldn't have undo transaction after re-initializing the editor" + ); + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_caret_move_in_vertical_content.html b/editor/libeditor/tests/test_caret_move_in_vertical_content.html new file mode 100644 index 0000000000..3e0854a752 --- /dev/null +++ b/editor/libeditor/tests/test_caret_move_in_vertical_content.html @@ -0,0 +1,209 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1103374 +--> +<head> + <title>Testing caret move in vertical content</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=289384">Mozilla Bug 1103374</a> +<input value="ABCD"><textarea>ABCD +EFGH</textarea> +<div contenteditable></div> +<script> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(async () => { + await (async function testContentEditable() { + const editor = document.querySelector("div[contenteditable]"); + const selection = getSelection(); + function nodeStr(node) { + if (node.nodeType === Node.TEXT_NODE) { + return `#text ("${node.data}")`; + } + if (node.nodeType === Node.ELEMENT_NODE) { + return `<${node.tagName.toLowerCase()}>`; + } + return node; + } + function rangeStr(container, offset) { + return `{ container: ${nodeStr(container)}, offset: ${offset} }`; + } + function selectionStr() { + if (selection.rangeCount === 0) { + return "<no range>"; + } + if (selection.isCollapsed) { + return rangeStr(selection.focusNode, selection.focusOffset); + } + return `${ + rangeStr(selection.anchorNode, selection.anchorOffset) + } - ${ + rangeStr(selection.focusNode, selection.focusOffset) + }`; + } + await (async function testVerticalRL() { + editor.innerHTML = '<p style="font-family: monospace; writing-mode: vertical-rl">ABCD<br>EFGH</p>'; + editor.focus(); + const firstLine = editor.querySelector("p").firstChild; + const secondLine = firstLine.nextSibling.nextSibling; + await new Promise(resolve => requestAnimationFrame( + () => requestAnimationFrame(resolve)) // guarantee sending focus notification to the widget + ); + selection.collapse(firstLine, firstLine.length / 2); + synthesizeKey("KEY_ArrowUp"); + is(selectionStr(), rangeStr(firstLine, firstLine.length / 2 - 1), + "contenteditable: ArrowUp should move caret to previous character in vertical-rl content"); + selection.collapse(firstLine, firstLine.length / 2 - 1); + synthesizeKey("KEY_ArrowDown"); + is(selectionStr(), rangeStr(firstLine, firstLine.length / 2), + "contenteditable: ArrowDown should move caret to next character in vertical-rl content"); + selection.collapse(firstLine, firstLine.length / 2); + synthesizeKey("KEY_ArrowLeft"); + is(selectionStr(), rangeStr(secondLine, secondLine.length / 2), + "contenteditable: ArrowLeft should move caret to next line in vertical-rl content"); + selection.collapse(secondLine, secondLine.length / 2); + synthesizeKey("KEY_ArrowRight"); + is(selectionStr(), rangeStr(firstLine, firstLine.length / 2), + "contenteditable: ArrowRight should move caret to previous line in vertical-rl content"); + selection.removeAllRanges(); + editor.blur(); + })(); + await (async function testVerticalLR() { + editor.innerHTML = '<p style="font-family: monospace; writing-mode: vertical-lr">ABCD<br>EFGH</p>'; + editor.focus(); + const firstLine = editor.querySelector("p").firstChild; + const secondLine = firstLine.nextSibling.nextSibling; + await new Promise(resolve => requestAnimationFrame( + () => requestAnimationFrame(resolve)) // guarantee sending focus notification to the widget + ); + selection.collapse(firstLine, firstLine.length / 2); + synthesizeKey("KEY_ArrowUp"); + is(selectionStr(), rangeStr(firstLine, firstLine.length / 2 - 1), + "contenteditable: ArrowUp should move caret to previous character in vertical-lr content"); + selection.collapse(firstLine, firstLine.length / 2 - 1); + synthesizeKey("KEY_ArrowDown"); + is(selectionStr(), rangeStr(firstLine, firstLine.length / 2), + "contenteditable: ArrowDown should move caret to next character in vertical-lr content"); + selection.collapse(firstLine, firstLine.length / 2); + synthesizeKey("KEY_ArrowRight"); + is(selectionStr(), rangeStr(secondLine, secondLine.length / 2), + "contenteditable: ArrowRight should move caret to next line in vertical-lr content"); + selection.collapse(secondLine, secondLine.length / 2); + synthesizeKey("KEY_ArrowLeft"); + is(selectionStr(), rangeStr(firstLine, firstLine.length / 2), + "contenteditable: ArrowLeft should move caret to previous line in vertical-lr content"); + selection.removeAllRanges(); + editor.blur(); + })(); + })(); + await (async function testInputTypeText() { + const editor = document.querySelector("input"); + await (async function testVerticalRL() { + editor.style.writingMode = "vertical-rl"; + editor.focus(); + await new Promise(resolve => requestAnimationFrame( + () => requestAnimationFrame(resolve)) // guarantee sending focus notification to the widget + ); + editor.selectionStart = editor.selectionEnd = editor.value.length / 2; + synthesizeKey("KEY_ArrowRight"); + is(editor.selectionStart, 0, + "<input>: ArrowRight should move caret to beginning of the input element if vertical-rl"); + editor.selectionStart = editor.selectionEnd = editor.value.length / 2; + synthesizeKey("KEY_ArrowLeft"); + is(editor.selectionStart, editor.value.length, + "<input>: ArrowLeft should move caret to end of the input element if vertical-rl"); + editor.selectionStart = editor.selectionEnd = editor.value.length / 2; + synthesizeKey("KEY_ArrowDown"); + is(editor.selectionStart, editor.value.length / 2 + 1, + "<input>: ArrowDown should move caret to next character if vertical-rl"); + editor.selectionStart = editor.selectionEnd = editor.value.length / 2; + synthesizeKey("KEY_ArrowUp"); + is(editor.selectionStart, editor.value.length / 2 - 1, + "<input>: ArrowUp should move caret to previous character if vertical-rl"); + editor.blur(); + })(); + await (async function testVerticalLR() { + editor.style.writingMode = "vertical-lr"; + editor.focus(); + await new Promise(resolve => requestAnimationFrame( + () => requestAnimationFrame(resolve)) // guarantee sending focus notification to the widget + ); + editor.selectionStart = editor.selectionEnd = editor.value.length / 2; + synthesizeKey("KEY_ArrowLeft"); + is(editor.selectionStart, 0, + "<input>: ArrowLeft should move caret to beginning of the input element if vertical-lr"); + editor.selectionStart = editor.selectionEnd = editor.value.length / 2; + synthesizeKey("KEY_ArrowRight"); + is(editor.selectionStart, editor.value.length, + "<input>: ArrowRight should move caret to end of the input element if vertical-lr"); + editor.selectionStart = editor.selectionEnd = editor.value.length / 2; + synthesizeKey("KEY_ArrowDown"); + is(editor.selectionStart, editor.value.length / 2 + 1, + "<input>: ArrowDown should move caret to next character if vertical-lr"); + editor.selectionStart = editor.selectionEnd = editor.value.length / 2; + synthesizeKey("KEY_ArrowUp"); + is(editor.selectionStart, editor.value.length / 2 - 1, + "<input>: ArrowUp should move caret to previous character if vertical-lr"); + editor.blur(); + })(); + })(); + await (async function testTextArea() { + const editor = document.querySelector("textarea"); + await (async function testVerticalRL() { + editor.style.writingMode = "vertical-rl"; + editor.focus(); + await new Promise(resolve => requestAnimationFrame( + () => requestAnimationFrame(resolve)) // guarantee sending focus notification to the widget + ); + editor.selectionStart = editor.selectionEnd = "ABCD\nEF".length; + synthesizeKey("KEY_ArrowRight"); + isfuzzy(editor.selectionStart, "AB".length, 1, + "<textarea>: ArrowRight should move caret to previous line of textarea element if vertical-rl"); + editor.selectionStart = editor.selectionEnd = "AB".length; + synthesizeKey("KEY_ArrowLeft"); + isfuzzy(editor.selectionStart, "ABCD\nEF".length, 1, + "<textarea>: ArrowLeft should move caret to next line of textarea element if vertical-rl"); + editor.selectionStart = editor.selectionEnd = "AB".length; + synthesizeKey("KEY_ArrowDown"); + is(editor.selectionStart, "ABC".length, + "<textarea>: ArrowDown should move caret to next character if vertical-rl"); + editor.selectionStart = editor.selectionEnd = "AB".length; + synthesizeKey("KEY_ArrowUp"); + is(editor.selectionStart, "A".length, + "<textarea>: ArrowUp should move caret to previous character if vertical-rl"); + editor.blur(); + })(); + await (async function testVerticalLR() { + editor.style.writingMode = "vertical-lr"; + editor.focus(); + await new Promise(resolve => requestAnimationFrame( + () => requestAnimationFrame(resolve)) // guarantee sending focus notification to the widget + ); + editor.selectionStart = editor.selectionEnd = "ABCD\nEF".length; + synthesizeKey("KEY_ArrowLeft"); + isfuzzy(editor.selectionStart, "AB".length, 1, + "<textarea>: ArrowLeft should move caret to previous line of textarea element if vertical-rl"); + editor.selectionStart = editor.selectionEnd = "AB".length; + synthesizeKey("KEY_ArrowRight"); + isfuzzy(editor.selectionStart, "ABCD\nEF".length, 1, + "<textarea>: ArrowRight should move caret to next line of textarea element if vertical-rl"); + editor.selectionStart = editor.selectionEnd = "AB".length; + synthesizeKey("KEY_ArrowDown"); + is(editor.selectionStart, "ABC".length, + "<textarea>: ArrowDown should move caret to next character if vertical-rl"); + editor.selectionStart = editor.selectionEnd = "AB".length; + synthesizeKey("KEY_ArrowUp"); + is(editor.selectionStart, "A".length, + "<textarea>: ArrowUp should move caret to previous character if vertical-rl"); + editor.blur(); + })(); + })(); + SimpleTest.finish(); +}); +</script> +</body> +</html> diff --git a/editor/libeditor/tests/test_cmd_absPos.html b/editor/libeditor/tests/test_cmd_absPos.html new file mode 100644 index 0000000000..c31c2c53a4 --- /dev/null +++ b/editor/libeditor/tests/test_cmd_absPos.html @@ -0,0 +1,296 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<title>Testing cmd_absPos command</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> +</head> +<body contenteditable></body> +<script> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + (function testTogglingPositionInBodyChildText() { + const description = "testTogglingPositionInBodyChildText"; + const textNode = document.createTextNode("abc"); + document.body.appendChild(textNode); + getSelection().collapse(textNode, 1); + + SpecialPowers.doCommand(window, "cmd_absPos"); + const div = textNode.parentNode; + is( + div.tagName.toLowerCase(), + "div", + `${ + description + }: a <div> element should be created and the text node should be wrapped in it` + ); + is( + div.style.position, + "absolute", + `${description}: position of the <div> should be set to "absolute"` + ); + isnot( + div.style.top, + "", + `${description}: top of the <div> should be set` + ); + isnot( + div.style.left, + "", + `${description}: left of the <div> should be set` + ); + ok( + getSelection().isCollapsed, + `${ + description + }: selection should be collapsed (making it absolutely positioned)` + ); + // Collapsing selection to start of the absolutely positioned <div> seems + // odd. Why don't we keep selection in the new <div>? + is( + getSelection().focusNode, + div, + `${ + description + }: selection should be collapsed in the absolutely positioned <div>` + ); + is( + getSelection().focusOffset, + 0, + `${ + description + }: selection should be collapsed at start of the absolutely positioned <div>` + ); + + SpecialPowers.doCommand(window, "cmd_absPos"); + ok( + !div.isConnected, + `${description}: the <div> should be removed from the <body>` + ); + is( + div.style.position, + "", + `${description}: position of the <div> should be unset` + ); + is( + div.style.top, + "", + `${description}: top of the <div> should be unset` + ); + is( + div.style.left, + "", + `${description}: left of the <div> should be unset` + ); + ok( + getSelection().isCollapsed, + `${ + description + }: selection should be collapsed (making it in normal flow)` + ); + // If we change the above behavior, we can keep selection collapsed in the + // text node here. + is( + getSelection().focusNode, + document.body, + `${description}: selection should be collapsed in the <body>` + ); + todo_is( + getSelection().focusNode.childNodes.item( + getSelection().focusOffset + ), + textNode, + `${description}: selection should be collapsed after the text node` + ); + textNode.remove(); + })(); + + (function testTogglingPositionOfDivContainingCaret() { + const description = "testTogglingPositionOfDivContainingCaret"; + const div = document.createElement("div"); + div.innerHTML = "abc"; + document.body.appendChild(div); + const textNode = div.firstChild; + getSelection().collapse(textNode, 1); + SpecialPowers.doCommand(window, "cmd_absPos"); + is( + div.style.position, + "absolute", + `${description}: position of the <div> should be set to "absolute"` + ); + isnot( + div.style.top, + "", + `${description}: top of the <div> should be set` + ); + isnot( + div.style.left, + "", + `${description}: left of the <div> should be set` + ); + ok( + getSelection().isCollapsed, + `${ + description + }: selection should be collapsed (making it absolutely positioned)` + ); + is( + getSelection().focusNode, + textNode, + `${ + description + }: selection should be collapsed in the absolutely positioned <div>` + ); + is( + getSelection().focusOffset, + 1, + `${ + description + }: selection should be collapsed at same offset in the absolutely positioned <div>` + ); + + SpecialPowers.doCommand(window, "cmd_absPos"); + ok( + !div.isConnected, + `${description}: the <div> should be removed from the <body>` + ); + is( + div.style.position, + "", + `${description}: position of the <div> should be unset` + ); + is( + div.style.top, + "", + `${description}: top of the <div> should be unset` + ); + is( + div.style.left, + "", + `${description}: left of the <div> should be unset` + ); + ok( + getSelection().isCollapsed, + `${ + description + }: selection should be collapsed (making it in normal flow)` + ); + is( + getSelection().focusNode, + textNode, + `${description}: selection should be collapsed in the <div>` + ); + is( + getSelection().focusOffset, + 1, + `${description}: selection should be collapsed at same offset in the <div>` + ); + div.remove(); + textNode.remove(); + })(); + + (function testTogglingPositionOfDivContainingSelectionRange() { + const description = "testTogglingPositionOfDivContainingSelectionRange"; + const div = document.createElement("div"); + div.innerHTML = "abc"; + document.body.appendChild(div); + const textNode = div.firstChild; + getSelection().setBaseAndExtent(textNode, 1, textNode, 2); + SpecialPowers.doCommand(window, "cmd_absPos"); + is( + div.style.position, + "absolute", + `${description}: position of the <div> should be set to "absolute"` + ); + isnot( + div.style.top, + "", + `${description}: top of the <div> should be set` + ); + isnot( + div.style.left, + "", + `${description}: left of the <div> should be set` + ); + is( + getSelection().anchorNode, + textNode, + `${ + description + }: selection should start from the text node in the absolutely positioned <div>` + ); + is( + getSelection().anchorOffset, + 1, + `${ + description + }: selection should start from "b" in the text node in the absolutely positioned <div>` + ); + is( + getSelection().focusNode, + textNode, + `${ + description + }: selection should end in the text node in the absolutely positioned <div>` + ); + is( + getSelection().focusOffset, + 2, + `${ + description + }: selection should end after "b" absolutely positioned <div>` + ); + + getSelection().setBaseAndExtent(textNode, 1, textNode, 2); + SpecialPowers.doCommand(window, "cmd_absPos"); + ok( + !div.isConnected, + `${description}: the <div> should be removed from the <body>` + ); + is( + div.style.position, + "", + `${description}: position of the <div> should be unset` + ); + is( + div.style.top, + "", + `${description}: top of the <div> should be unset` + ); + is( + div.style.left, + "", + `${description}: left of the <div> should be unset` + ); + is( + getSelection().anchorNode, + textNode, + `${description}: selection should start from the text node in the <div>` + ); + is( + getSelection().anchorOffset, + 1, + `${ + description + }: selection should start from "b" in the text node in the <div>` + ); + is( + getSelection().focusNode, + textNode, + `${description}: selection should end in the text node in the <div>` + ); + is( + getSelection().focusOffset, + 2, + `${description}: selection should end after "b" <div>` + ); + div.remove(); + textNode.remove(); + })(); + + document.body.removeAttribute("contenteditable"); + SimpleTest.finish(); +}); +</script> +</html> diff --git a/editor/libeditor/tests/test_cmd_backgroundColor.html b/editor/libeditor/tests/test_cmd_backgroundColor.html new file mode 100644 index 0000000000..7e089ad9f0 --- /dev/null +++ b/editor/libeditor/tests/test_cmd_backgroundColor.html @@ -0,0 +1,223 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<title>Testing "cmd_backgroundColor" behavior</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<div contenteditable></div> +<script> +"use strict"; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + document.execCommand("styleWithCSS", false, "true"); + const commandManager = + SpecialPowers.wrap(window). + docShell. + QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor). + getInterface(SpecialPowers.Ci.nsICommandManager); + const editor = document.querySelector("div[contenteditable]"); + editor.style.backgroundColor = "#ff0000"; + + editor.innerHTML = "<p>abc</p>"; + editor.getBoundingClientRect(); + editor.focus(); + getSelection().collapse(editor.querySelector("p").firstChild, 1); + let params = SpecialPowers.Cu.createCommandParams(); + commandManager.getCommandState("cmd_backgroundColor", window, params); + is( + params.getCStringValue("state_attribute"), + "rgb(255, 0, 0)", + "cmd_backgroundColor should return the editing host's background color (should ignore the transparent paragraph)" + ); + + editor.innerHTML = "<p style=\"background-color: #00ff00\">abc</p>"; + editor.getBoundingClientRect(); + editor.focus(); + getSelection().collapse(editor.querySelector("p").firstChild, 1); + params = SpecialPowers.Cu.createCommandParams(); + commandManager.getCommandState("cmd_backgroundColor", window, params); + is( + params.getCStringValue("state_attribute"), + "rgb(0, 255, 0)", + "cmd_backgroundColor should return the paragraph's background color" + ); + + editor.innerHTML = "<span style=\"background-color: #00ff00\">abc</span>"; + editor.getBoundingClientRect(); + editor.focus(); + getSelection().collapse(editor.querySelector("span").firstChild, 1); + params = SpecialPowers.Cu.createCommandParams(); + commandManager.getCommandState("cmd_backgroundColor", window, params); + is( + params.getCStringValue("state_attribute"), + "rgb(255, 0, 0)", + "cmd_backgroundColor shouldn't return the span's background color due to inline element" + ); + + editor.innerHTML = "<p style=\"background-color: #00ff00\" contenteditable=\"false\">a<span style=\"background-color: #0000ff\" contenteditable=\"true\">b</span>c</p>"; + editor.getBoundingClientRect(); + editor.focus(); + getSelection().collapse(editor.querySelector("span[contenteditable=true]").firstChild, 1); + params = SpecialPowers.Cu.createCommandParams(); + commandManager.getCommandState("cmd_backgroundColor", window, params); + is( + params.getCStringValue("state_attribute"), + "rgb(0, 255, 0)", + "cmd_backgroundColor should return non-editable block element's background color" + ); + + editor.innerHTML = "<p contenteditable=\"false\">a<span style=\"background-color: #0000ff\" contenteditable=\"true\">b</span>c</p>"; + editor.getBoundingClientRect(); + editor.focus(); + getSelection().collapse(editor.querySelector("span[contenteditable=true]").firstChild, 1); + params = SpecialPowers.Cu.createCommandParams(); + commandManager.getCommandState("cmd_backgroundColor", window, params); + is( + params.getCStringValue("state_attribute"), + "rgb(255, 0, 0)", + "cmd_backgroundColor should return the parent editing host's background color (should ignore the transparent non-editable paragraph)" + ); + + editor.style.backgroundColor = "#ff0000"; + editor.innerHTML = "<div><p><span>abc</span></p></div>"; + editor.getBoundingClientRect(); + editor.focus(); + getSelection().collapse(editor.querySelector("span").firstChild, 1); + SpecialPowers.doCommand(window, "cmd_backgroundColor", "#00ff00"); + is( + editor.outerHTML, + "<div contenteditable=\"\" style=\"background-color: rgb(255, 0, 0);\"><div><p style=\"background-color: rgb(0, 255, 0);\"><span>abc</span></p></div></div>", + "cmd_backgroundColor should set background of the closest block element from the caret in a text node" + ); + + editor.style.backgroundColor = "#ff0000"; + editor.innerHTML = "abc"; + editor.getBoundingClientRect(); + editor.focus(); + getSelection().collapse(editor.firstChild, 1); + SpecialPowers.doCommand(window, "cmd_backgroundColor", "#00ff00"); + is( + editor.outerHTML, + "<div contenteditable=\"\" style=\"background-color: rgb(0, 255, 0);\">abc</div>", + "cmd_backgroundColor should set background of the editing host which is direct block parent from the caret in a text node" + ); + + editor.style.backgroundColor = "#ff0000"; + editor.innerHTML = "<div contenteditable=\"false\"><span contenteditable=\"true\">abc</span></div>"; + editor.getBoundingClientRect(); + editor.focus(); + getSelection().collapse(editor.firstChild, 1); + SpecialPowers.doCommand(window, "cmd_backgroundColor", "#00ff00"); + is( + editor.outerHTML, + "<div contenteditable=\"\" style=\"background-color: rgb(255, 0, 0);\"><div contenteditable=\"false\"><span contenteditable=\"true\">abc</span></div></div>", + "cmd_backgroundColor should not set background color of inline editing host nor its non-editable parent block" + ); + + editor.style.backgroundColor = "#ff0000"; + editor.innerHTML = "<div><p><span>ab<br>c</span></p></div>"; + editor.getBoundingClientRect(); + editor.focus(); + getSelection().setBaseAndExtent(editor.querySelector("span"), 1, editor.querySelector("span"), 2); + SpecialPowers.doCommand(window, "cmd_backgroundColor", "#00ff00"); + is( + editor.outerHTML, + "<div contenteditable=\"\" style=\"background-color: rgb(255, 0, 0);\"><div><p style=\"background-color: rgb(0, 255, 0);\"><span>ab<br>c</span></p></div></div>", + "cmd_backgroundColor should set background of the closest block element when selection a leaf element" + ); + + editor.style.backgroundColor = "#ff0000"; + editor.innerHTML = "<div><p><span>abc</span></p></div>"; + editor.getBoundingClientRect(); + editor.focus(); + getSelection().setBaseAndExtent(editor.firstChild, 0, editor.firstChild, 1); + SpecialPowers.doCommand(window, "cmd_backgroundColor", "#00ff00"); + is( + editor.outerHTML, + "<div contenteditable=\"\" style=\"background-color: rgb(255, 0, 0);\"><div><p style=\"background-color: rgb(0, 255, 0);\"><span>abc</span></p></div></div>", + "cmd_backgroundColor should set background of the selected block element" + ); + + editor.style.backgroundColor = "#ff0000"; + editor.innerHTML = "<div><p><span>abc</span></p><p><span>def</span></p><p><span>ghi</span></p></div>"; + editor.getBoundingClientRect(); + editor.focus(); + getSelection().setBaseAndExtent(editor.querySelector("span").firstChild, 1, editor.querySelector("p + p + p > span").firstChild, 1); + SpecialPowers.doCommand(window, "cmd_backgroundColor", "#00ff00"); + is( + editor.outerHTML, + "<div contenteditable=\"\" style=\"background-color: rgb(255, 0, 0);\"><div><p style=\"background-color: rgb(0, 255, 0);\"><span>abc</span></p>" + + "<p style=\"background-color: rgb(0, 255, 0);\"><span>def</span></p>" + + "<p style=\"background-color: rgb(0, 255, 0);\"><span>ghi</span></p></div></div>", + "cmd_backgroundColor should set background of the paragraph elements in the selection range" + ); + + editor.style.backgroundColor = "#ff0000"; + editor.innerHTML = "<div><p><span>abc</span></p><p><span contenteditable=\"false\">def</span></p><p><span>ghi</span></p></div>"; + editor.getBoundingClientRect(); + editor.focus(); + getSelection().setBaseAndExtent(editor.querySelector("span").firstChild, 1, editor.querySelector("p + p + p > span").firstChild, 1); + SpecialPowers.doCommand(window, "cmd_backgroundColor", "#00ff00"); + is( + editor.outerHTML, + "<div contenteditable=\"\" style=\"background-color: rgb(255, 0, 0);\"><div><p style=\"background-color: rgb(0, 255, 0);\"><span>abc</span></p>" + + "<p style=\"background-color: rgb(0, 255, 0);\"><span contenteditable=\"false\">def</span></p>" + + "<p style=\"background-color: rgb(0, 255, 0);\"><span>ghi</span></p></div></div>", + "cmd_backgroundColor should set background of the paragraph elements in the selection range even if a paragraph has only non-editable content" + ); + + editor.style.backgroundColor = "#ff0000"; + editor.innerHTML = "<div><p><span>abc</span></p><p contenteditable=\"false\"><span>def</span></p><p><span>ghi</span></p></div>"; + editor.getBoundingClientRect(); + editor.focus(); + getSelection().setBaseAndExtent(editor.querySelector("span").firstChild, 1, editor.querySelector("p + p + p > span").firstChild, 1); + SpecialPowers.doCommand(window, "cmd_backgroundColor", "#00ff00"); + is( + editor.outerHTML, + "<div contenteditable=\"\" style=\"background-color: rgb(255, 0, 0);\"><div><p style=\"background-color: rgb(0, 255, 0);\"><span>abc</span></p>" + + "<p contenteditable=\"false\"><span>def</span></p>" + + "<p style=\"background-color: rgb(0, 255, 0);\"><span>ghi</span></p></div></div>", + "cmd_backgroundColor should set background of the paragraph elements in the selection range except the non-editable paragraph" + ); + + editor.style.backgroundColor = "#ff0000"; + editor.innerHTML = "<div><p><span>abc</span></p><span>def</span><p><span>ghi</span></p></div>"; + editor.getBoundingClientRect(); + editor.focus(); + getSelection().setBaseAndExtent(editor.querySelector("span").firstChild, 1, editor.querySelector("p + span + p > span").firstChild, 1); + SpecialPowers.doCommand(window, "cmd_backgroundColor", "#00ff00"); + is( + editor.outerHTML, + "<div contenteditable=\"\" style=\"background-color: rgb(255, 0, 0);\"><div style=\"background-color: rgb(0, 255, 0);\">" + + "<p style=\"background-color: rgb(0, 255, 0);\"><span>abc</span></p>" + + "<span>def</span>" + + "<p style=\"background-color: rgb(0, 255, 0);\"><span>ghi</span></p></div></div>", + "cmd_backgroundColor should set background of the paragraph elements in the selection range and the container <div> which has inline child" + ); + + editor.style.backgroundColor = "#ff0000"; + editor.innerHTML = "<p><span>abc</span></p><span>def</span><p><span>ghi</span></p>"; + editor.getBoundingClientRect(); + editor.focus(); + getSelection().setBaseAndExtent(editor.querySelector("span").firstChild, 1, editor.querySelector("p + span + p > span").firstChild, 1); + SpecialPowers.doCommand(window, "cmd_backgroundColor", "#00ff00"); + is( + editor.outerHTML, + "<div contenteditable=\"\" style=\"background-color: rgb(0, 255, 0);\">" + + "<p style=\"background-color: rgb(0, 255, 0);\"><span>abc</span></p>" + + "<span>def</span>" + + "<p style=\"background-color: rgb(0, 255, 0);\"><span>ghi</span></p></div>", + "cmd_backgroundColor should set background of the paragraph elements in the selection range and the editing host which has inline child" + ); + + // TODO: Add testcase for HTML styling mode. + + SimpleTest.finish(); +}); +</script> +</body> +</html> diff --git a/editor/libeditor/tests/test_cmd_fontFace_with_empty_string.html b/editor/libeditor/tests/test_cmd_fontFace_with_empty_string.html new file mode 100644 index 0000000000..c101aaf6c6 --- /dev/null +++ b/editor/libeditor/tests/test_cmd_fontFace_with_empty_string.html @@ -0,0 +1,30 @@ +<!doctype html> +<html> +<head> +<title>Tests typing in empty paragraph after running `cmd_fontFace` with empty string</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +<script> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + document.designMode = "on"; + document.body.innerHTML = "<p><font face=\"monospace\">abc</font><br></p>"; + getSelection().collapse(document.querySelector("font").firstChild, "abc".length); + document.execCommand("insertParagraph"); + // Calling document.execCommand("fontName", false, "") is NOOP, but HTMLEditor + // accepts empty string param for cmd_fontFace. + SpecialPowers.doCommand(window, "cmd_fontFace", ""); + document.execCommand("insertText", false, "d"); + is( + document.querySelector("p+p").innerHTML, + "d<br>", + "The typed text should not be wrapped in <font face=\"monospace\">" + ); + document.designMode = "off"; + SimpleTest.finish(); +}); +</script> +</head> +<body></body> +</html> diff --git a/editor/libeditor/tests/test_cmd_fontFace_with_tt.html b/editor/libeditor/tests/test_cmd_fontFace_with_tt.html new file mode 100644 index 0000000000..495076e9b5 --- /dev/null +++ b/editor/libeditor/tests/test_cmd_fontFace_with_tt.html @@ -0,0 +1,73 @@ +<!doctype html> +<html> +<head> + <title>Testing font face "tt"</title> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> +<div id="display"> +</div> + +<div id="content" contenteditable>abc</div> + +<pre id="test"> +</pre> + +<script class="testbody"> +"use strict"; +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + let editor = document.querySelector("div[contenteditable]"); + editor.focus(); + let selection = document.getSelection(); + + // "tt" is a magic value of "cmd_fontFace", it should work as "cmd_tt". + editor.innerHTML = "abc"; + selection.setBaseAndExtent(editor.firstChild, 1, editor.firstChild, 2); + SpecialPowers.doCommand(window, "cmd_fontFace", "tt"); + is(editor.innerHTML, "a<tt>b</tt>c", + "\"cmd_fontFace\" with \"tt\" should wrap selected text <tt> element"); + + editor.innerHTML = "<br>"; + selection.collapse(editor, 0); + SpecialPowers.doCommand(window, "cmd_fontFace", "tt"); + synthesizeKey("t"); + synthesizeKey("t"); + is(editor.innerHTML, "<tt>tt</tt><br>", + "Typed text after \"cmd_fontFace\" with \"tt\" should be wrapped by <tt> element"); + + // But it shouldn't work with `Document.execCommand()`. + editor.innerHTML = "abc"; + selection.setBaseAndExtent(editor.firstChild, 1, editor.firstChild, 2); + document.execCommand("fontname", false, "tt"); + is(editor.innerHTML, "a<font face=\"tt\">b</font>c", + "execCommand(\"fontname\") with \"tt\" should wrap selected text with <font> element"); + + editor.innerHTML = "<br>"; + selection.collapse(editor, 0); + document.execCommand("fontname", false, "tt"); + synthesizeKey("t"); + synthesizeKey("t"); + is(editor.innerHTML, "<font face=\"tt\">tt</font><br>", + "Typed text after execCommand(\"fontname\") with \"tt\" should be wrapped by <font> element"); + + // "cmd_fontFace" with "tt" should remove `<font>` element. + editor.innerHTML = "a<font face=\"sans-serif\">b</font>c"; + selection.selectAllChildren(editor.querySelector("font")); + SpecialPowers.doCommand(window, "cmd_fontFace", "tt"); + is(editor.innerHTML, "a<tt>b</tt>c", + "\"cmd_fontFace\" with \"tt\" should wrap selected text <tt> element after removing <font> element"); + + editor.innerHTML = "<font face=\"sans-serif\">abc</font>"; + selection.setBaseAndExtent(editor.firstChild.firstChild, 1, editor.firstChild.firstChild, 2); + SpecialPowers.doCommand(window, "cmd_fontFace", "tt"); + is(editor.innerHTML, "<font face=\"sans-serif\">a</font><tt>b</tt><font face=\"sans-serif\">c</font>", + "\"cmd_fontFace\" with \"tt\" should wrap selected text <tt> element after removing <font> element"); + + SimpleTest.finish(); +}); +</script> +</body> +</html> diff --git a/editor/libeditor/tests/test_cmd_increaseFont.html b/editor/libeditor/tests/test_cmd_increaseFont.html new file mode 100644 index 0000000000..ab7b9ba766 --- /dev/null +++ b/editor/libeditor/tests/test_cmd_increaseFont.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=767684 +--> +<title>Test for Bug 767684</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=767684">Mozilla Bug 767684</a> +<div contenteditable>foo<b>bar</b>baz</div> +<script> +SimpleTest.waitForExplicitFinish(); +(async () => { + getSelection().selectAllChildren(document.querySelector("div")); + SpecialPowers.doCommand(window, "cmd_increaseFont"); + is( + document.querySelector("div").innerHTML, + "<big>foo<b>bar</b>baz</big>", + "All selected text must be embiggened" + ); + SimpleTest.finish(); +})(); +</script> diff --git a/editor/libeditor/tests/test_cmd_paragraphState.html b/editor/libeditor/tests/test_cmd_paragraphState.html new file mode 100644 index 0000000000..9b13fc32bf --- /dev/null +++ b/editor/libeditor/tests/test_cmd_paragraphState.html @@ -0,0 +1,55 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<title>Testing "cmd_paragraphState" behavior</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<div contenteditable></div> +<script> +"use strict"; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + const editor = document.querySelector("div[contenteditable]"); + + editor.innerHTML = "<div><p>abc</p></div>"; + editor.focus(); + getSelection().collapse(editor.querySelector("p").firstChild, 1); + editor.getBoundingClientRect(); + SpecialPowers.doCommand(window, "cmd_paragraphState", ""); + is( + editor.innerHTML, + "<div>abc</div>", + "cmd_paragraphState with empty string should remove the parent block element" + ); + + editor.innerHTML = "<div><div contenteditable=\"false\"><p contenteditable>abc</p></div></div>"; + editor.focus(); + getSelection().collapse(editor.querySelector("p").firstChild, 1); + editor.getBoundingClientRect(); + SpecialPowers.doCommand(window, "cmd_paragraphState", ""); + is( + editor.innerHTML, + "<div><div contenteditable=\"false\"><p contenteditable=\"\">abc</p></div></div>", + "cmd_paragraphState with empty string should not remove editing host" + ); + + editor.innerHTML = "<div><div contenteditable=\"false\"><p><span contenteditable>abc</span></p></div></div>"; + editor.focus(); + getSelection().collapse(editor.querySelector("span").firstChild, 1); + editor.getBoundingClientRect(); + SpecialPowers.doCommand(window, "cmd_paragraphState", ""); + is( + editor.innerHTML, + "<div><div contenteditable=\"false\"><p><span contenteditable=\"\">abc</span></p></div></div>", + "cmd_paragraphState with empty string should not remove parents of inline editing host" + ); + + SimpleTest.finish(); +}); +</script> +</body> +</html> diff --git a/editor/libeditor/tests/test_composition_event_created_in_chrome.html b/editor/libeditor/tests/test_composition_event_created_in_chrome.html new file mode 100644 index 0000000000..11b24d3bc1 --- /dev/null +++ b/editor/libeditor/tests/test_composition_event_created_in_chrome.html @@ -0,0 +1,76 @@ +<!doctype html> +<html> + +<head> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> + + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> + +<body> + +<input id="input"> + +<script type="application/javascript"> + +// In nsEditorEventListener, when listening event is not created with proper +// event interface, it asserts the fact. +SimpleTest.waitForExplicitFinish(); + +var gInputElement = document.getElementById("input"); + +function getEditor(aInputElement) { + var editableElement = SpecialPowers.wrap(aInputElement); + ok(editableElement.editor, "There is no editor for the input element"); + return editableElement.editor; +} + +var gEditor; + +function testNotGenerateCompositionByCreatedEvents(aEventInterface) { + var compositionEvent = document.createEvent(aEventInterface); + if (compositionEvent.initCompositionEvent) { + compositionEvent.initCompositionEvent("compositionstart", true, true, window, "", ""); + } else if (compositionEvent.initMouseEvent) { + compositionEvent.initMouseEvent("compositionstart", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); + } + gInputElement.dispatchEvent(compositionEvent); + ok(!gEditor.composing, "Composition shouldn't be started with a created compositionstart event (" + aEventInterface + ")"); + + compositionEvent = document.createEvent(aEventInterface); + if (compositionEvent.initCompositionEvent) { + compositionEvent.initCompositionEvent("compositionupdate", true, false, window, "abc", ""); + } else if (compositionEvent.initMouseEvent) { + compositionEvent.initMouseEvent("compositionupdate", true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); + } + gInputElement.dispatchEvent(compositionEvent); + ok(!gEditor.composing, "Composition shouldn't be started with a created compositionupdate event (" + aEventInterface + ")"); + is(gInputElement.value, "", "Input element shouldn't be modified with a created compositionupdate event (" + aEventInterface + ")"); + + compositionEvent = document.createEvent(aEventInterface); + if (compositionEvent.initCompositionEvent) { + compositionEvent.initCompositionEvent("compositionend", true, false, window, "abc", ""); + } else if (compositionEvent.initMouseEvent) { + compositionEvent.initMouseEvent("compositionend", true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); + } + gInputElement.dispatchEvent(compositionEvent); + ok(!gEditor.composing, "Composition shouldn't be committed with a created compositionend event (" + aEventInterface + ")"); + is(gInputElement.value, "", "Input element shouldn't be committed with a created compositionend event (" + aEventInterface + ")"); +} + +function doTests() { + gInputElement.focus(); + gEditor = getEditor(gInputElement); + + testNotGenerateCompositionByCreatedEvents("CompositionEvent"); + testNotGenerateCompositionByCreatedEvents("MouseEvent"); + + SimpleTest.finish(); +} + +SimpleTest.waitForFocus(doTests); + +</script> +</body> +</html> diff --git a/editor/libeditor/tests/test_composition_with_highlight_in_texteditor.html b/editor/libeditor/tests/test_composition_with_highlight_in_texteditor.html new file mode 100644 index 0000000000..d6bb703231 --- /dev/null +++ b/editor/libeditor/tests/test_composition_with_highlight_in_texteditor.html @@ -0,0 +1,62 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<title>Test crash bug 1785311</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<input value="abc"> +<script> +"use strict"; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + const input = document.querySelector("input"); + input.focus(); + const editor = SpecialPowers.wrap(input).editor; + const selectionController = editor.selectionController; + const findSelection = selectionController.getSelection( + SpecialPowers.Ci.nsISelectionController.SELECTION_FIND + ); + const editActionListener = { + QueryInterface: SpecialPowers.ChromeUtils.generateQI(["nsIEditActionListener"]), + WillDeleteText: (textNode, offset, length) => {}, + DidInsertText: (textNode, offset, aString) => {}, + WillDeleteRanges: (rangesToDelete) => {}, + }; + // Highlight "a" + findSelection.setBaseAndExtent( + editor.rootElement.firstChild, 0, + editor.rootElement.firstChild, 1 + ); + try { + editor.addEditActionListener(editActionListener); + // Start composition at end of <input> + editor.selection.collapse(editor.rootElement.firstChild, "abc".length); + synthesizeCompositionChange({ + composition: { + string: "d", + clauses: [ + { length: 1, attr: COMPOSITION_ATTR_RAW_CLAUSE }, + ], + }, + caret: { + start: 1, + length: 0, + }, + }); + synthesizeComposition({ + type: "compositioncommitasis", + }); + ok(true, "Should not crash"); + } finally { + editor.removeEditActionListener(editActionListener); + SimpleTest.finish(); + } +}); +</script> +</body> +</html> diff --git a/editor/libeditor/tests/test_contenteditable_copy_empty_selection.html b/editor/libeditor/tests/test_contenteditable_copy_empty_selection.html new file mode 100644 index 0000000000..b24ef5ca5a --- /dev/null +++ b/editor/libeditor/tests/test_contenteditable_copy_empty_selection.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1857303 +--> +<head> + <meta charset="utf-8"> + <title>Test for contenteditable copy event fired even when selection is empty</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + <div contenteditable=true id="elem">Copy</div> + <script> + SimpleTest.waitForExplicitFinish(); + SimpleTest.waitForFocus(() => { + let copyEventFired = false; + const editableDiv = document.getElementById("elem"); + + editableDiv.addEventListener("copy", function () { + copyEventFired = true; + }); + + editableDiv.focus(); + synthesizeKey("c", { accelKey: true }); + ok(copyEventFired, "Copy event should be fired"); + + SimpleTest.finish(); + }); + </script> +</body> +</html> diff --git a/editor/libeditor/tests/test_contenteditable_focus.html b/editor/libeditor/tests/test_contenteditable_focus.html new file mode 100644 index 0000000000..741a5b48bf --- /dev/null +++ b/editor/libeditor/tests/test_contenteditable_focus.html @@ -0,0 +1,335 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Test for contenteditable focus</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<div id="display"> + First text in this document.<br> + <input id="inputText" type="text"><br> + <input id="inputTextReadonly" type="text" readonly><br> + <input id="inputButton" type="button" value="input[type=button]"><br> + <button id="button">button</button><br> + <div id="primaryEditor" contenteditable="true"> + editable contents.<br> + <input id="inputTextInEditor" type="text"><br> + <input id="inputTextReadonlyInEditor" type="text" readonly><br> + <input id="inputButtonInEditor" type="button" value="input[type=button]"><br> + <button id="buttonInEditor">button</button><br> + <div id="noeditableInEditor" contenteditable="false"> + <span id="spanInNoneditableInEditor">span element in noneditable in editor</span><br> + <input id="inputTextInNoneditableInEditor" type="text"><br> + <input id="inputTextReadonlyInNoneditableInEditor" type="text" readonly><br> + <input id="inputButtonInNoneditableInEditor" type="button" value="input[type=button]"><br> + <button id="buttonInNoneditableInEditor">button</button><br> + </div> + <span id="spanInEditor">span element in editor</span><br> + </div> + <div id="otherEditor" contenteditable="true"> + other editor. + </div> +</div> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<script> +"use strict"; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + function getNodeDescription(aNode) { + if (aNode === undefined) { + return "undefined"; + } + if (aNode === null) { + return "null"; + } + switch (aNode.nodeType) { + case Node.TEXT_NODE: + return `${aNode.nodeName}, "${aNode.data.replace(/\n/g, "\\n")}"`; + case Node.ELEMENT_NODE: + return `<${aNode.tagName.toLowerCase()}${ + aNode.getAttribute("id") !== null + ? ` id="${aNode.getAttribute("id")}"` + : "" + }${ + aNode.getAttribute("readonly") !== null + ? " readonly" + : "" + }${ + aNode.getAttribute("type") !== null + ? ` type="${aNode.getAttribute("type")}"` + : "" + }>`; + } + return aNode.nodeName; + } + + const fm = SpecialPowers.Services.focus; + // XXX using selCon for checking the visibility of the caret, however, + // selCon is shared in document, cannot get the element of owner of the + // caret from javascript? + const selCon = SpecialPowers.wrap(window).docShell. + QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor). + getInterface(SpecialPowers.Ci.nsISelectionDisplay). + QueryInterface(SpecialPowers.Ci.nsISelectionController); + + const primaryEditor = document.getElementById("primaryEditor"); + const spanInEditor = document.getElementById("spanInEditor"); + const otherEditor = document.getElementById("otherEditor"); + + (function test_initial_state_on_load() { + is( + getSelection().rangeCount, + 0, + "There should be no selection range at start" + ); + ok(!selCon.caretVisible, "The caret should not be visible in the document"); + // Move focus to <input type="text"> in the primary editor + primaryEditor.querySelector("input[type=text]").focus(); + is( + SpecialPowers.unwrap(fm.focusedElement), + primaryEditor.querySelector("input[type=text]"), + '<input type="text"> in the primary editor should get focus' + ); + todo_is( + getSelection().rangeCount, + 0, + 'There should be no selection range after calling focus() of <input type="text"> in the primary editor' + ); + ok( + selCon.caretVisible, + 'The caret should not be visible in the <input type="text"> in the primary editor' + ); + })(); + // Move focus to the editor + (function test_move_focus_from_child_input_to_parent_editor() { + primaryEditor.focus(); + is( + SpecialPowers.unwrap(fm.focusedElement), + primaryEditor, + `The editor should steal focus from <input type="text"> in the primary editor with calling its focus() (got ${ + getNodeDescription(SpecialPowers.unwrap(fm.focusedElement)) + }` + ); + is( + getSelection().rangeCount, + 1, + "There should be one range after focus() of the editor is called" + ); + const range = getSelection().getRangeAt(0); + ok( + range.collapsed, + "The selection range should be collapsed (immediately after calling focus() of the editor)" + ); + is( + range.startContainer, + primaryEditor.firstChild, + `The selection range should be in the first text node of the editor (immediately after calling focus() of the editor, got ${ + getNodeDescription(range.startContainer) + })` + ); + ok( + selCon.caretVisible, + "The caret should be visible in the primary editor (immediately after calling focus() of the editor)" + ); + })(); + // Move focus to other editor + (function test_move_focus_from_editor_to_the_other_editor() { + otherEditor.focus(); + is( + SpecialPowers.unwrap(fm.focusedElement), + otherEditor, + `The other editor should steal focus from the editor (got ${ + getNodeDescription(SpecialPowers.unwrap(fm.focusedElement)) + }` + ); + is( + getSelection().rangeCount, + 1, + "There should be one range after focus() of the other editor is called" + ); + const range = getSelection().getRangeAt(0); + ok( + range.collapsed, + "The selection range should be collapsed (immediately after calling focus() of the other editor)" + ); + is( + range.startContainer, + otherEditor.firstChild, + `The selection range should be in the first text node of the editor (immediately after calling focus() of the other editor, got ${ + getNodeDescription(range.startContainer) + })` + ); + ok( + selCon.caretVisible, + "The caret should be visible in the primary editor (immediately after calling focus() of the other editor)" + ); + })(); + // Move focus to <input type="text"> in the primary editor + (function test_move_focus_from_the_other_editor_to_input_in_the_editor() { + primaryEditor.querySelector("input[type=text]").focus(); + is( + SpecialPowers.unwrap(fm.focusedElement), + primaryEditor.querySelector("input[type=text]"), + `<input type="text"> in the primary editor should steal focus from the other editor (got ${ + getNodeDescription(SpecialPowers.unwrap(fm.focusedElement)) + }`); + is( + getSelection().rangeCount, + 1, + 'There should be one range after focus() of the <input type="text"> in the primary editor is called' + ); + const range = getSelection().getRangeAt(0); + ok( + range.collapsed, + 'The selection range should be collapsed (immediately after calling focus() of the <input type="text"> in the primary editor)' + ); + // XXX maybe, the caret can stay on the other editor if it's better. + is( + range.startContainer, + primaryEditor.firstChild, + `The selection range should be in the first text node of the editor (immediately after calling focus() of the <input type="text"> in the primary editor, got ${ + getNodeDescription(range.startContainer) + })` + ); + ok( + selCon.caretVisible, + 'The caret should be visible in the <input type="text"> (immediately after calling focus() of the <input type="text"> in the primary editor)' + ); + })(); + // Move focus to the other editor again + (function test_move_focus_from_the_other_editor_to_the_editor_with_Selection_API() { + otherEditor.focus(); + is( + SpecialPowers.unwrap(fm.focusedElement), + otherEditor, + `The other editor should steal focus from the <input type="text"> in the primary editor with its focus() (got ${ + getNodeDescription(SpecialPowers.unwrap(fm.focusedElement)) + })` + ); + // Set selection to the span element in the primary editor. + getSelection().collapse(spanInEditor.firstChild, 5); + is( + getSelection().rangeCount, + 1, + "There should be one selection range after collapsing selection into the <span> in the primary editor when the other editor has focus" + ); + is( + SpecialPowers.unwrap(fm.focusedElement), + primaryEditor, + `The editor should steal focus from the other editor with Selection API (got ${ + getNodeDescription(SpecialPowers.unwrap(fm.focusedElement)) + }` + ); + ok( + selCon.caretVisible, + "The caret should be visible in the primary editor (immediately after moving focus with Selection API)" + ); + })(); + // Move focus to the editor + (function test_move_focus() { + primaryEditor.focus(); + is( + SpecialPowers.unwrap(fm.focusedElement), + primaryEditor, + "The editor should keep having focus (immediately after calling focus() of the editor when it has focus)" + ); + is( + getSelection().rangeCount, + 1, + "There should be one selection range in the primary editor (immediately after calling focus() of the editor when it has focus)" + ); + const range = getSelection().getRangeAt(0); + ok( + range.collapsed, + "The selection range should be collapsed in the primary editor (immediately after calling focus() of the editor when it has focus)" + ); + is( + range.startOffset, + 5, + "The startOffset of the selection range shouldn't be changed (immediately after calling focus() of the editor when it has focus)" + ); + is( + range.startContainer.parentNode, + spanInEditor, + `The startContainer of the selection range shouldn't be changed (immediately after calling focus() of the editor when it has focus, got ${ + getNodeDescription(range.startContainer) + })`); + ok( + selCon.caretVisible, + "The caret should be visible in the primary editor (immediately after calling focus() of the editor when it has focus)" + ); + })(); + + // Move focus to each focusable element in the primary editor. + function test_move_focus_from_the_editor(aTargetElement, aFocusable, aCaretVisible) { + primaryEditor.focus(); + is( + SpecialPowers.unwrap(fm.focusedElement), + primaryEditor, + `The editor should have focus at preparing to move focus to ${ + getNodeDescription(aTargetElement) + } (got ${ + getNodeDescription(SpecialPowers.unwrap(fm.focusedElement)) + }`); + aTargetElement.focus(); + if (aFocusable) { + is( + SpecialPowers.unwrap(fm.focusedElement), + aTargetElement, + `${ + getNodeDescription(aTargetElement) + } should get focus with calling its focus() (got ${ + getNodeDescription(SpecialPowers.unwrap(fm.focusedElement)) + }` + ); + } else { + is( + SpecialPowers.unwrap(fm.focusedElement), + primaryEditor, + `${ + getNodeDescription(aTargetElement) + } should not take focus with calling its focus() (got ${ + getNodeDescription(SpecialPowers.unwrap(fm.focusedElement)) + }` + ); + } + is( + selCon.caretVisible, + aCaretVisible, + `The caret ${ + aCaretVisible ? "should" : "should not" + } visible after calling focus() of ${ + getNodeDescription(aTargetElement) + }` + ); + } + test_move_focus_from_the_editor(primaryEditor.querySelector("input[type=text]"), true, true); + test_move_focus_from_the_editor(primaryEditor.querySelector("input[type=text][readonly]"), true, true); + // XXX shouldn't the caret become invisible? + test_move_focus_from_the_editor(primaryEditor.querySelector("input[type=button]"), true, true); + test_move_focus_from_the_editor(primaryEditor.querySelector("button"), true, true); + test_move_focus_from_the_editor(primaryEditor.querySelector("div[contenteditable=false]"), false, true); + test_move_focus_from_the_editor(primaryEditor.querySelector("div[contenteditable=false] > span"), false, true); + test_move_focus_from_the_editor(primaryEditor.querySelector("div[contenteditable=false] > input[type=text]"), true, true); + test_move_focus_from_the_editor(primaryEditor.querySelector("div[contenteditable=false] > input[type=text][readonly]"), true, true); + test_move_focus_from_the_editor(primaryEditor.querySelector("div[contenteditable=false] > input[type=button]"), true, false); + test_move_focus_from_the_editor(primaryEditor.querySelector("div[contenteditable=false] > button"), true, false); + test_move_focus_from_the_editor(spanInEditor, false, true); + test_move_focus_from_the_editor(document.querySelector("input[type=text]"), true, true); + test_move_focus_from_the_editor(document.querySelector("input[type=text][readonly]"), true, true); + test_move_focus_from_the_editor(document.querySelector("input[type=button]"), true, false); + test_move_focus_from_the_editor(document.querySelector("button"), true, false); + + SimpleTest.finish(); +}); +</script> +</body> +</html> diff --git a/editor/libeditor/tests/test_contenteditable_text_input_handling.html b/editor/libeditor/tests/test_contenteditable_text_input_handling.html new file mode 100644 index 0000000000..c00056a0f0 --- /dev/null +++ b/editor/libeditor/tests/test_contenteditable_text_input_handling.html @@ -0,0 +1,317 @@ +<html> +<head> + <title>Test for text input event handling on contenteditable editor</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> +</head> +<body> +<div id="display"> + <p id="static">static content<input id="inputInStatic"><textarea id="textareaInStatic"></textarea></p> + <p id="editor"contenteditable="true">content editable<input id="inputInEditor"><textarea id="textareaInEditor"></textarea></p> +</div> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +const kLF = !navigator.platform.indexOf("Win") && false ? "\r\n" : "\n"; + +function runTests() { + var fm = Services.focus; + + var listener = { + handleEvent: function _hv(aEvent) { + aEvent.preventDefault(); // prevent the browser default behavior + }, + }; + var els = Services.els; + els.addSystemEventListener(window, "keypress", listener, false); + + var staticContent = document.getElementById("static"); + staticContent._defaultValue = getTextValue(staticContent); + staticContent._isFocusable = false; + staticContent._isEditable = false; + staticContent._isContentEditable = false; + staticContent._description = "non-editable p element"; + var inputInStatic = document.getElementById("inputInStatic"); + inputInStatic._defaultValue = getTextValue(inputInStatic); + inputInStatic._isFocusable = true; + inputInStatic._isEditable = true; + inputInStatic._isContentEditable = false; + inputInStatic._description = "input element in static content"; + var textareaInStatic = document.getElementById("textareaInStatic"); + textareaInStatic._defaultValue = getTextValue(textareaInStatic); + textareaInStatic._isFocusable = true; + textareaInStatic._isEditable = true; + textareaInStatic._isContentEditable = false; + textareaInStatic._description = "textarea element in static content"; + var editor = document.getElementById("editor"); + editor._defaultValue = getTextValue(editor); + editor._isFocusable = true; + editor._isEditable = true; + editor._isContentEditable = true; + editor._description = "contenteditable editor"; + var inputInEditor = document.getElementById("inputInEditor"); + inputInEditor._defaultValue = getTextValue(inputInEditor); + inputInEditor._isFocusable = true; + inputInEditor._isEditable = true; + inputInEditor._isContentEditable = false; + inputInEditor._description = "input element in contenteditable editor"; + var textareaInEditor = document.getElementById("textareaInEditor"); + textareaInEditor._defaultValue = getTextValue(textareaInEditor); + textareaInEditor._isFocusable = true; + textareaInEditor._isEditable = true; + textareaInEditor._isContentEditable = false; + textareaInEditor._description = "textarea element in contenteditable editor"; + + function getTextValue(aElement) { + if (aElement == editor) { + var value = ""; + for (var node = aElement.firstChild; node; node = node.nextSibling) { + if (node.nodeType == Node.TEXT_NODE) { + value += node.data; + } else if (node.nodeType == Node.ELEMENT_NODE) { + var tagName = node.tagName.toLowerCase(); + switch (tagName) { + case "input": + case "textarea": + value += kLF; + break; + default: + ok(false, "Undefined tag is used in the editor: " + tagName); + break; + } + } + } + return value; + } + return aElement.value; + } + + function testTextInput(aFocus) { + var when = " when " + + ((aFocus && aFocus._isFocusable) ? aFocus._description + " has focus" : + "nobody has focus"); + + function checkValue(aElement, aInsertedText) { + if (aElement == aFocus && aElement._isEditable) { + is(getTextValue(aElement), aInsertedText + aElement._defaultValue, + aElement._description + + " wasn't edited by synthesized key events" + when); + return; + } + is(getTextValue(aElement), aElement._defaultValue, + aElement._description + + " was edited by synthesized key events" + when); + } + + if (aFocus && aFocus._isFocusable) { + aFocus.focus(); + is(fm.focusedElement, aFocus, + aFocus._description + " didn't get focus at preparing tests" + when); + } else { + var focusedElement = fm.focusedElement; + if (focusedElement) { + focusedElement.blur(); + } + ok(!fm.focusedElement, + "Failed to blur at preparing tests" + when); + } + + if (aFocus && aFocus._isFocusable) { + sendString("ABC"); + checkValue(staticContent, "ABC"); + checkValue(inputInStatic, "ABC"); + checkValue(textareaInStatic, "ABC"); + checkValue(editor, "ABC"); + checkValue(inputInEditor, "ABC"); + checkValue(textareaInEditor, "ABC"); + + if (aFocus._isEditable) { + synthesizeKey("KEY_Backspace", {repeat: 3}); + checkValue(staticContent, ""); + checkValue(inputInStatic, ""); + checkValue(textareaInStatic, ""); + checkValue(editor, ""); + checkValue(inputInEditor, ""); + checkValue(textareaInEditor, ""); + } + } + + // When key events are fired on unfocused editor. + function testDispatchedKeyEvent(aTarget) { + var targetDescription = " (dispatched to " + aTarget._description + ")"; + function dispatchKeyEvent(aKeyCode, aChar, aDispatchTarget) { + var keyEvent = new KeyboardEvent("keypress", { + bubbles: true, + cancelable: true, + view: null, + keyCode: aKeyCode, + charCode: aChar ? aChar.charCodeAt(0) : 0 + }); + aDispatchTarget.dispatchEvent(keyEvent); + } + + function checkValueForDispatchedKeyEvent(aElement, aInsertedText) { + if (aElement == aTarget && aElement._isEditable && + (!aElement._isContentEditable || aElement == aFocus)) { + is(getTextValue(aElement), aInsertedText + aElement._defaultValue, + aElement._description + + " wasn't edited by dispatched key events" + + when + targetDescription); + return; + } + if (aElement == aTarget) { + is(getTextValue(aElement), aElement._defaultValue, + aElement._description + + " was edited by dispatched key events" + + when + targetDescription); + return; + } + is(getTextValue(aElement), aElement._defaultValue, + aElement._description + + " was edited by key events unexpectedly" + + when + targetDescription); + } + + dispatchKeyEvent(0, "A", aTarget); + dispatchKeyEvent(0, "B", aTarget); + dispatchKeyEvent(0, "C", aTarget); + + checkValueForDispatchedKeyEvent(staticContent, "ABC"); + checkValueForDispatchedKeyEvent(inputInStatic, "ABC"); + checkValueForDispatchedKeyEvent(textareaInStatic, "ABC"); + checkValueForDispatchedKeyEvent(editor, "ABC"); + checkValueForDispatchedKeyEvent(inputInEditor, "ABC"); + checkValueForDispatchedKeyEvent(textareaInEditor, "ABC"); + + dispatchKeyEvent(KeyboardEvent.DOM_VK_BACK_SPACE, 0, aTarget); + dispatchKeyEvent(KeyboardEvent.DOM_VK_BACK_SPACE, 0, aTarget); + dispatchKeyEvent(KeyboardEvent.DOM_VK_BACK_SPACE, 0, aTarget); + + checkValueForDispatchedKeyEvent(staticContent, ""); + checkValueForDispatchedKeyEvent(inputInStatic, ""); + checkValueForDispatchedKeyEvent(textareaInStatic, ""); + checkValueForDispatchedKeyEvent(editor, ""); + checkValueForDispatchedKeyEvent(inputInEditor, ""); + checkValueForDispatchedKeyEvent(textareaInEditor, ""); + } + + testDispatchedKeyEvent(staticContent); + testDispatchedKeyEvent(inputInStatic); + testDispatchedKeyEvent(textareaInStatic); + testDispatchedKeyEvent(editor); + testDispatchedKeyEvent(inputInEditor); + testDispatchedKeyEvent(textareaInEditor); + + if (!aFocus._isEditable) { + return; + } + + // IME + // input first character + synthesizeCompositionChange( + { "composition": + { "string": "\u3089", + "clauses": + [ + { "length": 1, "attr": COMPOSITION_ATTR_RAW_CLAUSE }, + ], + }, + "caret": { "start": 1, "length": 0 }, + }); + var queryText = synthesizeQueryTextContent(0, 100); + ok(queryText, "query text event result is null" + when); + if (!queryText) { + return; + } + ok(queryText.succeeded, "query text event failed" + when); + if (!queryText.succeeded) { + return; + } + is(queryText.text, "\u3089" + aFocus._defaultValue, + "composing text is incorrect" + when); + var querySelectedText = synthesizeQuerySelectedText(); + ok(querySelectedText, "query selected text event result is null" + when); + if (!querySelectedText) { + return; + } + ok(querySelectedText.succeeded, "query selected text event failed" + when); + if (!querySelectedText.succeeded) { + return; + } + is(querySelectedText.offset, 1, + "query selected text event returns wrong offset" + when); + is(querySelectedText.text, "", + "query selected text event returns wrong selected text" + when); + // commit composition + synthesizeComposition({ type: "compositioncommitasis" }); + queryText = synthesizeQueryTextContent(0, 100); + ok(queryText, "query text event result is null after commit" + when); + if (!queryText) { + return; + } + ok(queryText.succeeded, "query text event failed after commit" + when); + if (!queryText.succeeded) { + return; + } + is(queryText.text, "\u3089" + aFocus._defaultValue, + "composing text is incorrect after commit" + when); + querySelectedText = synthesizeQuerySelectedText(); + ok(querySelectedText, + "query selected text event result is null after commit" + when); + if (!querySelectedText) { + return; + } + ok(querySelectedText.succeeded, + "query selected text event failed after commit" + when); + if (!querySelectedText.succeeded) { + return; + } + is(querySelectedText.offset, 1, + "query selected text event returns wrong offset after commit" + when); + is(querySelectedText.text, "", + "query selected text event returns wrong selected text after commit" + + when); + + checkValue(staticContent, "\u3089"); + checkValue(inputInStatic, "\u3089"); + checkValue(textareaInStatic, "\u3089"); + checkValue(editor, "\u3089"); + checkValue(inputInEditor, "\u3089"); + checkValue(textareaInEditor, "\u3089"); + + synthesizeKey("KEY_Backspace"); + checkValue(staticContent, ""); + checkValue(inputInStatic, ""); + checkValue(textareaInStatic, ""); + checkValue(editor, ""); + checkValue(inputInEditor, ""); + checkValue(textareaInEditor, ""); + } + + testTextInput(inputInStatic); + testTextInput(textareaInStatic); + testTextInput(editor); + testTextInput(inputInEditor); + testTextInput(textareaInEditor); + + els.removeSystemEventListener(window, "keypress", listener, false); + + SimpleTest.finish(); +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_cut_copy_delete_command_enabled.html b/editor/libeditor/tests/test_cut_copy_delete_command_enabled.html new file mode 100644 index 0000000000..9e70178b11 --- /dev/null +++ b/editor/libeditor/tests/test_cut_copy_delete_command_enabled.html @@ -0,0 +1,227 @@ +<!DOCTYPE HTML> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1067255 +--> + +<head> + <title>Test for enabled state of cut/copy/delete commands</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> + +<body onload="doTest();"> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1067255">Mozilla Bug 1067255</a> + + <pre id="test"> + <script type="application/javascript"> + /** Test for Bug 1067255 **/ + SimpleTest.waitForExplicitFinish(); + + function doTest() { + var text = $("text-field"); + var textWithHandlers = $("text-field-2"); + var password = $("password-field"); + var passwordWithHandlers = $("password-field-2"); + var textWithParentHandlers = $("text-field-3"); + var passwordWithParentHandlers = $("password-field-3"); + var textarea1 = $("text-area-1"); + var textarea2 = $("text-area-2"); + var textarea3 = $("text-area-3"); + + var editor1 = SpecialPowers.wrap(text).editor; + var editor2 = SpecialPowers.wrap(password).editor; + var editor3 = SpecialPowers.wrap(textWithHandlers).editor; + var editor4 = SpecialPowers.wrap(passwordWithHandlers).editor; + var editor5 = SpecialPowers.wrap(textWithParentHandlers).editor; + var editor6 = SpecialPowers.wrap(passwordWithParentHandlers).editor; + var editor7 = SpecialPowers.wrap(textarea1).editor; + var editor8 = SpecialPowers.wrap(textarea2).editor; + var editor9 = SpecialPowers.wrap(textarea3).editor; + + text.focus(); + + ok(!editor1.canCopy(), + "nsIEditor.canCopy() should return false in <input type=text> with no selection"); + ok(!editor1.canCut(), + "nsIEditor.canCut() should return false in <input type=text> with no selection"); + ok(!SpecialPowers.isCommandEnabled(window, "cmd_copy"), + "cmd_copy command should be disabled in <input type=text> with no selection"); + ok(!SpecialPowers.isCommandEnabled(window, "cmd_cut"), + "cmd_cut command should be disabled in <input type=text> with no selection"); + ok(!SpecialPowers.isCommandEnabled(window, "cmd_delete"), + "cmd_delete command should be disabled in <input type=text> with no selection"); + + text.select(); + + ok(editor1.canCopy(), + "nsIEditor.canCopy() should return true in <input type=text> with selection"); + ok(editor1.canCut(), + "nsIEditor.canCut() should return true in <input type=text> with selection"); + ok(SpecialPowers.isCommandEnabled(window, "cmd_copy"), + "cmd_copy command should be enabled in <input type=text> with selection"); + ok(SpecialPowers.isCommandEnabled(window, "cmd_cut"), + "cmd_cut command should be enabled in <input type=text> with selection"); + ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"), + "cmd_delete command should be enabled in <input type=text> with selection"); + + password.focus(); + + ok(!editor2.canCopy(), + "nsIEditor.canCopy() should return false in <input type=password> with no selection"); + ok(!editor2.canCut(), + "nsIEditor.canCut() should return false in <input type=password> with no selection"); + ok(!SpecialPowers.isCommandEnabled(window, "cmd_copy"), + "cmd_copy command should be disabled in <input type=password> with no selection"); + ok(!SpecialPowers.isCommandEnabled(window, "cmd_cut"), + "cmd_cut command should be disabled in <input type=password> with no selection"); + ok(!SpecialPowers.isCommandEnabled(window, "cmd_delete"), + "cmd_delete command should be disabled in <input type=password> with no selection"); + + // Copy and cut commands don't do anything on password fields by default, + // so they remain disabled even when there is a selection... + password.select(); + + ok(!editor2.canCopy(), + "nsIEditor.canCopy() should return false in <input type=password> with selection"); + ok(!editor2.canCut(), + "nsIEditor.canCut() should return false in <input type=password> with selection"); + ok(!SpecialPowers.isCommandEnabled(window, "cmd_copy"), + "cmd_copy command should be disabled in <input type=password> with selection"); + ok(!SpecialPowers.isCommandEnabled(window, "cmd_cut"), + "cmd_cut command should be disabled in <input type=password> with selection"); + // Delete, on the other hand, does apply to password fields. + ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"), + "cmd_delete command should be enabled in <input type=password with selection>"); + + // ...but webpages can hook up event handlers to cut/copy events, so we have to + // keep the cut and copy commands enabled if event handlers are attached, + // for both regular edit fields and password fields (even when there's no + // selection, as we don't know what the handler might want to do). + textWithHandlers.focus(); + + ok(editor3.canCopy(), + "nsIEditor.canCopy() should return true in <input type=text> with event handler"); + ok(editor3.canCut(), + "nsIEditor.canCut() should return true in <input type=text> with event handler"); + ok(SpecialPowers.isCommandEnabled(window, "cmd_copy"), + "cmd_copy command should be enabled in <input type=text> with event handler"); + ok(SpecialPowers.isCommandEnabled(window, "cmd_cut"), + "cmd_cut command should be enabled in <input type=text> with event handler"); + + passwordWithHandlers.focus(); + + ok(editor4.canCopy(), + "nsIEditor.canCopy() should return true in <input type=password> with event handler"); + ok(editor4.canCut(), + "nsIEditor.canCut() should return true in <input type=password> with event handler"); + ok(SpecialPowers.isCommandEnabled(window, "cmd_copy"), + "cmd_copy command should be enabled in <input type=password> with event handler"); + ok(SpecialPowers.isCommandEnabled(window, "cmd_cut"), + "cmd_cut command should be enabled in <input type=password> with event handler"); + + // Also check that the commands are enabled if there's a handler on a parent element. + textWithParentHandlers.focus(); + + ok(editor5.canCopy(), + "nsIEditor.canCopy() should return true in <input type=text> with event handler on an ancestor"); + ok(editor5.canCut(), + "nsIEditor.canCut() should return true in <input type=text> with event handler on an ancestor"); + ok(SpecialPowers.isCommandEnabled(window, "cmd_copy"), + "cmd_copy command should be enabled in <input type=text> with event handler on an ancestor"); + ok(SpecialPowers.isCommandEnabled(window, "cmd_cut"), + "cmd_cut command should be enabled in <input type=text> with event handler on an ancestor"); + + passwordWithParentHandlers.focus(); + + ok(editor6.canCopy(), + "nsIEditor.canCopy() should return true in <input type=password> with event handler on an ancestor"); + ok(editor6.canCut(), + "nsIEditor.canCut() should return true in <input type=password> with event handler on an ancestor"); + ok(SpecialPowers.isCommandEnabled(window, "cmd_copy"), + "cmd_copy command should be enabled in <input type=password> with event handler on an ancestor"); + ok(SpecialPowers.isCommandEnabled(window, "cmd_cut"), + "cmd_cut command should be enabled in <input type=password> with event handler on an ancestor"); + + // TEXTAREA tests + + textarea1.focus(); + + ok(!editor7.canCopy(), + "nsIEditor.canCopy() should return false in <textarea> with no selection"); + ok(!editor7.canCut(), + "nsIEditor.canCut() should return false in <textarea> with no selection"); + ok(!SpecialPowers.isCommandEnabled(window, "cmd_copy"), + "cmd_copy command should be disabled in <textarea> with no selection"); + ok(!SpecialPowers.isCommandEnabled(window, "cmd_cut"), + "cmd_cut command should be disabled in <textarea> with no selection"); + ok(!SpecialPowers.isCommandEnabled(window, "cmd_delete"), + "cmd_delete command should be disabled in <textarea> with no selection"); + + textarea1.select(); + + ok(editor7.canCopy(), + "nsIEditor.canCopy() should return true in <textarea> with selection"); + ok(editor7.canCut(), + "nsIEditor.canCut() should return true in <textarea> with selection"); + ok(SpecialPowers.isCommandEnabled(window, "cmd_copy"), + "cmd_copy command should be enabled in <textarea> with selection"); + ok(SpecialPowers.isCommandEnabled(window, "cmd_cut"), + "cmd_cut command should be enabled in <textarea> with selection"); + ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"), + "cmd_delete command should be enabled in <textarea> with selection"); + + textarea2.focus(); + + ok(!editor8.canCopy(), + "nsIEditor.canCopy() should return false in <textarea> with only a 'cut' handler"); + ok(editor8.canCut(), + "nsIEditor.canCut() should return true in <textarea> with only a 'cut' handler"); + ok(!SpecialPowers.isCommandEnabled(window, "cmd_copy"), + "cmd_copy command should be disabled in <textarea> with only a 'cut' handler"); + ok(SpecialPowers.isCommandEnabled(window, "cmd_cut"), + "cmd_cut command should be enabled in <textarea> with only a 'cut' handler"); + ok(!SpecialPowers.isCommandEnabled(window, "cmd_delete"), + "cmd_delete command should be disabled in <textarea> with only a 'cut' handler"); + + textarea3.focus(); + + ok(editor9.canCopy(), + "nsIEditor.canCopy() should return true in <textarea> with only a 'copy' handler on ancestor"); + ok(!editor9.canCut(), + "nsIEditor.canCut() should return false in <textarea> with only a 'copy' handler on ancestor"); + ok(SpecialPowers.isCommandEnabled(window, "cmd_copy"), + "cmd_copy command should be enabled in <textarea> with only a 'copy' handler on ancestor"); + ok(!SpecialPowers.isCommandEnabled(window, "cmd_cut"), + "cmd_cut command should be disabled in <textarea> with only a 'copy' handler on ancestor"); + ok(!SpecialPowers.isCommandEnabled(window, "cmd_delete"), + "cmd_delete command should be disabled in <textarea> with only a 'copy' handler on ancestor"); + + SimpleTest.finish(); + } + </script> + </pre> + + <input type="text" value="Gonzo says hi" id="text-field" /> + <input type="password" value="Jan also" id="password-field" /> + <input type="text" value="Hi says Gonzo" id="text-field-2" oncut="cut()" oncopy="copy()" ondelete="delete()"/> + <input type="password" value="Also Jan" id="password-field-2" oncut="cut()" oncopy="copy()" ondelete="delete()"/> + <div oncut="cut()"> + <ul oncopy="copy()"> + <li><input type="text" value="Hi again" id="text-field-3"/></li> + <li><input type="password" value="And again, hi" id="password-field-3"/></li> + </ul> + </div> + <textarea id="text-area-1">textarea</textarea> + <textarea oncut="cut()" id="text-area-2">textarea with cut handler</textarea> + <div oncopy="copy()"> + <blockquote> + <p><textarea id="text-area-3">textarea with copy handler on parent</textarea></p> + </blockquote> + </div> +</body> +</html> diff --git a/editor/libeditor/tests/test_cut_copy_delete_command_enabled.xhtml b/editor/libeditor/tests/test_cut_copy_delete_command_enabled.xhtml new file mode 100644 index 0000000000..46c91e87d0 --- /dev/null +++ b/editor/libeditor/tests/test_cut_copy_delete_command_enabled.xhtml @@ -0,0 +1,215 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Test for enabled state of cut/copy/delete commands"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + + <script type="application/javascript"> + <![CDATA[ + SimpleTest.waitForExplicitFinish(); + SimpleTest.waitForFocus(() => { + let text = document.getElementById("textbox"); + let password = document.getElementById("password"); + + let editor1 = text.editor; + let editor2 = password.editor; + + text.focus(); + text.select(); + + ok(editor1.canCopy(), + "nsIEditor.canCopy() should return true in <input>"); + ok(editor1.canCut(), + "nsIEditor.canCut() should return true in <input>"); + ok(SpecialPowers.isCommandEnabled(window, "cmd_copy"), + "cmd_copy command should be enabled in <input>"); + ok(SpecialPowers.isCommandEnabled(window, "cmd_cut"), + "cmd_cut command should be enabled in <input>"); + ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"), + "cmd_delete command should be enabled in <input>"); + + password.focus(); + password.select(); + + // Copy and cut commands should be disabled on password fields. + ok(!editor2.canCopy(), + "nsIEditor.canCopy() should return false in <input type=password>"); + ok(!editor2.canCut(), + "nsIEditor.canCut() should return false in <input type=password>"); + ok(!SpecialPowers.isCommandEnabled(window, "cmd_copy"), + "cmd_copy command should be disabled in <input type=password>"); + ok(!SpecialPowers.isCommandEnabled(window, "cmd_cut"), + "cmd_cut command should be disabled in <input type=password>"); + ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"), + "cmd_delete command should be enabled in <input type=password>"); + + // If selection is in unmasked range, allow to copy the selected + // password into the clipboard. + editor2.unmask(0); + ok(editor2.canCopy(), + "nsIEditor.canCopy() should return true in <input type=password> if the password is unmasked"); + ok(editor2.canCut(), + "nsIEditor.canCut() should return true in <input type=password> if the password is unmasked"); + ok(SpecialPowers.isCommandEnabled(window, "cmd_copy"), + "cmd_copy command should be enabled in <input type=password> if the password is unmasked"); + ok(SpecialPowers.isCommandEnabled(window, "cmd_cut"), + "cmd_cut command should be enabled in <input type=password> if the password is unmasked"); + ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"), + "cmd_delete command should be enabled in <input type=password> if the password is unmasked"); + + // If unmasked range will be masked automatically, we shouldn't allow to + // copy the selected password since the state may be changed during + // showing edit menu or something. + editor2.unmask(0, 13, 1000); + ok(!editor2.canCopy(), + "nsIEditor.canCopy() should return false in <input type=password> if the password is unmasked but will be masked automatically"); + ok(!editor2.canCut(), + "nsIEditor.canCut() should return false in <input type=password> if the password is unmasked but will be masked automatically"); + ok(!SpecialPowers.isCommandEnabled(window, "cmd_copy"), + "cmd_copy command should be disabled in <input type=password> if the password is unmasked but will be masked automatically"); + ok(!SpecialPowers.isCommandEnabled(window, "cmd_cut"), + "cmd_cut command should be disabled in <input type=password> if the password is unmasked but will be masked automatically"); + ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"), + "cmd_delete command should be enabled in <input type=password> if the password is unmasked but will be masked automatically"); + + // <input type="password"> does not support setSelectionRange() oddly. + function setSelectionRange(aEditor, aStart, aEnd) { + let container = aEditor.rootElement.firstChild; + aEditor.selection.setBaseAndExtent(container, aStart, container, aEnd); + } + + // Check the range boundaries. + editor2.unmask(3, 9); + setSelectionRange(editor2, 0, 2); + ok(!editor2.canCopy(), + "nsIEditor.canCopy() should return false in <input type=password> (unmasked range 3-9, selected range 0-2)"); + ok(!editor2.canCut(), + "nsIEditor.canCut() should return false in <input type=password> (unmasked range 3-9, selected range 0-2)"); + ok(!SpecialPowers.isCommandEnabled(window, "cmd_copy"), + "cmd_copy command should be disabled in <input type=password> (unmasked range 3-9, selected range 0-2)"); + ok(!SpecialPowers.isCommandEnabled(window, "cmd_cut"), + "cmd_cut command should be disabled in <input type=password> (unmasked range 3-9, selected range 0-2)"); + ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"), + "cmd_delete command should be enabled in <input type=password> (unmasked range 3-9, selected range 0-2)"); + + setSelectionRange(editor2, 2, 3); + ok(!editor2.canCopy(), + "nsIEditor.canCopy() should return false in <input type=password> (unmasked range 3-9, selected range 2-3)"); + ok(!editor2.canCut(), + "nsIEditor.canCut() should return false in <input type=password> (unmasked range 3-9, selected range 2-3)"); + ok(!SpecialPowers.isCommandEnabled(window, "cmd_copy"), + "cmd_copy command should be disabled in <input type=password> (unmasked range 3-9, selected range 2-3)"); + ok(!SpecialPowers.isCommandEnabled(window, "cmd_cut"), + "cmd_cut command should be disabled in <input type=password> (unmasked range 3-9, selected range 2-3)"); + ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"), + "cmd_delete command should be enabled in <input type=password> (unmasked range 3-9, selected range 2-3)"); + + setSelectionRange(editor2, 2, 5); + ok(!editor2.canCopy(), + "nsIEditor.canCopy() should return false in <input type=password> (unmasked range 3-9, selected range 2-5)"); + ok(!editor2.canCut(), + "nsIEditor.canCut() should return false in <input type=password> (unmasked range 3-9, selected range 2-5)"); + ok(!SpecialPowers.isCommandEnabled(window, "cmd_copy"), + "cmd_copy command should be disabled in <input type=password> (unmasked range 3-9, selected range 2-5)"); + ok(!SpecialPowers.isCommandEnabled(window, "cmd_cut"), + "cmd_cut command should be disabled in <input type=password> (unmasked range 3-9, selected range 2-5)"); + ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"), + "cmd_delete command should be enabled in <input type=password> (unmasked range 3-9, selected range 2-5)"); + + setSelectionRange(editor2, 2, 10); + ok(!editor2.canCopy(), + "nsIEditor.canCopy() should return false in <input type=password> (unmasked range 3-9, selected range 2-10)"); + ok(!editor2.canCut(), + "nsIEditor.canCut() should return false in <input type=password> (unmasked range 3-9, selected range 2-10)"); + ok(!SpecialPowers.isCommandEnabled(window, "cmd_copy"), + "cmd_copy command should be disabled in <input type=password> (unmasked range 3-9, selected range 2-10)"); + ok(!SpecialPowers.isCommandEnabled(window, "cmd_cut"), + "cmd_cut command should be disabled in <input type=password> (unmasked range 3-9, selected range 2-10)"); + ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"), + "cmd_delete command should be enabled in <input type=password> (unmasked range 3-9, selected range 2-10)"); + + setSelectionRange(editor2, 2, 10); + ok(!editor2.canCopy(), + "nsIEditor.canCopy() should return false in <input type=password> (unmasked range 3-9, selected range 3-10)"); + ok(!editor2.canCut(), + "nsIEditor.canCut() should return false in <input type=password> (unmasked range 3-9, selected range 3-10)"); + ok(!SpecialPowers.isCommandEnabled(window, "cmd_copy"), + "cmd_copy command should be disabled in <input type=password> (unmasked range 3-9, selected range 3-10)"); + ok(!SpecialPowers.isCommandEnabled(window, "cmd_cut"), + "cmd_cut command should be disabled in <input type=password> (unmasked range 3-9, selected range 3-10)"); + ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"), + "cmd_delete command should be enabled in <input type=password> (unmasked range 3-9, selected range 3-10)"); + + setSelectionRange(editor2, 8, 12); + ok(!editor2.canCopy(), + "nsIEditor.canCopy() should return false in <input type=password> (unmasked range 3-9, selected range 8-12)"); + ok(!editor2.canCut(), + "nsIEditor.canCut() should return false in <input type=password> (unmasked range 3-9, selected range 8-12)"); + ok(!SpecialPowers.isCommandEnabled(window, "cmd_copy"), + "cmd_copy command should be disabled in <input type=password> (unmasked range 3-9, selected range 8-12)"); + ok(!SpecialPowers.isCommandEnabled(window, "cmd_cut"), + "cmd_cut command should be disabled in <input type=password> (unmasked range 3-9, selected range 8-12)"); + ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"), + "cmd_delete command should be enabled in <input type=password> (unmasked range 3-9, selected range 8-12)"); + + setSelectionRange(editor2, 9, 12); + ok(!editor2.canCopy(), + "nsIEditor.canCopy() should return false in <input type=password> (unmasked range 3-9, selected range 9-12)"); + ok(!editor2.canCut(), + "nsIEditor.canCut() should return false in <input type=password> (unmasked range 3-9, selected range 9-12)"); + ok(!SpecialPowers.isCommandEnabled(window, "cmd_copy"), + "cmd_copy command should be disabled in <input type=password> (unmasked range 3-9, selected range 9-12)"); + ok(!SpecialPowers.isCommandEnabled(window, "cmd_cut"), + "cmd_cut command should be disabled in <input type=password> (unmasked range 3-9, selected range 9-12)"); + ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"), + "cmd_delete command should be enabled in <input type=password> (unmasked range 3-9, selected range 9-12)"); + + setSelectionRange(editor2, 10, 12); + ok(!editor2.canCopy(), + "nsIEditor.canCopy() should return false in <input type=password> (unmasked range 3-9, selected range 10-12)"); + ok(!editor2.canCut(), + "nsIEditor.canCut() should return false in <input type=password> (unmasked range 3-9, selected range 10-12)"); + ok(!SpecialPowers.isCommandEnabled(window, "cmd_copy"), + "cmd_copy command should be disabled in <input type=password> (unmasked range 3-9, selected range 10-12)"); + ok(!SpecialPowers.isCommandEnabled(window, "cmd_cut"), + "cmd_cut command should be disabled in <input type=password> (unmasked range 3-9, selected range 10-12)"); + ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"), + "cmd_delete command should be enabled in <input type=password> (unmasked range 3-9, selected range 10-12)"); + + setSelectionRange(editor2, 3, 9); + ok(editor2.canCopy(), + "nsIEditor.canCopy() should return true in <input type=password> (unmasked range 3-9, selected range 3-9)"); + ok(editor2.canCut(), + "nsIEditor.canCut() should return true in <input type=password> (unmasked range 3-9, selected range 3-9)"); + ok(SpecialPowers.isCommandEnabled(window, "cmd_copy"), + "cmd_copy command should be enabled in <input type=password> (unmasked range 3-9, selected range 3-9)"); + ok(SpecialPowers.isCommandEnabled(window, "cmd_cut"), + "cmd_cut command should be enabled in <input type=password> (unmasked range 3-9, selected range 3-9)"); + ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"), + "cmd_delete command should be enabled in <input type=password> (unmasked range 3-9, selected range 3-9)"); + + setSelectionRange(editor2, 4, 8); + ok(editor2.canCopy(), + "nsIEditor.canCopy() should return true in <input type=password> (unmasked range 3-9, selected range 4-8)"); + ok(editor2.canCut(), + "nsIEditor.canCut() should return true in <input type=password> (unmasked range 3-9, selected range 4-8)"); + ok(SpecialPowers.isCommandEnabled(window, "cmd_copy"), + "cmd_copy command should be enabled in <input type=password> (unmasked range 3-9, selected range 4-8)"); + ok(SpecialPowers.isCommandEnabled(window, "cmd_cut"), + "cmd_cut command should be enabled in <input type=password> (unmasked range 3-9, selected range 4-8)"); + ok(SpecialPowers.isCommandEnabled(window, "cmd_delete"), + "cmd_delete command should be enabled in <input type=password> (unmasked range 3-9, selected range 4-8)"); + + SimpleTest.finish(); + }); + ]]></script> + + <vbox flex="1"> + <input xmlns="http://www.w3.org/1999/xhtml" id="textbox" value="normal text"/> + <input xmlns="http://www.w3.org/1999/xhtml" id="password" type="password" value="password text"/> + </vbox> + +</window> diff --git a/editor/libeditor/tests/test_cut_copy_password.html b/editor/libeditor/tests/test_cut_copy_password.html new file mode 100644 index 0000000000..4e0319d68c --- /dev/null +++ b/editor/libeditor/tests/test_cut_copy_password.html @@ -0,0 +1,92 @@ +<!doctype html> +<html> +<head> + <title>Test for cut/copy in password field</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <input type="password"> +<script> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(async () => { + let input = document.getElementsByTagName("input")[0]; + let editor = SpecialPowers.wrap(input).editor; + const kMask = editor.passwordMask; + async function copyToClipboard(aExpectedValue) { + try { + await SimpleTest.promiseClipboardChange( + aExpectedValue, () => { SpecialPowers.doCommand(window, "cmd_copy"); }, + undefined, undefined, aExpectedValue === null); + } catch (e) { + console.error(e); + } + } + async function cutToClipboard(aExpectedValue) { + try { + await SimpleTest.promiseClipboardChange( + aExpectedValue, () => { SpecialPowers.doCommand(window, "cmd_cut"); }, + undefined, undefined, aExpectedValue === null); + } catch (e) { + console.error(e); + } + } + input.value = "abcdef"; + input.focus(); + + input.setSelectionRange(0, 6); + ok(true, "Trying to copy masked password..."); + await copyToClipboard(null); + isnot(SpecialPowers.getClipboardData("text/plain"), "abcdef", + "Copying masked password shouldn't copy raw value into the clipboard"); + isnot(SpecialPowers.getClipboardData("text/plain"), `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`, + "Copying masked password shouldn't copy masked value into the clipboard"); + ok(true, "Trying to cut masked password..."); + await cutToClipboard(null); + isnot(SpecialPowers.getClipboardData("text/plain"), "abcdef", + "Cutting masked password shouldn't copy raw value into the clipboard"); + isnot(SpecialPowers.getClipboardData("text/plain"), `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`, + "Cutting masked password shouldn't copy masked value into the clipboard"); + is(input.value, "abcdef", + "Cutting masked password shouldn't modify the value"); + + editor.unmask(2, 4); + input.setSelectionRange(0, 6); + ok(true, "Trying to copy partially masked password..."); + await copyToClipboard(null); + isnot(SpecialPowers.getClipboardData("text/plain"), "abcdef", + "Copying partially masked password shouldn't copy raw value into the clipboard"); + isnot(SpecialPowers.getClipboardData("text/plain"), `${kMask}${kMask}cd${kMask}${kMask}`, + "Copying partially masked password shouldn't copy partially masked value into the clipboard"); + isnot(SpecialPowers.getClipboardData("text/plain"), `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`, + "Copying partially masked password shouldn't copy masked value into the clipboard"); + ok(true, "Trying to cut partially masked password..."); + await cutToClipboard(null); + isnot(SpecialPowers.getClipboardData("text/plain"), "abcdef", + "Cutting partially masked password shouldn't copy raw value into the clipboard"); + isnot(SpecialPowers.getClipboardData("text/plain"), `${kMask}${kMask}cd${kMask}${kMask}`, + "Cutting partially masked password shouldn't copy partially masked value into the clipboard"); + isnot(SpecialPowers.getClipboardData("text/plain"), `${kMask}${kMask}${kMask}${kMask}${kMask}${kMask}`, + "Cutting partially masked password shouldn't copy masked value into the clipboard"); + is(input.value, "abcdef", + "Cutting partially masked password shouldn't modify the value"); + + input.setSelectionRange(2, 4); + ok(true, "Trying to copy unmasked password..."); + await copyToClipboard("cd"); + is(input.value, "abcdef", + "Copying unmasked password shouldn't modify the value"); + + input.value = "012345"; + editor.unmask(2, 4); + input.setSelectionRange(2, 4); + ok(true, "Trying to cut unmasked password..."); + await cutToClipboard("23"); + is(input.value, "0145", + "Cutting unmasked password should modify the value"); + + SimpleTest.finish(); +}); +</script> +</body> +</html> diff --git a/editor/libeditor/tests/test_defaultParagraphSeparatorBR_between_blocks.html b/editor/libeditor/tests/test_defaultParagraphSeparatorBR_between_blocks.html new file mode 100644 index 0000000000..fdb673a6e1 --- /dev/null +++ b/editor/libeditor/tests/test_defaultParagraphSeparatorBR_between_blocks.html @@ -0,0 +1,62 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for insertParagraph when defaultParagraphSeparator is br and between blocks</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + "use strict"; + SimpleTest.waitForExplicitFinish(); + SimpleTest.waitForFocus(() => { + const editor = document.createElement("div"); + editor.setAttribute("contenteditable", ""); + editor.innerHTML = "<div>abc</div><div>def</div>"; + document.body.appendChild(editor); + editor.focus(); + document.execCommand("defaultParagraphSeparator", false, "br"); + getSelection().collapse(editor, 1); // put caret between the <div>s + ok( + document.execCommand("insertParagraph"), + 'execCommand("insertParagraph") should return true' + ) + is( + editor.innerHTML, + "<div>abc</div><br><div>def</div>", + "<br> element should be inserted between the <div> elements" + ); + ok( + getSelection().isCollapsed, + "Selection should be collapsed after insertParagraph" + ); + is( + getSelection().focusNode, + editor, + "Caret should be in the editing host" + ); + is( + getSelection().focusOffset, + 1, + "Caret should be around the <br> element" + ); + is( + SpecialPowers.wrap(getSelection()).interlinePosition, + true, + "Caret should be painted at start of the new line" + ); + document.execCommand("insertText", false, "X"); + todo_is( + editor.innerHTML, + "<div>abc</div><br>X<div>def</div>", + '"X" should be inserted after the inserted <br> element' + ); + SimpleTest.finish(); + }); + </script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_doc_scrollbar_toggled_designMode_on_mousedown.html b/editor/libeditor/tests/test_doc_scrollbar_toggled_designMode_on_mousedown.html new file mode 100644 index 0000000000..860e87424a --- /dev/null +++ b/editor/libeditor/tests/test_doc_scrollbar_toggled_designMode_on_mousedown.html @@ -0,0 +1,64 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=620906 +--> +<head> + <title>Test for Bug 620906</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=620906">Mozilla Bug 620906</a> +<p id="display"></p> +<div id="content"> + <iframe srcdoc= + "<body contenteditable + onmousedown=' + document.designMode="on"; + document.designMode="off"; + ' + > + <div style='height: 1000px;'></div> + </body>"> + </iframe> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 620906 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(async () => { + const iframe = document.querySelector("iframe"); + is(iframe.contentWindow.scrollY, 0, "Sanity check"); + const rect = iframe.getBoundingClientRect(); + const waitForTick = () => { + return new Promise(resolve => requestAnimationFrame( + () => requestAnimationFrame(resolve)) + ); + }; + await waitForTick(); + let scrollEventFired = false; + iframe.contentWindow.addEventListener("scroll", () => { + scrollEventFired = true; + }, {once: true}); + for (let i = 0; i < 10; i++) { + synthesizeMouse(iframe, rect.width - 4, rect.height / 2, { type: "mousemove" }); + synthesizeMouse(iframe, rect.width - 5, rect.height / 2, { type: "mousemove" }); + synthesizeMouse(iframe, rect.width - 5, rect.height / 2, {}); + await waitForTick(); + if (scrollEventFired) { + isnot(iframe.contentWindow.scrollY, 0, "The scrollbar should work"); + SimpleTest.finish(); + return; + } + } + ok(false, "The scrollbar didn't work"); + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_dom_input_event_on_htmleditor.html b/editor/libeditor/tests/test_dom_input_event_on_htmleditor.html new file mode 100644 index 0000000000..60b1678b17 --- /dev/null +++ b/editor/libeditor/tests/test_dom_input_event_on_htmleditor.html @@ -0,0 +1,1694 @@ +<html> +<head> + <title>Test for input event of text editor</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" + href="/tests/SimpleTest/test.css" /> +</head> +<body> +<div id="display"> + <iframe id="editor1" srcdoc="<html><body contenteditable id='eventTarget'></body></html>"></iframe> + <iframe id="editor2" srcdoc="<html contenteditable id='eventTarget'><body></body></html>"></iframe> + <iframe id="editor3" srcdoc="<html><body><div contenteditable id='eventTarget'></div></body></html>"></iframe> + <iframe id="editor4" srcdoc="<html contenteditable id='eventTarget'><body><div contenteditable></div></body></html>"></iframe> + <iframe id="editor5" srcdoc="<html><body id='eventTarget'></body><script>document.designMode='on';</script></html>"></iframe> +</div> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> +"use strict"; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests, window); + +const kIsWin = navigator.platform.indexOf("Win") == 0; +const kIsMac = navigator.platform.indexOf("Mac") == 0; + +function runTests() { + const kWordSelectEatSpaceToNextWord = SpecialPowers.getBoolPref("layout.word_select.eat_space_to_next_word"); + const kImgURL = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAEElEQVR42mNgaGD4D8YwBgAw9AX9Y9zBwwAAAABJRU5ErkJggg=="; + + function doTests(aDocument, aWindow, aDescription) { + aDescription += ": "; + aWindow.focus(); + + let body = aDocument.body; + let selection = aWindow.getSelection(); + + function getHTMLEditor() { + let editingSession = SpecialPowers.wrap(aWindow).docShell.editingSession; + if (!editingSession) { + return null; + } + let editor = editingSession.getEditorForWindow(aWindow); + if (!editor) { + return null; + } + return editor.QueryInterface(SpecialPowers.Ci.nsIHTMLEditor); + } + let htmlEditor = getHTMLEditor(); + + let eventTarget = aDocument.getElementById("eventTarget"); + // The event target must be focusable because it's the editing host. + eventTarget.focus(); + + let editTarget = aDocument.getElementById("editTarget"); + if (!editTarget) { + editTarget = eventTarget; + } + + // Root element never can be edit target. If the editTarget is the root + // element, replace with its body. + if (editTarget == aDocument.documentElement) { + editTarget = body; + } + + editTarget.innerHTML = ""; + + // If the editTarget isn't its editing host, move caret to the start of it. + if (eventTarget != editTarget) { + aDocument.getSelection().collapse(editTarget, 0); + } + + /** + * Tester function. + * + * @param aTestData Class like object to run a set of tests. + * - action: + * Short explanation what it does. + * - cancelBeforeInput: + * true if preventDefault() of "beforeinput" should be + * called. + * @param aFunc Function to run test. + * @param aExpected Object which has: + * - innerHTML [optional]: + * Set string value if the test needs to check content of + * editTarget. + * Set undefined if the test does not need to check it. + * - innerHTMLForCanceled [optional]: + * Set string value if canceling "beforeinput" does not + * keep the value before calling aFunc. + * - beforeInputEvent [optional]: + * Set object which has `cancelable`, `inputType`, `data` and + * `targetRanges` if a "beforeinput" event should be fired. + * If getTargetRanges() should return same array as selection when + * the beforeinput event is dispatched, set `targetRanges` to + * kSameAsSelection. + * Set null if "beforeinput" event shouldn't be fired. + * - inputEvent [optional]: + * Set object which has `inputType` and `data` if an "input" event + * should be fired if aTestData.cancelBeforeInput is not true. + * Set null if "input" event shouldn't be fired. + * Note that if expected "beforeinput" event is cancelable and + * aTestData.cancelBeforeInput is true, this is ignored. + */ + const kSameAsSelection = "same as selection"; + function runTest(aTestData, aFunc, aExpected) { + let beforeInputEvent = null; + let inputEvent = null; + let selectionRanges = []; + let beforeInputHandler = aEvent => { + if (aTestData.cancelBeforeInput) { + aEvent.preventDefault(); + } + ok(!beforeInputEvent, + `${aDescription}Multiple "beforeinput" events are fired at ${aTestData.action} (inputType: "${aEvent.inputType}", data: ${aEvent.data})`); + ok(aEvent.isTrusted, + `${aDescription}"beforeinput" event at ${aTestData.action} must be trusted`); + is(aEvent.target, eventTarget, + `${aDescription}"beforeinput" event at ${aTestData.action} is fired on unexpected element: ${aEvent.target.tagName}`); + is(aEvent.constructor.name, "InputEvent", + `${aDescription}"beforeinput" event at ${aTestData.action} should be dispatched with InputEvent interface`); + ok(aEvent.bubbles, + `${aDescription}"beforeinput" event at ${aTestData.action} must be bubbles`); + beforeInputEvent = aEvent; + selectionRanges = []; + for (let i = 0; i < selection.rangeCount; i++) { + let range = selection.getRangeAt(i); + selectionRanges.push({startContainer: range.startContainer, startOffset: range.startOffset, + endContainer: range.endContainer, endOffset: range.endOffset}); + } + }; + let inputHandler = aEvent => { + ok(!inputEvent, + `${aDescription}Multiple "input" events are fired at ${aTestData.action} (inputType: "${aEvent.inputType}", data: ${aEvent.data})`); + ok(aEvent.isTrusted, + `${aDescription}"input" event at ${aTestData.action} must be trusted`); + is(aEvent.target, eventTarget, + `${aDescription}"input" event at ${aTestData.action} is fired on unexpected element: ${aEvent.target.tagName}`); + is(aEvent.constructor.name, "InputEvent", + `${aDescription}"input" event at ${aTestData.action} should be dispatched with InputEvent interface`); + ok(!aEvent.cancelable, + `${aDescription}"input" event at ${aTestData.action} must not be cancelable`); + ok(aEvent.bubbles, + `${aDescription}"input" event at ${aTestData.action} must be bubbles`); + let duration = Math.abs(window.performance.now() - aEvent.timeStamp); + ok(duration < 30 * 1000, + `${aDescription}perhaps, timestamp wasn't set correctly :${aEvent.timeStamp} (expected it to be within 30s of ` + + `the current time but it differed by ${duration}ms)`); + inputEvent = aEvent; + }; + + try { + aWindow.addEventListener("beforeinput", beforeInputHandler, true); + aWindow.addEventListener("input", inputHandler, true); + + let initialValue = editTarget.innerHTML; + + aFunc(); + + (function verify() { + try { + function checkTargetRanges(aEvent, aTargetRanges) { + let targetRanges = aEvent.getTargetRanges(); + if (aTargetRanges.length === 0) { + is(targetRanges.length, 0, + `${aDescription}getTargetRanges() of "${aEvent.type}" event for ${aTestData.action} should return empty array`); + return; + } + is(targetRanges.length, aTargetRanges.length, + `${aDescription}getTargetRanges() of "${aEvent.type}" event for ${aTestData.action} should return array of static range`); + if (targetRanges.length !== aTargetRanges.length) { + return; + } + for (let i = 0; i < aTargetRanges.length; i++) { + is(targetRanges[i].startContainer, aTargetRanges[i].startContainer, + `${aDescription}startContainer of getTargetRanges()[${i}] of "${aEvent.type}" event for ${aTestData.action} does not match`); + is(targetRanges[i].startOffset, aTargetRanges[i].startOffset, + `${aDescription}startOffset of getTargetRanges()[${i}] of "${aEvent.type}" event for ${aTestData.action} does not match`); + is(targetRanges[i].endContainer, aTargetRanges[i].endContainer, + `${aDescription}endContainer of getTargetRanges()[${i}] of "${aEvent.type}" event for ${aTestData.action} does not match`); + is(targetRanges[i].endOffset, aTargetRanges[i].endOffset, + `${aDescription}endOffset of getTargetRanges()[${i}] of "${aEvent.type}" event for ${aTestData.action} does not match`); + } + } + + if (aExpected.innerHTML !== undefined) { + if (aTestData.cancelBeforeInput && aExpected.innerHTMLForCanceled === undefined) { + is(editTarget.innerHTML, initialValue, + `${aDescription}innerHTML should be "${initialValue}" after ${aTestData.action}`); + } else { + let expectedValue = + aTestData.cancelBeforeInput ? aExpected.innerHTMLForCanceled : aExpected.innerHTML; + is(editTarget.innerHTML, expectedValue, + `${aDescription}innerHTML should be "${expectedValue}" after ${aTestData.action}`); + } + } + if (aExpected.beforeInputEvent === null || aExpected.beforeInputEvent === undefined) { + ok(!beforeInputEvent, + `${aDescription}"beforeinput" event shouldn't have been fired at ${aTestData.action}`); + } else { + ok(beforeInputEvent, + `${aDescription}"beforeinput" event should've been fired at ${aTestData.action}`); + is(beforeInputEvent.cancelable, aExpected.beforeInputEvent.cancelable, + `${aDescription}"beforeinput" event by ${aTestData.action} should be ${ + aExpected.beforeInputEvent.cancelable ? "cancelable" : "not cancelable" + }`); + is(beforeInputEvent.inputType, aExpected.beforeInputEvent.inputType, + `${aDescription}inputType of "beforeinput" event by ${aTestData.action} should be "${aExpected.beforeInputEvent.inputType}"`); + is(beforeInputEvent.data, aExpected.beforeInputEvent.data, + `${aDescription}data of "beforeinput" event by ${aTestData.action} should be ${ + aExpected.beforeInputEvent.data === null ? "null" : `"${aExpected.beforeInputEvent.data}"` + }`); + is(beforeInputEvent.dataTransfer, null, + `${aDescription}dataTransfer of "beforeinput" event by ${aTestData.action} should be null`); + checkTargetRanges( + beforeInputEvent, + aExpected.beforeInputEvent.targetRanges === kSameAsSelection + ? selectionRanges + : aExpected.beforeInputEvent.targetRanges + ); + } + if (( + aTestData.cancelBeforeInput === true && + aExpected.beforeInputEvent && + aExpected.beforeInputEvent.cancelable + ) || aExpected.inputEvent === null || aExpected.inputEvent === undefined) { + ok(!inputEvent, + `${aDescription}"input" event shouldn't have been fired at ${aTestData.action}`); + } else { + ok(inputEvent, + `${aDescription}"input" event should've been fired at ${aTestData.action}`); + is(inputEvent.cancelable, false, + `${aDescription}"input" event by ${aTestData.action} should be not be cancelable`); + is(inputEvent.inputType, aExpected.inputEvent.inputType, + `${aDescription}inputType of "input" event by ${aTestData.action} should be "${aExpected.inputEvent.inputType}"`); + is(inputEvent.data, aExpected.inputEvent.data, + `${aDescription}data of "input" event by ${aTestData.action} should be ${ + aExpected.inputEvent.data === null ? "null" : `"${aExpected.inputEvent.data}"` + }`); + is(inputEvent.dataTransfer, null, + `${aDescription}dataTransfer of "input" event by ${aTestData.action} should be null`); + is(inputEvent.getTargetRanges().length, 0, + `${aDescription}getTargetRanges() of "input" event by ${aTestData.action} should return empty array`); + } + } catch (ex) { + ok(false, `${aDescription}unexpected exception at verifying test result of "${aTestData.action}": ${ex.toString()}`); + } + })(); + } finally { + aWindow.removeEventListener("beforeinput", beforeInputHandler, true); + aWindow.removeEventListener("input", inputHandler, true); + } + } + + (function test_typing_a_in_empty_editor(aTestData) { + editTarget.innerHTML = ""; + editTarget.focus(); + + runTest(aTestData, + () => { + synthesizeKey("a", {}, aWindow); + }, + { + innerHTML: "a", + beforeInputEvent: { + cancelable: true, + inputType: "insertText", + data: "a", + targetRanges: kSameAsSelection, + }, + inputEvent: { + inputType: "insertText", + data: "a", + }, + } + ); + })({ + action: 'typing "a" in empty editor', + }); + + function test_typing_b_at_end_of_editor(aTestData) { + editTarget.innerHTML = "a"; + selection.collapse(editTarget.firstChild, 1); + + runTest( + aTestData, + () => { + synthesizeKey("b", {}, aWindow); + }, + { + innerHTML: "ab", + beforeInputEvent: { + cancelable: true, + inputType: "insertText", + data: "b", + targetRanges: kSameAsSelection, + }, + inputEvent: { + inputType: "insertText", + data: "b", + }, + } + ); + } + test_typing_b_at_end_of_editor({ + action: 'typing "b" after "a" and canceling "beforeinput"', + cancelBeforeInput: true, + }); + test_typing_b_at_end_of_editor({ + action: 'typing "b" after "a"', + cancelBeforeInput: false, + }); + + function test_typing_backspace_to_delete_last_character(aTestData) { + editTarget.innerHTML = "a"; + let textNode = editTarget.firstChild; + selection.collapse(textNode, 1); + + runTest( + aTestData, + () => { + synthesizeKey("KEY_Backspace", {}, aWindow); + }, + { + innerHTML: "<br>", + beforeInputEvent: { + cancelable: true, + inputType: "deleteContentBackward", + data: null, + targetRanges: [ + { + startContainer: textNode, + startOffset: 0, + endContainer: textNode, + endOffset: 1, + }, + ], + }, + inputEvent: { + inputType: "deleteContentBackward", + data: null, + }, + } + ); + } + test_typing_backspace_to_delete_last_character({ + action: 'typing "Backspace" to delete the last character and canceling "beforeinput"', + cancelBeforeInput: true, + }); + test_typing_backspace_to_delete_last_character({ + action: 'typing "Backspace" to delete the last character', + cancelBeforeInput: false, + }); + + (function test_typing_backspace_in_empty_editor(aTestData) { + editTarget.innerHTML = ""; + editTarget.focus(); + + runTest( + aTestData, + () => { + synthesizeKey("KEY_Backspace", {}, aWindow); + }, + { + innerHTML: editTarget.tagName === "DIV" ? "" : "<br>", + beforeInputEvent: { + cancelable: true, + inputType: "deleteContentBackward", + data: null, + targetRanges: kSameAsSelection, + }, + } + ); + })({ + action: 'typing "Backspace" in empty editor', + }); + + function test_typing_enter_at_end_of_editor(aTestData) { + editTarget.innerHTML = "B"; + selection.collapse(editTarget.firstChild, 1); + + runTest( + aTestData, + () => { + synthesizeKey("KEY_Enter", {}, aWindow); + }, + { + innerHTML: "<div>B</div><div><br></div>", + beforeInputEvent: { + cancelable: true, + inputType: "insertParagraph", + data: null, + targetRanges: kSameAsSelection, + }, + inputEvent: { + inputType: "insertParagraph", + data: null, + }, + } + ); + } + test_typing_enter_at_end_of_editor({ + action: 'typing "Enter" at end of editor and canceling "beforeinput"', + cancelBeforeInput: true, + }); + test_typing_enter_at_end_of_editor({ + action: 'typing "Enter" at end of editor', + cancelBeforeInput: false, + }); + + function test_typing_C_in_empty_last_line(aTestData) { + editTarget.innerHTML = "<div>B</div><div><br></div>"; + selection.collapse(editTarget.querySelector("div + div"), 0); + + runTest( + aTestData, + () => { + synthesizeKey("C", {shiftKey: true}, aWindow); + }, + { + innerHTML: "<div>B</div><div>C<br></div>", + beforeInputEvent: { + cancelable: true, + inputType: "insertText", + data: "C", + targetRanges: kSameAsSelection, + }, + inputEvent: { + inputType: "insertText", + data: "C", + }, + } + ); + } + test_typing_C_in_empty_last_line({ + action: 'typing "C" in empty last line and canceling "beforeinput"', + cancelBeforeInput: true, + }); + test_typing_C_in_empty_last_line({ + action: 'typing "C" in empty last line', + cancelBeforeInput: false, + }); + + function test_typing_enter_in_non_empty_last_line(aTestData) { + editTarget.innerHTML = "<div>B</div><div>C<br></div>"; + selection.collapse(editTarget.querySelector("div + div").firstChild, 1); + + runTest( + aTestData, + () => { + synthesizeKey("KEY_Enter", {}, aWindow); + }, + { + innerHTML: "<div>B</div><div>C</div><div><br></div>", + beforeInputEvent: { + cancelable: true, + inputType: "insertParagraph", + data: null, + targetRanges: kSameAsSelection, + }, + inputEvent: { + inputType: "insertParagraph", + data: null, + }, + } + ); + } + test_typing_enter_in_non_empty_last_line({ + action: 'typing "Enter" at end of non-empty line and canceling "beforeinput"', + cancelBeforeInput: true, + }); + test_typing_enter_in_non_empty_last_line({ + action: 'typing "Enter" at end of non-empty line', + cancelBeforeInput: false, + }); + + (function test_setting_innerHTML(aTestData) { + editTarget.innerHTML = ""; + editTarget.focus(); + + runTest( + aTestData, + () => { + editTarget.innerHTML = "foo-bar"; + }, + { innerHTML: "foo-bar" } + ); + })({ + action: "setting innerHTML to non-empty value", + }); + + (function test_setting_innerHTML_to_empty(aTestData) { + editTarget.innerHTML = "foo-bar"; + editTarget.focus(); + + runTest( + aTestData, + () => { + editTarget.innerHTML = ""; + }, + { innerHTML: editTarget.tagName === "DIV" ? "" : "<br>"} + ); + })({ + action: "setting innerHTML to empty value", + }); + + function test_typing_white_space_in_empty_editor(aTestData) { + editTarget.innerHTML = ""; + editTarget.focus(); + + runTest( + aTestData, + () => { + synthesizeKey(" ", {}, aWindow); + }, + { + innerHTML: " ", + beforeInputEvent: { + cancelable: true, + inputType: "insertText", + data: " ", + targetRanges: kSameAsSelection, + }, + inputEvent: { + inputType: "insertText", + data: " ", + }, + } + ); + } + test_typing_white_space_in_empty_editor({ + action: 'typing space in empty editor and canceling "beforeinput"', + cancelBeforeInput: true, + }); + test_typing_white_space_in_empty_editor({ + action: "typing space in empty editor", + cancelBeforeInput: false, + }); + + (function test_typing_delete_at_end_of_editor(aTestData) { + editTarget.innerHTML = " "; + selection.collapse(editTarget.firstChild, 1); + + runTest( + aTestData, + () => { + synthesizeKey("KEY_Delete", {}, aWindow); + }, + { + innerHTML: " ", + beforeInputEvent: { + cancelable: true, + inputType: "deleteContentForward", + data: null, + targetRanges: kSameAsSelection, + }, + } + ); + })({ + action: 'typing "Delete" at end of editor', + }); + + (function test_typing_arrow_left_to_move_caret(aTestData) { + editTarget.innerHTML = " "; + selection.collapse(editTarget.firstChild, 1); + + runTest( + aTestData, + () => { + synthesizeKey("KEY_ArrowLeft", {}, aWindow); + }, + { innerHTML: " " } + ); + })({ + action: 'typing "ArrowLeft" to move caret', + }); + + function test_typing_delete_to_delete_last_character(aTestData) { + editTarget.innerHTML = "\u00A0"; + let textNode = editTarget.firstChild; + selection.collapse(textNode, 0); + + runTest( + aTestData, + () => { + synthesizeKey("KEY_Delete", {}, aWindow); + }, + { + innerHTML: "<br>", + beforeInputEvent: { + cancelable: true, + inputType: "deleteContentForward", + data: null, + targetRanges: [ + { + startContainer: textNode, + startOffset: 0, + endContainer: textNode, + endOffset: 1, + }, + ], + }, + inputEvent: { + inputType: "deleteContentForward", + data: null, + }, + } + ); + } + test_typing_delete_to_delete_last_character({ + action: 'typing "Delete" to delete last character (NBSP) and canceling "beforeinput"', + cancelBeforeInput: true, + }); + test_typing_delete_to_delete_last_character({ + action: 'typing "Delete" to delete last character (NBSP)', + cancelBeforeInput: false, + }); + + function test_undoing_deleting_last_character(aTestData) { + editTarget.innerHTML = "\u00A0"; + selection.collapse(editTarget.firstChild, 0); + synthesizeKey("KEY_Delete", {}, aWindow); + + runTest( + aTestData, + () => { + synthesizeKey("z", {accelKey: true}, aWindow); + }, + { + innerHTML: " ", + beforeInputEvent: { + cancelable: true, + inputType: "historyUndo", + data: null, + targetRanges: [], + }, + inputEvent: { + inputType: "historyUndo", + data: null, + }, + } + ); + } + test_undoing_deleting_last_character({ + action: 'undoing deleting last character and canceling "beforeinput"', + cancelBeforeInput: true, + }); + test_undoing_deleting_last_character({ + action: 'undoing deleting last character', + cancelBeforeInput: false, + }); + + (function test_undoing_without_undoable_transaction(aTestData) { + htmlEditor.enableUndo(false); + htmlEditor.enableUndo(true); + editTarget.innerHTML = "\u00A0"; + selection.collapse(editTarget.firstChild, 0); + synthesizeKey("KEY_Delete", {}, aWindow); + synthesizeKey("z", {accelKey: true}, aWindow); + + runTest( + aTestData, + () => { + synthesizeKey("z", {accelKey: true}, aWindow); + }, + { innerHTML: " " } + ); + })({ + action: "trying to undo without undoable transaction", + }); + + function test_redoing_deleting_last_character(aTestData) { + htmlEditor.enableUndo(false); + htmlEditor.enableUndo(true); + editTarget.innerHTML = "\u00A0"; + selection.collapse(editTarget.firstChild, 0); + synthesizeKey("KEY_Delete", {}, aWindow); + synthesizeKey("z", {accelKey: true}, aWindow); + + runTest( + aTestData, + () => { + synthesizeKey("z", {accelKey: true, shiftKey: true}, aWindow); + }, + { + innerHTML: "<br>", + beforeInputEvent: { + cancelable: true, + inputType: "historyRedo", + data: null, + targetRanges: [], + }, + inputEvent: { + inputType: "historyRedo", + data: null, + }, + } + ); + } + test_redoing_deleting_last_character({ + action: 'redoing deleting last character and canceling "beforeinput"', + cancelBeforeInput: true, + }); + test_redoing_deleting_last_character({ + action: 'redoing deleting last character', + cancelBeforeInput: false, + }); + + (function test_redoing_without_redoable_transaction(aTestData) { + htmlEditor.enableUndo(false); + htmlEditor.enableUndo(true); + editTarget.innerHTML = "\u00A0"; + selection.collapse(editTarget.firstChild, 0); + synthesizeKey("KEY_Delete", {}, aWindow); + synthesizeKey("z", {accelKey: true}, aWindow); + synthesizeKey("z", {accelKey: true, shiftKey: true}, aWindow); + + runTest( + aTestData, + () => { + synthesizeKey("z", {accelKey: true, shiftKey: true}, aWindow); + }, + { innerHTML: "<br>" } + ); + })({ + action: "trying to redo without redoable transaction", + }); + + function test_inserting_linebreak(aTestData) { + editTarget.innerHTML = "<br>"; + selection.collapse(editTarget, 0); + + runTest( + aTestData, + () => { + synthesizeKey("KEY_Enter", {shiftKey: true}, aWindow); + }, + { + innerHTML: "<br><br>", + beforeInputEvent: { + cancelable: true, + inputType: "insertLineBreak", + data: null, + targetRanges: kSameAsSelection, + }, + inputEvent: { + inputType: "insertLineBreak", + data: null, + }, + } + ); + } + test_inserting_linebreak({ + action: 'inserting a linebreak and canceling "beforeinput"', + cancelBeforeInput: true, + }); + test_inserting_linebreak({ + action: "inserting a linebreak", + cancelBeforeInput: false, + }); + + function test_typing_backspace_to_delete_selected_characters(aTestData) { + editTarget.innerHTML = "a"; + selection.selectAllChildren(editTarget); + + let expectedTargetRanges = [ + { + startContainer: editTarget.firstChild, + startOffset: 0, + endContainer: editTarget.firstChild, + endOffset: editTarget.firstChild.length, + }, + ]; + runTest( + aTestData, + () => { + synthesizeKey("KEY_Backspace", {}, aWindow); + }, + { + innerHTML: "<br>", + beforeInputEvent: { + cancelable: true, + inputType: "deleteContentBackward", + data: null, + targetRanges: expectedTargetRanges, + }, + inputEvent: { + inputType: "deleteContentBackward", + data: null, + }, + } + ); + } + test_typing_backspace_to_delete_selected_characters({ + action: 'typing "Backspace" to delete selected characters and canceling "beforeinput"', + cancelBeforeInput: true, + }); + test_typing_backspace_to_delete_selected_characters({ + action: 'typing "Backspace" to delete selected characters', + cancelBeforeInput: false, + }); + + function test_typing_delete_to_delete_selected_characters(aTestData) { + editTarget.innerHTML = "a"; + selection.selectAllChildren(editTarget); + + let expectedTargetRanges = [ + { + startContainer: editTarget.firstChild, + startOffset: 0, + endContainer: editTarget.firstChild, + endOffset: editTarget.firstChild.length, + }, + ]; + runTest( + aTestData, + () => { + synthesizeKey("KEY_Delete", {}, aWindow); + }, + { + innerHTML: "<br>", + beforeInputEvent: { + cancelable: true, + inputType: "deleteContentForward", + data: null, + targetRanges: expectedTargetRanges, + }, + inputEvent: { + inputType: "deleteContentForward", + data: null, + }, + } + ); + } + test_typing_delete_to_delete_selected_characters({ + action: 'typing "Delete" to delete selected characters and canceling "beforeinput"', + cancelBeforeInput: true, + }); + test_typing_delete_to_delete_selected_characters({ + action: 'typing "Delete" to delete selected characters', + cancelBeforeInput: false, + }); + + function test_deleting_word_backward_from_its_end(aTestData) { + editTarget.innerHTML = "abc def"; + let textNode = editTarget.firstChild; + selection.collapse(textNode, "abc def".length); + + let expectedTargetRanges = [ + { + startContainer: textNode, + startOffset: "abc ".length, + endContainer: textNode, + endOffset: textNode.length, + }, + ]; + runTest( + aTestData, + () => { + SpecialPowers.doCommand(aWindow, "cmd_deleteWordBackward"); + }, + { + innerHTML: "abc ", + beforeInputEvent: { + cancelable: true, + inputType: "deleteWordBackward", + data: null, + targetRanges: expectedTargetRanges, + }, + inputEvent: { + inputType: "deleteWordBackward", + data: null, + }, + } + ); + } + test_deleting_word_backward_from_its_end({ + action: 'deleting word backward from its end and canceling "beforeinput"', + cancelBeforeInput: true, + }); + test_deleting_word_backward_from_its_end({ + action: "deleting word backward from its end", + cancelBeforeInput: false, + }); + + function test_deleting_word_forward_from_its_start(aTestData) { + editTarget.innerHTML = "abc def"; + let textNode = editTarget.firstChild; + selection.collapse(textNode, 0); + + let expectedTargetRanges = [ + { + startContainer: textNode, + startOffset: 0, + endContainer: textNode, + endOffset: kWordSelectEatSpaceToNextWord ? "abc ".length : "abc".length, + }, + ]; + runTest( + aTestData, + () => { + SpecialPowers.doCommand(aWindow, "cmd_deleteWordForward"); + }, + { + innerHTML: kWordSelectEatSpaceToNextWord ? "def" : " def", + beforeInputEvent: { + cancelable: true, + inputType: "deleteWordForward", + data: null, + targetRanges: expectedTargetRanges, + }, + inputEvent: { + inputType: "deleteWordForward", + data: null, + }, + } + ); + } + test_deleting_word_forward_from_its_start({ + action: 'deleting word forward from its start and canceling "beforeinput"', + cancelBeforeInput: true, + }); + test_deleting_word_forward_from_its_start({ + action: "deleting word forward from its start", + cancelBeforeInput: false, + }); + + (function test_deleting_word_backward_from_middle_of_second_word(aTestData) { + editTarget.innerHTML = "abc def"; + let textNode = editTarget.firstChild; + selection.setBaseAndExtent(textNode, "abc d".length, textNode, "abc de".length); + + let expectedTargetRanges = [ + { + startContainer: textNode, + startOffset: kIsWin ? "abc ".length : "abc d".length, + endContainer: textNode, + endOffset: kIsWin ? "abc d".length : "abc de".length, + }, + ]; + runTest( + aTestData, + () => { + SpecialPowers.doCommand(aWindow, "cmd_deleteWordBackward"); + }, + { + // Only on Windows, we collapse selection to start before handling this command. + innerHTML: kIsWin ? "abc ef" : "abc df", + beforeInputEvent: { + cancelable: true, + inputType: kIsWin ? "deleteWordBackward" : "deleteContentBackward", + data: null, + targetRanges: expectedTargetRanges, + }, + inputEvent: { + inputType: kIsWin ? "deleteWordBackward" : "deleteContentBackward", + data: null, + }, + } + ); + })({ + action: "removing characters backward from middle of second word", + }); + + (function test_deleting_word_forward_from_middle_of_first_word(aTestData) { + editTarget.innerHTML = "abc def"; + let textNode = editTarget.firstChild; + selection.setBaseAndExtent(textNode, "a".length, textNode, "ab".length); + + let expectedTargetRanges = [ + { + startContainer: textNode, + startOffset: "a".length, + endContainer: textNode, + endOffset: (function expectedEndOffset() { + if (!kIsWin) { + return "ab".length; + } + return kWordSelectEatSpaceToNextWord ? "abc ".length : "abc".length; + })(), + }, + ]; + runTest( + aTestData, + () => { + SpecialPowers.doCommand(aWindow, "cmd_deleteWordForward"); + }, + { + // Only on Windows, we collapse selection to start before handling this command. + innerHTML: (function () { + if (!kIsWin) { + return "ac def"; + } + return kWordSelectEatSpaceToNextWord ? "adef" : "a def"; + })(), + beforeInputEvent: { + cancelable: true, + inputType: kIsWin ? "deleteWordForward" : "deleteContentForward", + data: null, + targetRanges: expectedTargetRanges, + }, + inputEvent: { + inputType: kIsWin ? "deleteWordForward" : "deleteContentForward", + data: null, + }, + } + ); + })({ + action: "removing characters forward from middle of first word", + }); + + (function test_deleting_characters_backward_to_start_of_line(aTestData) { + editTarget.innerHTML = "abc def"; + let textNode = editTarget.firstChild; + selection.collapse(textNode, "abc d".length); + + let expectedTargetRanges = [ + { + startContainer: textNode, + startOffset: 0, + endContainer: textNode, + endOffset: "abc d".length, + }, + ]; + runTest( + aTestData, + () => { + SpecialPowers.doCommand(aWindow, "cmd_deleteToBeginningOfLine"); + }, + { + innerHTML: "ef", + beforeInputEvent: { + cancelable: true, + inputType: "deleteSoftLineBackward", + data: null, + targetRanges: expectedTargetRanges, + }, + inputEvent: { + inputType: "deleteSoftLineBackward", + data: null, + }, + } + ); + })({ + action: "removing characters backward to start of line", + }); + + (function test_deleting_characters_forward_to_end_of_line(aTestData) { + editTarget.innerHTML = "abc def"; + let textNode = editTarget.firstChild; + selection.collapse(textNode, "ab".length); + + let expectedTargetRanges = [ + { + startContainer: textNode, + startOffset: "ab".length, + endContainer: textNode, + endOffset: "abc def".length, + }, + ]; + runTest( + aTestData, + () => { + SpecialPowers.doCommand(aWindow, "cmd_deleteToEndOfLine"); + }, + { + innerHTML: "ab", + beforeInputEvent: { + cancelable: true, + inputType: "deleteSoftLineForward", + data: null, + targetRanges: expectedTargetRanges, + }, + inputEvent: { + inputType: "deleteSoftLineForward", + data: null, + }, + } + ); + })({ + action: "removing characters forward to end of line", + }); + + (function test_deleting_characters_backward_to_start_of_line_with_non_collapsed_selection(aTestData) { + editTarget.innerHTML = "abc def"; + let textNode = editTarget.firstChild; + selection.setBaseAndExtent(textNode, "abc d".length, textNode, "abc_de".length); + + let expectedTargetRanges = [ + { + startContainer: textNode, + startOffset: kIsWin ? 0 : "abc d".length, + endContainer: textNode, + endOffset: kIsWin ? "abc d".length : "abc de".length, + }, + ]; + runTest( + aTestData, + () => { + SpecialPowers.doCommand(aWindow, "cmd_deleteToBeginningOfLine"); + }, + { + // Only on Windows, we collapse selection to start before handling this command. + innerHTML: kIsWin ? "ef" : "abc df", + beforeInputEvent: { + cancelable: true, + inputType: kIsWin ? "deleteSoftLineBackward" : "deleteContentBackward", + data: null, + targetRanges: expectedTargetRanges, + }, + inputEvent: { + inputType: kIsWin ? "deleteSoftLineBackward" : "deleteContentBackward", + data: null, + }, + } + ); + })({ + action: "removing characters backward to start of line (with selection in second word)", + }); + + (function test_deleting_characters_forward_to_end_of_line_with_non_collapsed_selection(aTestData) { + editTarget.innerHTML = "abc def"; + let textNode = editTarget.firstChild; + selection.setBaseAndExtent(textNode, "a".length, textNode, "ab".length); + + let expectedTargetRanges = [ + { + startContainer: textNode, + startOffset: "a".length, + endContainer: textNode, + endOffset: kIsWin ? "abc def".length : "ab".length, + }, + ]; + runTest( + aTestData, + () => { + SpecialPowers.doCommand(aWindow, "cmd_deleteToEndOfLine"); + }, + { + // Only on Windows, we collapse selection to start before handling this command. + innerHTML: kIsWin ? "a" : "ac def", + beforeInputEvent: { + cancelable: true, + inputType: kIsWin ? "deleteSoftLineForward" : "deleteContentForward", + data: null, + targetRanges: expectedTargetRanges, + }, + inputEvent: { + inputType: kIsWin ? "deleteSoftLineForward" : "deleteContentForward", + data: null, + }, + } + ); + })({ + action: "removing characters forward to end of line (with selection in second word)", + }); + + function test_switching_text_direction_from_default(aTestData) { + const editingHost = aDocument.getElementById("editTarget") || aDocument.getElementById("eventTarget"); + try { + editingHost.removeAttribute("dir"); + htmlEditor.flags &= ~SpecialPowers.Ci.nsIEditor.eEditorRightToLeft; + htmlEditor.flags |= SpecialPowers.Ci.nsIEditor.eEditorLeftToRight; // XXX flags update is required, must be a bug. + aDocument.documentElement.scrollTop; // XXX Update the body frame + editTarget.focus(); + + runTest( + aTestData, + () => { + SpecialPowers.doCommand(aWindow, "cmd_switchTextDirection"); + if (aTestData.cancelBeforeInput) { + is(editingHost.getAttribute("dir"), null, + `${aDescription}dir attribute of the element shouldn't have been set by ${aTestData.action}`); + } else { + is(editingHost.getAttribute("dir"), "rtl", + `${aDescription}dir attribute of the element should've been set to "rtl" by ${aTestData.action}`); + } + }, + { + beforeInputEvent: { + cancelable: true, + inputType: "formatSetBlockTextDirection", + data: "rtl", + targetRanges: [], + }, + inputEvent: { + inputType: "formatSetBlockTextDirection", + data: "rtl", + }, + } + ); + } finally { + editingHost.removeAttribute("dir"); + htmlEditor.flags &= ~SpecialPowers.Ci.nsIEditor.eEditorRightToLeft; + htmlEditor.flags |= SpecialPowers.Ci.nsIEditor.eEditorLeftToRight; // XXX flags update is required, must be a bug. + aDocument.documentElement.scrollTop; // XXX Update the body frame + } + } + test_switching_text_direction_from_default({ + action: 'switching text direction from default to "rtl" and canceling "beforeinput"', + cancelBeforeInput: true, + }); + test_switching_text_direction_from_default({ + action: 'switching text direction from default to "rtl"', + cancelBeforeInput: false, + }); + + function test_switching_text_direction_from_rtl_to_ltr(aTestData) { + const editingHost = aDocument.getElementById("editTarget") || aDocument.getElementById("eventTarget"); + try { + editingHost.setAttribute("dir", "rtl"); + htmlEditor.flags &= ~SpecialPowers.Ci.nsIEditor.eEditorLeftToRight; + htmlEditor.flags |= SpecialPowers.Ci.nsIEditor.eEditorRightToLeft; // XXX flags update is required, must be a bug. + aDocument.documentElement.scrollTop; // XXX Update the body frame + editTarget.focus(); + + runTest( + aTestData, + () => { + SpecialPowers.doCommand(aWindow, "cmd_switchTextDirection"); + let expectedDirValue = aTestData.cancelBeforeInput ? "rtl" : "ltr"; + is(editingHost.getAttribute("dir"), expectedDirValue, + `${aDescription}dir attribute of the element should be "${expectedDirValue}" after ${aTestData.action}`); + }, + { + beforeInputEvent: { + cancelable: true, + inputType: "formatSetBlockTextDirection", + data: "ltr", + targetRanges: [], + }, + inputEvent: { + inputType: "formatSetBlockTextDirection", + data: "ltr", + }, + } + ); + } finally { + editingHost.removeAttribute("dir"); + htmlEditor.flags &= ~SpecialPowers.Ci.nsIEditor.eEditorRightToLeft; + htmlEditor.flags |= SpecialPowers.Ci.nsIEditor.eEditorLeftToRight; // XXX flags update is required, must be a bug. + aDocument.documentElement.scrollTop; // XXX Update the body frame + } + } + test_switching_text_direction_from_rtl_to_ltr({ + action: 'switching text direction from "rtl" to "ltr" and canceling "beforeinput"', + cancelBeforeInput: true, + }); + test_switching_text_direction_from_rtl_to_ltr({ + action: 'switching text direction from "rtl" to "ltr" and canceling "beforeinput"', + cancelBeforeInput: false, + }); + + + function test_inserting_link(aTestData) { + editTarget.innerHTML = "link"; + selection.selectAllChildren(editTarget); + + runTest( + aTestData, + () => { + SpecialPowers.doCommand(aWindow, "cmd_insertLinkNoUI", "https://example.com/foo/bar.html"); + }, + { + innerHTML: '<a href="https://example.com/foo/bar.html">link</a>', + beforeInputEvent: { + cancelable: true, + inputType: "insertLink", + data: "https://example.com/foo/bar.html", + targetRanges: kSameAsSelection, + }, + inputEvent: { + inputType: "insertLink", + data: "https://example.com/foo/bar.html", + }, + } + ); + } + test_inserting_link({ + action: 'setting link with absolute URL and canceling "beforeinput"', + cancelBeforeInput: true, + }); + test_inserting_link({ + action: "setting link with absolute URL", + cancelBeforeInput: false, + }); + + (function test_inserting_link_with_relative_url(aTestData) { + editTarget.innerHTML = "link"; + selection.selectAllChildren(editTarget); + + runTest( + aTestData, + () => { + SpecialPowers.doCommand(aWindow, "cmd_insertLinkNoUI", "foo/bar.html"); + }, + { + innerHTML: '<a href="foo/bar.html">link</a>', + beforeInputEvent: { + cancelable: true, + inputType: "insertLink", + data: "foo/bar.html", + targetRanges: kSameAsSelection, + }, + inputEvent: { + inputType: "insertLink", + data: "foo/bar.html", + }, + } + ); + })({ + action: "setting link with relative URL", + }); + + (function test_format_commands() { + for (let test of [{command: "cmd_bold", + tag: "b", + otherRemoveTags: ["strong"], + inputType: "formatBold"}, + {command: "cmd_italic", + tag: "i", + otherRemoveTags: ["em"], + inputType: "formatItalic"}, + {command: "cmd_underline", + tag: "u", + inputType: "formatUnderline"}, + {command: "cmd_strikethrough", + tag: "strike", + otherRemoveTags: ["s"], + inputType: "formatStrikeThrough"}, + {command: "cmd_subscript", + tag: "sub", + exclusiveTags: ["sup"], + inputType: "formatSubscript"}, + {command: "cmd_superscript", + tag: "sup", + exclusiveTags: ["sub"], + inputType: "formatSuperscript"}]) { + function test_formatting_text(aTestData) { + editTarget.innerHTML = "format"; + selection.selectAllChildren(editTarget); + + runTest( + aTestData, + () => { + SpecialPowers.doCommand(aWindow, test.command); + }, + { + innerHTML: `<${test.tag}>format</${test.tag}>`, + beforeInputEvent: { + cancelable: true, + inputType: test.inputType, + data: null, + targetRanges: kSameAsSelection, + }, + inputEvent: { + inputType: test.inputType, + data: null, + }, + } + ); + } + test_formatting_text({ + action: `formatting with "${test.command}" and canceling "beforeinput"`, + cancelBeforeInput: true, + }); + test_formatting_text({ + action: `formatting with "${test.command}" and canceling "beforeinput"`, + cancelBeforeInput: false, + }); + + function test_removing_format_text(aTestData) { + editTarget.innerHTML = `<${test.tag}>format</${test.tag}>`; + selection.selectAllChildren(editTarget); + + runTest( + aTestData, + () => { + SpecialPowers.doCommand(aWindow, test.command); + }, + { + innerHTML: "format", + beforeInputEvent: { + cancelable: true, + inputType: test.inputType, + data: null, + targetRanges: kSameAsSelection, + }, + inputEvent: { + inputType: test.inputType, + data: null, + }, + } + ); + } + test_removing_format_text({ + action: `removing format with "${test.command}" and canceling "beforeinput"`, + cancelBeforeInput: true, + }); + test_removing_format_text({ + action: `removing format with "${test.command}" and canceling "beforeinput"`, + cancelBeforeInput: false, + }); + + (function test_removing_format_styled_by_others() { + if (!test.otherRemoveTags) { + return; + } + for (let anotherTag of test.otherRemoveTags) { + function test_removing_format_styled_by_another_element(aTestData) { + editTarget.innerHTML = `<${anotherTag}>format</${anotherTag}>`; + selection.selectAllChildren(editTarget); + + runTest( + aTestData, + () => { + SpecialPowers.doCommand(aWindow, test.command); + }, + { + innerHTML: "format", + beforeInputEvent: { + cancelable: true, + inputType: test.inputType, + data: null, + targetRanges: kSameAsSelection, + }, + inputEvent: { + inputType: test.inputType, + data: null, + }, + } + ); + } + test_removing_format_styled_by_another_element({ + action: `removing <${anotherTag}> element with "${test.command}" and canceling "beforeinput"`, + cancelBeforeInput: true, + }); + test_removing_format_styled_by_another_element({ + action: `removing <${anotherTag}> element with "${test.command}"`, + cancelBeforeInput: false, + }); + + function test_removing_format_styled_by_both_primary_one_and_another_one(aTestData) { + editTarget.innerHTML = `<${test.tag}><${anotherTag}>format</${anotherTag}></${test.tag}>`; + selection.selectAllChildren(editTarget); + + runTest( + aTestData, + () => { + SpecialPowers.doCommand(aWindow, test.command); + }, + { + innerHTML: "format", + beforeInputEvent: { + cancelable: true, + inputType: test.inputType, + data: null, + targetRanges: kSameAsSelection, + }, + inputEvent: { + inputType: test.inputType, + data: null, + }, + } + ); + } + test_removing_format_styled_by_both_primary_one_and_another_one({ + action: `removing both <${test.tag}> and <${anotherTag}> elements with "${test.command}" and canceling "beforeinput"`, + cancelBeforeInput: true, + }); + test_removing_format_styled_by_both_primary_one_and_another_one({ + action: `removing both <${test.tag}> and <${anotherTag}> elements with "${test.command}"`, + cancelBeforeInput: false, + }); + } + })(); + (function test_formatting_text_styled_by_exclusive_elements() { + if (!test.exclusiveTags) { + return; + } + for (let exclusiveTag of test.exclusiveTags) { + function test_formatting_text_styled_by_exclusive_element(aTestData) { + editTarget.innerHTML = `<${exclusiveTag}>format</${exclusiveTag}>`; + selection.selectAllChildren(editTarget); + + runTest( + aTestData, + () => { + SpecialPowers.doCommand(aWindow, test.command); + }, + { + innerHTML: `<${test.tag}>format</${test.tag}>`, + beforeInputEvent: { + cancelable: true, + inputType: test.inputType, + data: null, + targetRanges: kSameAsSelection, + }, + inputEvent: { + inputType: test.inputType, + data: null, + }, + } + ); + } + test_formatting_text_styled_by_exclusive_element({ + action: `removing <${exclusiveTag}> element with formatting with "${test.command}" and canceling "beforeinput"`, + cancelBeforeInput: true, + }); + test_formatting_text_styled_by_exclusive_element({ + action: `removing <${exclusiveTag}> element with formatting with "${test.command}"`, + cancelBeforeInput: false, + }); + } + })(); + } + })(); + + function test_indenting_text(aTestData) { + editTarget.innerHTML = "format"; + selection.selectAllChildren(editTarget); + + runTest( + aTestData, + () => { + SpecialPowers.doCommand(aWindow, "cmd_indent"); + }, + { + innerHTML: "<blockquote>format</blockquote>", + beforeInputEvent: { + cancelable: true, + inputType: "formatIndent", + data: null, + targetRanges: kSameAsSelection, + }, + inputEvent: { + inputType: "formatIndent", + data: null, + }, + } + ); + } + test_indenting_text({ + action: 'indenting text and canceling "beforeinput"', + cancelBeforeInput: true, + }); + test_indenting_text({ + action: 'indenting text', + cancelBeforeInput: false, + }); + + function test_outdenting_blockquote(aTestData) { + editTarget.innerHTML = "<blockquote>format</blockquote>"; + selection.selectAllChildren(editTarget.firstChild); + + runTest( + aTestData, + () => { + SpecialPowers.doCommand(aWindow, "cmd_outdent"); + }, + { + innerHTML: "format", + beforeInputEvent: { + cancelable: true, + inputType: "formatOutdent", + data: null, + targetRanges: kSameAsSelection, + }, + inputEvent: { + inputType: "formatOutdent", + data: null, + }, + } + ); + } + test_outdenting_blockquote({ + action: 'outdenting blockquote and canceling "beforeinput"', + cancelBeforeInput: true, + }); + test_outdenting_blockquote({ + action: 'outdenting blockquote', + cancelBeforeInput: false, + }); + + function test_typing_delete_to_delete_img(aTestData) { + editTarget.innerHTML = `<img src="${kImgURL}">`; + selection.collapse(editTarget, 0); + + runTest( + aTestData, + () => { + synthesizeKey("KEY_Delete", {}, aWindow); + }, + { + innerHTML: "<br>", + beforeInputEvent: { + cancelable: true, + inputType: "deleteContentForward", + data: null, + targetRanges: [ + { + startContainer: editTarget, + startOffset: 0, + endContainer: editTarget, + endOffset: 1, + }, + ], + }, + inputEvent: { + inputType: "deleteContentForward", + data: null, + }, + } + ); + } + test_typing_delete_to_delete_img({ + action: 'typing "Delete" to delete the <img> element and canceling "beforeinput"', + cancelBeforeInput: true, + }); + test_typing_delete_to_delete_img({ + action: 'typing "Delete" to delete the <img> element', + cancelBeforeInput: false, + }); + + function test_typing_backspace_to_delete_img(aTestData) { + editTarget.innerHTML = `<img src="${kImgURL}">`; + selection.collapse(editTarget, 1); + + runTest( + aTestData, + () => { + synthesizeKey("KEY_Backspace", {}, aWindow); + }, + { + innerHTML: "<br>", + beforeInputEvent: { + cancelable: true, + inputType: "deleteContentBackward", + data: null, + targetRanges: [ + { + startContainer: editTarget, + startOffset: 0, + endContainer: editTarget, + endOffset: 1, + }, + ], + }, + inputEvent: { + inputType: "deleteContentBackward", + data: null, + }, + } + ); + } + test_typing_backspace_to_delete_img({ + action: 'typing "Backspace" to delete the <img> element and canceling "beforeinput"', + cancelBeforeInput: true, + }); + test_typing_backspace_to_delete_img({ + action: 'typing "Backspace" to delete the <img> element', + cancelBeforeInput: false, + }); + } + + doTests(document.getElementById("editor1").contentDocument, + document.getElementById("editor1").contentWindow, + "Editor1, body has contenteditable attribute"); + doTests(document.getElementById("editor2").contentDocument, + document.getElementById("editor2").contentWindow, + "Editor2, html has contenteditable attribute"); + doTests(document.getElementById("editor3").contentDocument, + document.getElementById("editor3").contentWindow, + "Editor3, div has contenteditable attribute"); + doTests(document.getElementById("editor4").contentDocument, + document.getElementById("editor4").contentWindow, + "Editor4, html and div have contenteditable attribute"); + doTests(document.getElementById("editor5").contentDocument, + document.getElementById("editor5").contentWindow, + "Editor5, html and div have contenteditable attribute"); + + SimpleTest.finish(); +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_dom_input_event_on_texteditor.html b/editor/libeditor/tests/test_dom_input_event_on_texteditor.html new file mode 100644 index 0000000000..e3d9eb3e5c --- /dev/null +++ b/editor/libeditor/tests/test_dom_input_event_on_texteditor.html @@ -0,0 +1,926 @@ +<html> +<head> + <title>Test for input event of text editor</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" + href="/tests/SimpleTest/test.css" /> +</head> +<body> +<div id="display"> + <input type="text" id="input"> + <textarea id="textarea"></textarea> +</div> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> +"use strict"; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.expectAssertions(0, 1); // In a11y module +SimpleTest.waitForFocus(runTests, window); + +function runTests() { + const kWordSelectEatSpaceToNextWord = SpecialPowers.getBoolPref("layout.word_select.eat_space_to_next_word"); + + function doTests(aElement, aDescription, aIsTextarea) { + aDescription += ": "; + aElement.focus(); + aElement.value = ""; + + /** + * Tester function. + * + * @param aTestData Class like object to run a set of tests. + * - action: + * Short explanation what it does. + * - cancelBeforeInput: + * true if preventDefault() of "beforeinput" should be + * called. + * @param aFunc Function to run test. + * @param aExpected Object which has: + * - value [optional]: + * Set string value if the test needs to check value of + * aElement. + * Set undefined if the test does not need to check it. + * - valueForCanceled [optional]: + * Set string value if canceling "beforeinput" does not + * keep the value before calling aFunc. + * - beforeInputEvent [optional]: + * Set object which has `cancelable`, `inputType` and `data` + * if a "beforeinput" event should be fired. + * Set null if "beforeinput" event shouldn't be fired. + * - inputEvent [optional]: + * Set object which has `inputType` and `data` if an "input" event + * should be fired if aTestData.cancelBeforeInput is not true. + * Set null if "input" event shouldn't be fired. + * Note that if expected "beforeinput" event is cancelable and + * aTestData.cancelBeforeInput is true, this is ignored. + */ + function runTest(aTestData, aFunc, aExpected) { + let initializing = false; + let beforeInputEvent = null; + let inputEvent = null; + let beforeInputHandler = (aEvent) => { + if (initializing) { + return; + } + ok(!beforeInputEvent, + `${aDescription}Multiple "beforeinput" events are fired at ${aTestData.action} (inputType: "${aEvent.inputType}", data: ${aEvent.data})`); + if (aTestData.cancelBeforeInput) { + aEvent.preventDefault(); + } + ok(aEvent.isTrusted, + `${aDescription}"beforeinput" event at ${aTestData.action} must be trusted`); + is(aEvent.target, aElement, + `${aDescription}"beforeinput" event at ${aTestData.action} is fired on unexpected element: ${aEvent.target.tagName}`); + ok(aEvent instanceof InputEvent, + `${aDescription}"beforeinput" event at ${aTestData.action} should be dispatched with InputEvent interface`); + ok(aEvent.bubbles, + `${aDescription}"beforeinput" event at ${aTestData.action} must be bubbles`); + beforeInputEvent = aEvent; + }; + let inputHandler = (aEvent) => { + if (initializing) { + return; + } + ok(!inputEvent, + `${aDescription}Multiple "input" events are fired at ${aTestData.action} (inputType: "${aEvent.inputType}", data: ${aEvent.data})`); + ok(aEvent.isTrusted, + `${aDescription}"input" event at ${aTestData.action} must be trusted`); + is(aEvent.target, aElement, `"input" event at ${aTestData.action} is fired on unexpected element: ${aEvent.target.tagName}`); + ok(aEvent instanceof InputEvent, + `${aDescription}"input" event at ${aTestData.action} should be dispatched with InputEvent interface`); + ok(!aEvent.cancelable, + `${aDescription}"input" event at ${aTestData.action} must not be cancelable`); + ok(aEvent.bubbles, + `${aDescription}"input" event at ${aTestData.action} must be bubbles`); + let duration = Math.abs(window.performance.now() - aEvent.timeStamp); + ok(duration < 30 * 1000, + `${aDescription}perhaps, timestamp wasn't set correctly :${aEvent.timeStamp} (expected it to be within 30s of ` + + `the current time but it differed by ${duration}ms)`); + inputEvent = aEvent; + }; + + if (aTestData.cancelBeforeInput && + (aExpected.beforeInputEvent === null || aExpected.beforeInputEvent === undefined)) { + ok(false, + `${aDescription}cancelBeforeInput must not be true for ${aTestData.action} because "beforeinput" event is not expected`); + return; + } + + try { + aElement.addEventListener("beforeinput", beforeInputHandler, true); + aElement.addEventListener("input", inputHandler, true); + + let initialValue = aElement.value; + + aFunc(); + + (function verify() { + try { + if (aExpected.value !== undefined) { + if (aTestData.cancelBeforeInput && aExpected.valueForCanceled === undefined) { + is(aElement.value, initialValue, + `${aDescription}the value should be "${initialValue}" after ${aTestData.action}`); + } else { + let expectedValue = + aTestData.cancelBeforeInput ? aExpected.valueForCanceled : aExpected.value; + is(aElement.value, expectedValue, + `${aDescription}the value should be "${expectedValue}" after ${aTestData.action}`); + } + } + if (aExpected.beforeInputEvent === null || aExpected.beforeInputEvent === undefined) { + ok(!beforeInputEvent, + `${aDescription}"beforeinput" event shouldn't have been fired at ${aTestData.action}`); + } else { + ok(beforeInputEvent, + `${aDescription}"beforeinput" event should've been fired at ${aTestData.action}`); + is(beforeInputEvent.cancelable, aExpected.beforeInputEvent.cancelable, + `${aDescription}"beforeinput" event by ${aTestData.action} should be ${ + aExpected.beforeInputEvent.cancelable ? "cancelable" : "not cancelable" + }`); + is(beforeInputEvent.inputType, aExpected.beforeInputEvent.inputType, + `${aDescription}inputType of "beforeinput" event by ${aTestData.action} should be "${aExpected.beforeInputEvent.inputType}"`); + is(beforeInputEvent.data, aExpected.beforeInputEvent.data, + `${aDescription}data of "beforeinput" event by ${aTestData.action} should be ${ + aExpected.beforeInputEvent.data === null ? "null" : `"${aExpected.beforeInputEvent.data}"` + }`); + is(beforeInputEvent.dataTransfer, null, + `${aDescription}dataTransfer of "beforeinput" event by ${aTestData.action} should be null`); + is(beforeInputEvent.getTargetRanges().length, 0, + `${aDescription}getTargetRanges() of "beforeinput" event by ${aTestData.action} should return empty array`); + } + if (( + aTestData.cancelBeforeInput === true && + aExpected.beforeInputEvent && + aExpected.beforeInputEvent.cancelable + ) || aExpected.inputEvent === null || aExpected.inputEvent === undefined) { + ok(!inputEvent, + `${aDescription}"input" event shouldn't have been fired at ${aTestData.action}`); + } else { + ok(inputEvent, + `${aDescription}"input" event should've been fired at ${aTestData.action}`); + is(inputEvent.cancelable, false, + `${aDescription}"input" event by ${aTestData.action} should be not be cancelable`); + is(inputEvent.inputType, aExpected.inputEvent.inputType, + `${aDescription}inputType of "input" event by ${aTestData.action} should be "${aExpected.inputEvent.inputType}"`); + is(inputEvent.data, aExpected.inputEvent.data, + `${aDescription}data of "input" event by ${aTestData.action} should be ${ + aExpected.inputEvent.data === null ? "null" : `"${aExpected.inputEvent.data}"` + }`); + is(inputEvent.dataTransfer, null, + `${aDescription}dataTransfer of "input" event by ${aTestData.action} should be null`); + is(inputEvent.getTargetRanges().length, 0, + `${aDescription}getTargetRanges() of "input" event by ${aTestData.action} should return empty array`); + } + } catch (ex) { + ok(false, `${aDescription}unexpected exception at verifying test result of "${aTestData.action}": ${ex.toString()}`); + } + })(); + } finally { + aElement.removeEventListener("beforeinput", beforeInputHandler, true); + aElement.removeEventListener("input", inputHandler, true); + } + } + + function test_typing_a_in_empty_editor(aTestData) { + aElement.value = ""; + aElement.focus(); + + runTest( + aTestData, + () => { + sendString("a"); + }, + { + value: "a", + beforeInputEvent: { + cancelable: true, + inputType: "insertText", + data: "a", + }, + inputEvent: { + inputType: "insertText", + data: "a", + }, + } + ); + } + test_typing_a_in_empty_editor({ + action: 'typing "a" and canceling beforeinput', + cancelBeforeInput: true, + }); + test_typing_a_in_empty_editor({ + action: 'typing "a"', + cancelBeforeInput: false, + }); + + function test_typing_backspace_to_delete_last_character(aTestData) { + aElement.value = "a"; + aElement.focus(); + aElement.setSelectionStart = "a".length; + + runTest( + aTestData, + () => { + synthesizeKey("KEY_Backspace"); + }, + { + value: "", + beforeInputEvent: { + cancelable: true, + inputType: "deleteContentBackward", + data: null, + }, + inputEvent: { + inputType: "deleteContentBackward", + data: null, + }, + } + ); + } + test_typing_backspace_to_delete_last_character({ + actin: 'typing "Backspace" to delete "a" and canceling "beforeinput"', + cancelBeforeInput: true, + }); + test_typing_backspace_to_delete_last_character({ + actin: 'typing "Backspace" to delete "a"', + cancelBeforeInput: false, + }); + + function test_typing_enter_in_empty_editor(aTestData) { + aElement.value = ""; + aElement.focus(); + + runTest( + aTestData, + () => { + synthesizeKey("KEY_Enter"); + }, + aIsTextarea + ? { + value: "\n", + beforeInputEvent: { + cancelable: true, + inputType: "insertLineBreak", + data: null, + }, + inputEvent: { + inputType: "insertLineBreak", + data: null, + }, + } + : { + value: "", + beforeInputEvent: { + cancelable: true, + inputType: "insertLineBreak", + data: null, + }, + } + ); + } + test_typing_enter_in_empty_editor({ + action: 'typing "Enter" in empty editor and canceling "beforeinput"', + cancelBeforeInput: true, + }); + test_typing_enter_in_empty_editor({ + action: 'typing "Enter" in empty editor', + cancelBeforeInput: false, + }); + + (function test_setting_value(aTestData) { + aElement.value = ""; + aElement.focus(); + + runTest( + aTestData, + () => { + aElement.value = "foo-bar"; + }, + { value: "foo-bar" } + ); + })({ + action: "setting non-empty value", + }); + + (function test_setting_empty_value(aTestData) { + aElement.value = "foo-bar"; + aElement.focus(); + + runTest( + aTestData, + () => { + aElement.value = ""; + }, + { value: "" } + ); + })({ + action: "setting empty value", + }); + + (function test_typing_space_in_empty_editor(aTestData) { + aElement.value = ""; + aElement.focus(); + + runTest( + aTestData, + () => { + sendString(" "); + }, + { + value: " ", + beforeInputEvent: { + cancelable: true, + inputType: "insertText", + data: " ", + }, + inputEvent: { + inputType: "insertText", + data: " ", + }, + } + ); + })({ + action: "typing space", + }); + + (function test_typing_delete_at_end_of_editor(aTestData) { + aElement.value = " "; + aElement.focus(); + + runTest( + aTestData, + () => { + synthesizeKey("KEY_Delete"); + }, + { + value: " ", + beforeInputEvent: { + cancelable: true, + inputType: "deleteContentForward", + data: null, + }, + } + ); + })({ + action: 'typing "Delete" at end of editor', + }); + + (function test_typing_arrow_left_to_move_caret(aTestData) { + aElement.value = " "; + aElement.focus(); + aElement.selectionStart = 1; + + runTest( + aTestData, + () => { + synthesizeKey("KEY_ArrowLeft"); + }, + { value: " " } + ); + })({ + action: 'typing "ArrowLeft" to move caret', + }); + + function test_typing_delete_to_delete_last_character(aTestData) { + aElement.value = " "; + aElement.focus(); + aElement.selectionStart = 0; + + runTest( + aTestData, + () => { + synthesizeKey("KEY_Delete"); + }, + { + value: "", + beforeInputEvent: { + cancelable: true, + inputType: "deleteContentForward", + data: null, + }, + inputEvent: { + inputType: "deleteContentForward", + data: null, + }, + } + ); + } + test_typing_delete_to_delete_last_character({ + action: 'typing "Delete" to delete space and canceling "beforeinput"', + cancelBeforeInput: true, + }); + test_typing_delete_to_delete_last_character({ + action: 'typing "Delete" to delete space', + cancelBeforeInput: false, + }); + + function test_undoing_deleting_last_character(aTestData) { + aElement.value = "a"; + aElement.focus(); + aElement.selectionStart = 0; + synthesizeKey("KEY_Delete"); + + runTest( + aTestData, + () => { + synthesizeKey("z", {accelKey: true}); + }, + { + value: "a", + beforeInputEvent: { + cancelable: true, + inputType: "historyUndo", + data: null, + }, + inputEvent: { + inputType: "historyUndo", + data: null, + }, + } + ); + } + test_undoing_deleting_last_character({ + action: 'undoing deleting last character and canceling "beforeinput"', + cancelBeforeInput: true, + }); + test_undoing_deleting_last_character({ + action: "undoing deleting last character", + cancelBeforeInput: false, + }); + + (function test_undoing_without_undoable_transaction(aTestData) { + aElement.value = "a"; + aElement.focus(); + aElement.selectionStart = 0; + synthesizeKey("KEY_Delete"); + synthesizeKey("z", {accelKey: true}); + + runTest( + aTestData, + () => { + synthesizeKey("z", {accelKey: true}); + }, + { value: "a" } + ); + })({ + action: "trying to undo without undoable transaction" + }); + + function test_redoing_deleting_last_character(aTestData) { + aElement.value = "a"; + aElement.focus(); + aElement.selectionStart = 0; + synthesizeKey("KEY_Delete"); + synthesizeKey("z", {accelKey: true}); + + runTest( + aTestData, + () => { + synthesizeKey("Z", {accelKey: true, shiftKey: true}); + }, + { + value: "", + beforeInputEvent: { + cancelable: true, + inputType: "historyRedo", + data: null, + }, + inputEvent: { + inputType: "historyRedo", + data: null, + }, + } + ); + } + test_redoing_deleting_last_character({ + action: 'redoing deleting last character and canceling "beforeinput"', + cancelBeforeInput: true, + }); + test_redoing_deleting_last_character({ + action: "redoing deleting last character", + cancelBeforeInput: false, + }); + + (function test_redoing_without_redoable_transaction(aTestData) { + aElement.value = "a"; + aElement.focus(); + aElement.selectionStart = 0; + synthesizeKey("KEY_Delete"); + synthesizeKey("z", {accelKey: true}); + synthesizeKey("Z", {accelKey: true, shiftKey: true}); + + runTest( + aTestData, + () => { + synthesizeKey("Z", {accelKey: true, shiftKey: true}); + }, + { value: "" } + ); + })({ + action: "trying to redo without redoable transaction" + }); + + function test_typing_backspace_with_selecting_all_characters(aTestData) { + aElement.value = "abc"; + aElement.focus(); + aElement.select(); + + runTest( + aTestData, + () => { + synthesizeKey("KEY_Backspace"); + }, + { + value: "", + beforeInputEvent: { + cancelable: true, + inputType: "deleteContentBackward", + data: null, + }, + inputEvent: { + inputType: "deleteContentBackward", + data: null, + }, + } + ); + } + test_typing_backspace_with_selecting_all_characters({ + action: 'typing "Backspace" to delete all selected characters and canceling "beforeinput"', + cancelBeforeInput: true, + }); + test_typing_backspace_with_selecting_all_characters({ + action: 'typing "Backspace" to delete all selected characters', + cancelBeforeInput: false, + }); + + function test_typing_delete_with_selecting_all_characters(aTestData) { + aElement.value = "abc"; + aElement.focus(); + aElement.select(); + + runTest( + aTestData, + () => { + synthesizeKey("KEY_Delete"); + }, + { + value: "", + beforeInputEvent: { + cancelable: true, + inputType: "deleteContentForward", + data: null, + }, + inputEvent: { + inputType: "deleteContentForward", + data: null, + }, + } + ); + } + test_typing_delete_with_selecting_all_characters({ + action: 'typing "Delete" to delete all selected characters and canceling "beforeinput"', + cancelBeforeInput: true, + }); + test_typing_delete_with_selecting_all_characters({ + action: 'typing "Delete" to delete all selected characters and canceling "beforeinput"', + cancelBeforeInput: false, + }); + + function test_deleting_word_backward_from_its_end(aTestData) { + aElement.value = "abc def"; + aElement.focus(); + document.documentElement.scrollTop; // XXX Needs reflow here for working with nsFrameSelection, must be a bug. + aElement.setSelectionRange("abc def".length, "abc def".length); + + runTest( + aTestData, + () => { + SpecialPowers.doCommand(window, "cmd_deleteWordBackward"); + }, + { + value: "abc ", + beforeInputEvent: { + cancelable: true, + inputType: "deleteWordBackward", + data: null, + }, + inputEvent: { + inputType: "deleteWordBackward", + data: null, + }, + } + ); + } + test_deleting_word_backward_from_its_end({ + action: 'deleting word backward from its end and canceling "beforeinput"', + cancelBeforeInput: true, + }); + test_deleting_word_backward_from_its_end({ + action: 'deleting word backward from its end', + cancelBeforeInput: false, + }); + + function test_deleting_word_forward_from_its_start(aTestData) { + aElement.value = "abc def"; + aElement.focus(); + document.documentElement.scrollTop; // XXX Needs reflow here for working with nsFrameSelection, must be a bug. + aElement.setSelectionRange(0, 0); + + runTest( + aTestData, + () => { + SpecialPowers.doCommand(window, "cmd_deleteWordForward"); + }, + { + value: kWordSelectEatSpaceToNextWord ? "def" : " def", + beforeInputEvent: { + cancelable: true, + inputType: "deleteWordForward", + data: null, + }, + inputEvent: { + inputType: "deleteWordForward", + data: null, + }, + } + ); + } + test_deleting_word_forward_from_its_start({ + action: 'deleting word forward from its start and canceling "beforeinput"', + cancelBeforeInput: true, + }); + test_deleting_word_forward_from_its_start({ + action: "deleting word forward from its start", + cancelBeforeInput: false, + }); + + (function test_deleting_word_backward_from_middle_of_second_word(aTestData) { + aElement.value = "abc def"; + aElement.focus(); + document.documentElement.scrollTop; // XXX Needs reflow here for working with nsFrameSelection, must be a bug. + aElement.setSelectionRange("abc d".length, "abc de".length); + + runTest( + aTestData, + () => { + SpecialPowers.doCommand(window, "cmd_deleteWordBackward"); + }, + { + value: "abc df", + beforeInputEvent: { + cancelable: true, + inputType: "deleteContentBackward", + data: null, + }, + inputEvent: { + inputType: "deleteContentBackward", + data: null, + }, + } + ); + })({ + action: "removing characters backward from middle of second word", + }); + + (function test_deleting_word_forward_from_middle_of_first_word(aTestData) { + aElement.value = "abc def"; + aElement.focus(); + document.documentElement.scrollTop; // XXX Needs reflow here for working with nsFrameSelection, must be a bug. + aElement.setSelectionRange("a".length, "ab".length); + + runTest( + aTestData, + () => { + SpecialPowers.doCommand(window, "cmd_deleteWordForward"); + }, + { + value: "ac def", + beforeInputEvent: { + cancelable: true, + inputType: "deleteContentForward", + data: null, + }, + inputEvent: { + inputType: "deleteContentForward", + data: null, + }, + } + ); + })({ + action: "removing characters forward from middle of first word", + }); + + (function test_deleting_characters_backward_to_start_of_line(aTestData) { + aElement.value = "abc def"; + aElement.focus(); + document.documentElement.scrollTop; // XXX Needs reflow here for working with nsFrameSelection, must be a bug. + aElement.setSelectionRange("abc d".length, "abc d".length); + + runTest( + aTestData, + () => { + SpecialPowers.doCommand(window, "cmd_deleteToBeginningOfLine"); + }, + { + value: "ef", + beforeInputEvent: { + cancelable: true, + inputType: "deleteSoftLineBackward", + data: null, + }, + inputEvent: { + inputType: "deleteSoftLineBackward", + data: null, + }, + } + ); + })({ + action: "removing characters backward to start of line" + }); + + (function test_deleting_characters_forward_to_end_of_line(aTestData) { + aElement.value = "abc def"; + aElement.focus(); + document.documentElement.scrollTop; // XXX Needs reflow here for working with nsFrameSelection, must be a bug. + aElement.setSelectionRange("ab".length, "ab".length); + + runTest( + aTestData, + () => { + SpecialPowers.doCommand(window, "cmd_deleteToEndOfLine"); + }, + { + value: "ab", + beforeInputEvent: { + cancelable: true, + inputType: "deleteSoftLineForward", + data: null, + }, + inputEvent: { + inputType: "deleteSoftLineForward", + data: null, + }, + } + ); + })({ + action: "removing characters forward to end of line", + }); + + (function test_deleting_characters_backward_to_start_of_line_with_non_collapsed_selection(aTestData) { + aElement.value = "abc def"; + aElement.focus(); + document.documentElement.scrollTop; // XXX Needs reflow here for working with nsFrameSelection, must be a bug. + aElement.setSelectionRange("abc d".length, "abc_de".length); + + runTest( + aTestData, + () => { + SpecialPowers.doCommand(window, "cmd_deleteToBeginningOfLine"); + }, + { + value: "abc df", + beforeInputEvent: { + cancelable: true, + inputType: "deleteContentBackward", + data: null, + }, + inputEvent: { + inputType: "deleteContentBackward", + data: null, + }, + } + ); + })({ + action: "removing characters backward to start of line (with selection in second word)", + }); + + (function test_deleting_characters_forward_to_end_of_line_with_non_collapsed_selection(aTestData) { + aElement.value = "abc def"; + aElement.focus(); + document.documentElement.scrollTop; // XXX Needs reflow here for working with nsFrameSelection, must be a bug. + aElement.setSelectionRange("a".length, "ab".length); + + runTest( + aTestData, + () => { + SpecialPowers.doCommand(window, "cmd_deleteToEndOfLine"); + }, + { + value: "ac def", + beforeInputEvent: { + cancelable: true, + inputType: "deleteContentForward", + data: null, + }, + inputEvent: { + inputType: "deleteContentForward", + data: null, + }, + } + ); + })({ + action: "removing characters forward to end of line (with selection in second word)", + }); + + function test_switching_text_direction_from_default(aTestData) { + try { + aElement.removeAttribute("dir"); + aElement.scrollTop; // XXX Update the root frame + aElement.focus(); + + runTest( + aTestData, + () => { + SpecialPowers.doCommand(window, "cmd_switchTextDirection"); + if (aTestData.cancelBeforeInput) { + is(aElement.getAttribute("dir"), null, + `${aDescription}dir attribute of the element shouldn't have been set by ${aTestData.action}`); + } else { + is(aElement.getAttribute("dir"), "rtl", + `${aDescription}dir attribute of the element should've been set to "rtl" by ${aTestData.action}`); + } + }, + { + beforeInputEvent: { + cancelable: true, + inputType: "formatSetBlockTextDirection", + data: "rtl", + }, + inputEvent: { + inputType: "formatSetBlockTextDirection", + data: "rtl", + }, + } + ); + } finally { + aElement.removeAttribute("dir"); + aElement.scrollTop; // XXX Update the root frame + } + } + test_switching_text_direction_from_default({ + action: 'switching text direction from default to "rtl" and canceling "beforeinput"', + cancelBeforeInput: true, + }); + test_switching_text_direction_from_default({ + action: 'switching text direction from default to "rtl"', + cancelBeforeInput: false, + }); + + function test_switching_text_direction_from_rtl_to_ltr(aTestData) { + try { + aElement.setAttribute("dir", "rtl"); + aElement.scrollTop; // XXX Update the root frame + aElement.focus(); + + runTest( + aTestData, + () => { + SpecialPowers.doCommand(window, "cmd_switchTextDirection"); + let expectedDirValue = aTestData.cancelBeforeInput ? "rtl" : "ltr"; + is(aElement.getAttribute("dir"), expectedDirValue, + `${aDescription}dir attribute of the element should be "${expectedDirValue}" after ${aTestData.action}`); + }, + { + beforeInputEvent: { + cancelable: true, + inputType: "formatSetBlockTextDirection", + data: "ltr", + }, + inputEvent: { + inputType: "formatSetBlockTextDirection", + data: "ltr", + }, + } + ); + } finally { + aElement.removeAttribute("dir"); + aElement.scrollTop; // XXX Update the root frame + } + } + test_switching_text_direction_from_rtl_to_ltr({ + action: 'switching text direction from "rtl" to "ltr" and canceling "beforeinput"', + cancelBeforeInput: true, + }); + test_switching_text_direction_from_rtl_to_ltr({ + action: 'switching text direction from "rtl" to "ltr" and canceling "beforeinput"', + cancelBeforeInput: false, + }); + } + + doTests(document.getElementById("input"), "<input type=\"text\">", false); + doTests(document.getElementById("textarea"), "<textarea>", true); + + SimpleTest.finish(); +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_dragdrop.html b/editor/libeditor/tests/test_dragdrop.html new file mode 100644 index 0000000000..6295661faa --- /dev/null +++ b/editor/libeditor/tests/test_dragdrop.html @@ -0,0 +1,3451 @@ +<!doctype html> +<html> + +<head> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> + + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> + +<body> + <div id="dropZone" + ondragenter="event.dataTransfer.dropEffect = 'copy'; event.preventDefault();" + ondragover="event.dataTransfer.dropEffect = 'copy'; event.preventDefault();" + ondrop="event.preventDefault();" + style="height: 4px; background-color: lemonchiffon;"></div> + <div id="container"></div> + +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); + +function checkInputEvent(aEvent, aExpectedTarget, aInputType, aData, aDataTransfer, aTargetRanges, aDescription) { + ok(aEvent instanceof InputEvent, `${aDescription}: "${aEvent.type}" event should be dispatched with InputEvent interface`); + is(aEvent.cancelable, aEvent.type === "beforeinput", `${aDescription}: "${aEvent.type}" event should be ${aEvent.type === "beforeinput" ? "" : "never "}cancelable`); + is(aEvent.bubbles, true, `${aDescription}: "${aEvent.type}" event should always bubble`); + is(aEvent.target, aExpectedTarget, `${aDescription}: "${aEvent.type}" event should be fired on the <${aExpectedTarget.tagName.toLowerCase()}> element`); + is(aEvent.inputType, aInputType, `${aDescription}: inputType of "${aEvent.type}" event should be "${aInputType}" on the <${aExpectedTarget.tagName.toLowerCase()}> element`); + is(aEvent.data, aData, `${aDescription}: data of "${aEvent.type}" event should be ${aData} on the <${aExpectedTarget.tagName.toLowerCase()}> element`); + if (aDataTransfer === null) { + is(aEvent.dataTransfer, null, `${aDescription}: dataTransfer should be null on the <${aExpectedTarget.tagName.toLowerCase()}> element`); + } else { + for (let dataTransfer of aDataTransfer) { + let description = `${aDescription}: on the <${aExpectedTarget.tagName.toLowerCase()}> element`; + if (dataTransfer.todo) { + // XXX It seems that synthesizeDrop() don't emulate perfectly if caller specifies the data directly. + todo_is(aEvent.dataTransfer.getData(dataTransfer.type), dataTransfer.data, + `${description}: dataTransfer of "${aEvent.type}" event should have "${dataTransfer.data}" whose type is "${dataTransfer.type}"`); + } else { + is(aEvent.dataTransfer.getData(dataTransfer.type), dataTransfer.data, + `${description}: dataTransfer of "${aEvent.type}" event should have "${dataTransfer.data}" whose type is "${dataTransfer.type}"`); + } + } + } + let targetRanges = aEvent.getTargetRanges(); + if (aTargetRanges.length === 0) { + is(targetRanges.length, 0, + `${aDescription}: getTargetRange() of "${aEvent.type}" event should return empty array`); + } else { + is(targetRanges.length, aTargetRanges.length, + `${aDescription}: getTargetRange() of "${aEvent.type}" event should return static range array`); + if (targetRanges.length == aTargetRanges.length) { + for (let i = 0; i < targetRanges.length; i++) { + is(targetRanges[i].startContainer, aTargetRanges[i].startContainer, + `${aDescription}: startContainer of getTargetRanges()[${i}] of "${aEvent.type}" event does not match`); + is(targetRanges[i].startOffset, aTargetRanges[i].startOffset, + `${aDescription}: startOffset of getTargetRanges()[${i}] of "${aEvent.type}" event does not match`); + is(targetRanges[i].endContainer, aTargetRanges[i].endContainer, + `${aDescription}: endContainer of getTargetRanges()[${i}] of "${aEvent.type}" event does not match`); + is(targetRanges[i].endOffset, aTargetRanges[i].endOffset, + `${aDescription}: endOffset of getTargetRanges()[${i}] of "${aEvent.type}" event does not match`); + } + } + } +} + +// eslint-disable-next-line complexity +async function doTest() { + const container = document.getElementById("container"); + const dropZone = document.getElementById("dropZone"); + + let beforeinputEvents = []; + let inputEvents = []; + let dragEvents = []; + function onBeforeinput(event) { + beforeinputEvents.push(event); + } + function onInput(event) { + inputEvents.push(event); + } + document.addEventListener("beforeinput", onBeforeinput); + document.addEventListener("input", onInput); + + function preventDefaultDeleteByDrag(aEvent) { + if (aEvent.inputType === "deleteByDrag") { + aEvent.preventDefault(); + } + } + function preventDefaultInsertFromDrop(aEvent) { + if (aEvent.inputType === "insertFromDrop") { + aEvent.preventDefault(); + } + } + + const selection = window.getSelection(); + + const kIsMac = navigator.platform.includes("Mac"); + const kIsWin = navigator.platform.includes("Win"); + + const kNativeLF = kIsWin ? "\r\n" : "\n"; + + const kModifiersToCopy = { + ctrlKey: !kIsMac, + altKey: kIsMac, + } + + function comparePlainText(aGot, aExpected, aDescription) { + is(aGot.replace(/\r\n?/g, "\n"), aExpected, aDescription); + } + function compareHTML(aGot, aExpected, aDescription) { + is(aGot.replace(/\r\n?/g, "\n"), aExpected, aDescription); + } + + async function trySynthesizePlainDragAndDrop(aDescription, aOptions) { + try { + await synthesizePlainDragAndDrop(aOptions); + return true; + } catch (e) { + ok(false, `${aDescription}: Failed to emulate drag and drop (${e.message})`); + return false; + } + } + + // -------- Test dragging regular text + await (async function test_dragging_regular_text() { + const description = "dragging part of non-editable <span> element"; + container.innerHTML = '<span style="font-size: 24px;">Some Text</span>'; + const span = document.querySelector("div#container > span"); + selection.setBaseAndExtent(span.firstChild, 4, span.firstChild, 6); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), + span.textContent.substring(4, 6), + `${description}: dataTransfer should have selected text as "text/plain"`); + compareHTML(aEvent.dataTransfer.getData("text/html"), + span.outerHTML.replace(/>.+</, `>${span.textContent.substring(4, 6)}<`), + `${description}: dataTransfer should have the parent inline element and only selected text as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: dropZone, + } + ) + ) { + is(beforeinputEvents.length, 0, + `${description}: No "beforeinput" event should be fired when dragging non-editable selection to non-editable drop zone`); + is(inputEvents.length, 0, + `${description}: No "input" event should be fired when dragging non-editable selection to non-editable drop zone`); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging text from an <input> + await (async function test_dragging_text_from_input_element() { + const description = "dragging part of text in <input> element"; + container.innerHTML = '<input value="Drag Me">'; + const input = document.querySelector("div#container > input"); + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + input.setSelectionRange(1, 4); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), + input.value.substring(1, 4), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should not have data as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(input).editor.selection, + destElement: dropZone, + } + ) + ) { + is(beforeinputEvents.length, 0, + `${description}: No "beforeinput" event should be fired when dragging <input> value to non-editable drop zone`); + is(inputEvents.length, 0, + `${description}: No "input" event should be fired when dragging <input> value to non-editable drop zone`); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging text from an <textarea> + await (async function test_dragging_text_from_textarea_element() { + const description = "dragging part of text in <textarea> element"; + container.innerHTML = "<textarea>Some Text To Drag</textarea>"; + const textarea = document.querySelector("div#container > textarea"); + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + textarea.setSelectionRange(1, 7); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), + textarea.value.substring(1, 7), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should not have data as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(textarea).editor.selection, + destElement: dropZone, + } + ) + ) { + is(beforeinputEvents.length, 0, + `${description}: No "beforeinput" event should be fired when dragging <textarea> value to non-editable drop zone`); + is(inputEvents.length, 0, + `${description}: No "input" event should be fired when dragging <textarea> value to non-editable drop zone`); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging text from a contenteditable + await (async function test_dragging_text_from_contenteditable() { + const description = "dragging part of text in contenteditable element"; + container.innerHTML = "<p contenteditable>This is some <b>editable</b> text.</p>"; + const b = document.querySelector("div#container > p > b"); + selection.setBaseAndExtent(b.firstChild, 2, b.firstChild, 6); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), + b.textContent.substring(2, 6), + `${description}: dataTransfer should have selected text as "text/plain"`); + compareHTML(aEvent.dataTransfer.getData("text/html"), + b.outerHTML.replace(/>.+</, `>${b.textContent.substring(2, 6)}<`), + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: dropZone, + } + ) + ) { + is(beforeinputEvents.length, 0, + `${description}: No "beforeinput" event should be fired when dragging <textarea> value to non-editable drop zone`); + is(inputEvents.length, 0, + `${description}: No "input" event should be fired when dragging <textarea> value to non-editable drop zone`); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired`); + } + document.removeEventListener("drop", onDrop); + })(); + + + for (const inputType of ["text", "search"]) { + // -------- Test dragging regular text of text/html to <input> + await (async function test_dragging_text_from_span_element_to_input_element() { + const description = `dragging text in non-editable <span> to <input type=${inputType}>`; + container.innerHTML = `<span>Static</span><input type="${inputType}">`; + const span = document.querySelector("div#container > span"); + const input = document.querySelector("div#container > input"); + selection.setBaseAndExtent(span.firstChild, 2, span.firstChild, 5); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), + span.textContent.substring(2, 5), + `${description}: dataTransfer should have selected text as "text/plain"`); + compareHTML(aEvent.dataTransfer.getData("text/html"), + span.outerHTML.replace(/>.+</, `>${span.textContent.substring(2, 5)}<`), + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: input, + } + ) + ) { + is(input.value, span.textContent.substring(2, 5), + `${description}: <input>.value should be modified`); + is(beforeinputEvents.length, 1, + `${description}: one "beforeinput" event should be fired on <input>`); + checkInputEvent(beforeinputEvents[0], input, "insertFromDrop", span.textContent.substring(2, 5), null, [], description); + is(inputEvents.length, 1, + `${description}: one "input" event should be fired on <input>`); + checkInputEvent(inputEvents[0], input, "insertFromDrop", span.textContent.substring(2, 5), null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <input>`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging regular text of text/html to disabled <input> + await (async function test_dragging_text_from_span_element_to_disabled_input_element() { + const description = `dragging text in non-editable <span> to <input disabled type="${inputType}">`; + container.innerHTML = `<span>Static</span><input disabled type="${inputType}">`; + const span = document.querySelector("div#container > span"); + const input = document.querySelector("div#container > input"); + selection.setBaseAndExtent(span.firstChild, 2, span.firstChild, 5); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: input, + } + ) + ) { + is(input.value, "", + `${description}: <input disable>.value should not be modified`); + is(beforeinputEvents.length, 0, + `${description}: no "beforeinput" event should be fired on <input disabled>`); + is(inputEvents.length, 0, + `${description}: no "input" event should be fired on <input disabled>`); + is(dragEvents.length, 0, + `${description}: no "drop" event should be fired on <input disabled>`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging regular text of text/html to readonly <input> + await (async function test_dragging_text_from_span_element_to_readonly_input_element() { + const description = `dragging text in non-editable <span> to <input readonly type="${inputType}">`; + container.innerHTML = `<span>Static</span><input readonly type="${inputType}">`; + const span = document.querySelector("div#container > span"); + const input = document.querySelector("div#container > input"); + selection.setBaseAndExtent(span.firstChild, 2, span.firstChild, 5); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), + span.textContent.substring(2, 5), + `${description}: dataTransfer should have selected text as "text/plain"`); + compareHTML(aEvent.dataTransfer.getData("text/html"), + span.outerHTML.replace(/>.+</, `>${span.textContent.substring(2, 5)}<`), + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: input, + } + ) + ) { + is(input.value, "", + `${description}: <input readonly>.value should not be modified`); + is(beforeinputEvents.length, 0, + `${description}: no "beforeinput" event should be fired on <input readonly>`); + is(inputEvents.length, 0, + `${description}: no "input" event should be fired on <input readonly>`); + is(dragEvents.length, 0, + `${description}: no "drop" event should be fired on <input readonly>`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging only text/html data (like from another app) to <input>. + await (async function test_dragging_only_html_text_to_input_element() { + const description = `dragging only text/html data to <input type="${inputType}>`; + container.innerHTML = `<span>Static</span><input type="${inputType}">`; + const span = document.querySelector("div#container > span"); + const input = document.querySelector("div#container > input"); + selection.selectAllChildren(span); + beforeinputEvents = []; + inputEvents = []; + const onDragStart = aEvent => { + // Clear all dataTransfer data first. Then, it'll be filled only with + // the text/html data passed to synthesizeDrop(). + aEvent.dataTransfer.clearData(); + }; + window.addEventListener("dragstart", onDragStart, {capture: true}); + synthesizeDrop(span, input, [[{type: "text/html", data: "Some <b>Bold<b> Text"}]], "copy"); + is(beforeinputEvents.length, 0, + `${description}: no "beforeinput" event should be fired on <input>`); + is(inputEvents.length, 0, + `${description}: no "input" event should be fired on <input>`); + window.removeEventListener("dragstart", onDragStart, {capture: true}); + })(); + + // -------- Test dragging both text/plain and text/html data (like from another app) to <input>. + await (async function test_dragging_both_html_text_and_plain_text_to_input_element() { + const description = `dragging both text/plain and text/html data to <input type=${inputType}>`; + container.innerHTML = `<span>Static</span><input type="${inputType}">`; + const span = document.querySelector("div#container > span"); + const input = document.querySelector("div#container > input"); + selection.selectAllChildren(span); + beforeinputEvents = []; + inputEvents = []; + const onDragStart = aEvent => { + // Clear all dataTransfer data first. Then, it'll be filled only with + // the text/plain data and text/html data passed to synthesizeDrop(). + aEvent.dataTransfer.clearData(); + }; + window.addEventListener("dragstart", onDragStart, {capture: true}); + synthesizeDrop(span, input, [[{type: "text/html", data: "Some <b>Bold<b> Text"}, + {type: "text/plain", data: "Some Plain Text"}]], "copy"); + is(input.value, "Some Plain Text", + `${description}: The text/plain data should be inserted`); + is(beforeinputEvents.length, 1, + `${description}: only one "beforeinput" events should be fired on <input> element`); + checkInputEvent(beforeinputEvents[0], input, "insertFromDrop", "Some Plain Text", null, [], + description); + is(inputEvents.length, 1, + `${description}: only one "input" events should be fired on <input> element`); + checkInputEvent(inputEvents[0], input, "insertFromDrop", "Some Plain Text", null, [], + description); + window.removeEventListener("dragstart", onDragStart, {capture: true}); + })(); + + // -------- Test dragging special text type from another app to <input> + await (async function test_dragging_only_moz_text_internal_to_input_element() { + const description = `dragging both text/x-moz-text-internal data to <input type="${inputType}">`; + container.innerHTML = `<span>Static</span><input type="${inputType}">`; + const span = document.querySelector("div#container > span"); + const input = document.querySelector("div#container > input"); + selection.selectAllChildren(span); + beforeinputEvents = []; + inputEvents = []; + const onDragStart = aEvent => { + // Clear all dataTransfer data first. Then, it'll be filled only with + // the text/x-moz-text-internal data passed to synthesizeDrop(). + aEvent.dataTransfer.clearData(); + }; + window.addEventListener("dragstart", onDragStart, {capture: true}); + synthesizeDrop(span, input, [[{type: "text/x-moz-text-internal", data: "Some Special Text"}]], "copy"); + is(input.value, "", + `${description}: <input>.value should not be modified with "text/x-moz-text-internal" data`); + // Note that even if editor does not handle given dataTransfer, web apps + // may handle it by itself. Therefore, editor should dispatch "beforeinput" + // event. + is(beforeinputEvents.length, 1, + `${description}: one "beforeinput" event should be fired when dropping "text/x-moz-text-internal" data into <input> element`); + // But unfortunately, on <input> and <textarea>, dataTransfer won't be set... + checkInputEvent(beforeinputEvents[0], input, "insertFromDrop", "", null, [], description); + is(inputEvents.length, 0, + `${description}: no "input" event should be fired when dropping "text/x-moz-text-internal" data into <input> element`); + window.removeEventListener("dragstart", onDragStart, {capture: true}); + })(); + + // -------- Test dragging contenteditable to <input> + await (async function test_dragging_from_contenteditable_to_input_element() { + const description = `dragging text in contenteditable to <input type="${inputType}">`; + container.innerHTML = `<div contenteditable>Some <b>bold</b> text</div><input type="${inputType}">`; + const contenteditable = document.querySelector("div#container > div"); + const input = document.querySelector("div#container > input"); + const selectionContainers = [contenteditable.firstChild, contenteditable.firstChild.nextSibling.nextSibling]; + selection.setBaseAndExtent(selectionContainers[0], 2, selectionContainers[1], 2); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "me bold t", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "me <b>bold</b> t", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: input, + } + ) + ) { + is(contenteditable.innerHTML, "Soext", + `${description}: Dragged range should be removed from contenteditable`); + is(input.value, "me bold t", + `${description}: <input>.value should be modified`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on contenteditable and <input>`); + checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, + [{startContainer: selectionContainers[0], startOffset: 2, + endContainer: selectionContainers[1], endOffset: 2}], + description); + checkInputEvent(beforeinputEvents[1], input, "insertFromDrop", "me bold t", null, [], description); + is(inputEvents.length, 2, + `${description}: 2 "input" events should be fired on contenteditable and <input>`); + checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, [], description); + checkInputEvent(inputEvents[1], input, "insertFromDrop", "me bold t", null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <textarea>`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging contenteditable to <input> (canceling "deleteByDrag") + await (async function test_dragging_from_contenteditable_to_input_element_and_canceling_delete_by_drag() { + const description = `dragging text in contenteditable to <input type="${inputType}"> (canceling "deleteByDrag")`; + container.innerHTML = `<div contenteditable>Some <b>bold</b> text</div><input type="${inputType}">`; + const contenteditable = document.querySelector("div#container > div"); + const input = document.querySelector("div#container > input"); + const selectionContainers = [contenteditable.firstChild, contenteditable.firstChild.nextSibling.nextSibling]; + selection.setBaseAndExtent(selectionContainers[0], 2, selectionContainers[1], 2); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "me bold t", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "me <b>bold</b> t", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultDeleteByDrag); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: input, + } + ) + ) { + is(contenteditable.innerHTML, "Some <b>bold</b> text", + `${description}: Dragged range shouldn't be removed from contenteditable`); + is(input.value, "me bold t", + `${description}: <input>.value should be modified`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on contenteditable and <input>`); + checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, + [{startContainer: selectionContainers[0], startOffset: 2, + endContainer: selectionContainers[1], endOffset: 2}], + description); + checkInputEvent(beforeinputEvents[1], input, "insertFromDrop", "me bold t", null, [], description); + is(inputEvents.length, 1, + `${description}: only one "input" event should be fired on <input>`); + checkInputEvent(inputEvents[0], input, "insertFromDrop", "me bold t", null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <input>`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultDeleteByDrag); + })(); + + // -------- Test dragging contenteditable to <input> (canceling "insertFromDrop") + await (async function test_dragging_from_contenteditable_to_input_element_and_canceling_insert_from_drop() { + const description = `dragging text in contenteditable to <input type="${inputType}"> (canceling "insertFromDrop")`; + container.innerHTML = "<div contenteditable>Some <b>bold</b> text</div><input>"; + const contenteditable = document.querySelector("div#container > div"); + const input = document.querySelector("div#container > input"); + const selectionContainers = [contenteditable.firstChild, contenteditable.firstChild.nextSibling.nextSibling]; + selection.setBaseAndExtent(selectionContainers[0], 2, selectionContainers[1], 2); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "me bold t", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "me <b>bold</b> t", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultInsertFromDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: input, + } + ) + ) { + is(contenteditable.innerHTML, "Soext", + `${description}: Dragged range should be removed from contenteditable`); + is(input.value, "", + `${description}: <input>.value shouldn't be modified`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on contenteditable and <input>`); + checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, + [{startContainer: selectionContainers[0], startOffset: 2, + endContainer: selectionContainers[1], endOffset: 2}], + description); + checkInputEvent(beforeinputEvents[1], input, "insertFromDrop", "me bold t", null, [], description); + is(inputEvents.length, 1, + `${description}: only one "input" event should be fired on contenteditable`); + checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <input>`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultInsertFromDrop); + })(); + } + + // -------- Test dragging regular text of text/html to <input type="number"> + // + // FIXME(emilio): The -moz-appearance bit is just a hack to + // work around bug 1611720. + await (async function test_dragging_from_span_element_to_input_element_whose_type_number() { + const description = `dragging text in non-editable <span> to <input type="number">`; + container.innerHTML = `<span>123456</span><input type="number" style="-moz-appearance: textfield">`; + const span = document.querySelector("div#container > span"); + const input = document.querySelector("div#container > input"); + selection.setBaseAndExtent(span.firstChild, 2, span.firstChild, 5); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), + span.textContent.substring(2, 5), + `${description}: dataTransfer should have selected text as "text/plain"`); + compareHTML(aEvent.dataTransfer.getData("text/html"), + span.outerHTML.replace(/>.+</, `>${span.textContent.substring(2, 5)}<`), + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: input, + } + ) + ) { + is(input.value, span.textContent.substring(2, 5), + `${description}: <input>.value should be modified`); + is(beforeinputEvents.length, 1, + `${description}: one "beforeinput" event should be fired on <input>`); + checkInputEvent(beforeinputEvents[0], input, "insertFromDrop", span.textContent.substring(2, 5), null, [], description); + is(inputEvents.length, 1, + `${description}: one "input" event should be fired on <input>`); + checkInputEvent(inputEvents[0], input, "insertFromDrop", span.textContent.substring(2, 5), null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <input>`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging only text/plain data (like from another app) to contenteditable. + await (async function test_dragging_only_plain_text_to_contenteditable() { + const description = "dragging both text/plain and text/html data to contenteditable"; + container.innerHTML = '<span>Static</span><div contenteditable style="min-height: 3em;"></div>'; + const span = document.querySelector("div#container > span"); + const contenteditable = document.querySelector("div#container > div"); + selection.selectAllChildren(span); + beforeinputEvents = []; + inputEvents = []; + const onDragStart = aEvent => { + // Clear all dataTransfer data first. Then, it'll be filled only with + // the text/plain data and text/html data passed to synthesizeDrop(). + aEvent.dataTransfer.clearData(); + }; + window.addEventListener("dragstart", onDragStart, {capture: true}); + synthesizeDrop(span, contenteditable, [[{type: "text/plain", data: "Sample Text"}]], "copy"); + is(contenteditable.innerHTML, "Sample Text", + `${description}: The text/plain data should be inserted`); + is(beforeinputEvents.length, 1, + `${description}: only one "beforeinput" events should be fired on contenteditable element`); + checkInputEvent(beforeinputEvents[0], contenteditable, "insertFromDrop", null, + [{todo: true, type: "text/plain", data: "Sample Text"}], + [{startContainer: contenteditable, startOffset: 0, + endContainer: contenteditable, endOffset: 0}], + description); + is(inputEvents.length, 1, + `${description}: only one "input" events should be fired on contenteditable element`); + checkInputEvent(inputEvents[0], contenteditable, "insertFromDrop", null, + [{todo: true, type: "text/plain", data: "Sample Text"}], + [], + description); + window.removeEventListener("dragstart", onDragStart, {capture: true}); + })(); + + // -------- Test dragging only text/html data (like from another app) to contenteditable. + await (async function test_dragging_only_html_text_to_contenteditable() { + const description = "dragging only text/html data to contenteditable"; + container.innerHTML = '<span>Static</span><div contenteditable style="min-height: 3em;"></div>'; + const span = document.querySelector("div#container > span"); + const contenteditable = document.querySelector("div#container > div"); + selection.selectAllChildren(span); + beforeinputEvents = []; + inputEvents = []; + const onDragStart = aEvent => { + // Clear all dataTransfer data first. Then, it'll be filled only with + // the text/plain data and text/html data passed to synthesizeDrop(). + aEvent.dataTransfer.clearData(); + }; + window.addEventListener("dragstart", onDragStart, {capture: true}); + synthesizeDrop(span, contenteditable, [[{type: "text/html", data: "Sample <i>Italic</i> Text"}]], "copy"); + is(contenteditable.innerHTML, "Sample <i>Italic</i> Text", + `${description}: The text/plain data should be inserted`); + is(beforeinputEvents.length, 1, + `${description}: only one "beforeinput" events should be fired on contenteditable element`); + checkInputEvent(beforeinputEvents[0], contenteditable, "insertFromDrop", null, + [{todo: true, type: "text/html", data: "Sample <i>Italic</i> Text"}], + [{startContainer: contenteditable, startOffset: 0, + endContainer: contenteditable, endOffset: 0}], + description); + is(inputEvents.length, 1, + `${description}: only one "input" events should be fired on contenteditable element`); + checkInputEvent(inputEvents[0], contenteditable, "insertFromDrop", null, + [{todo: true, type: "text/html", data: "Sample <i>Italic</i> Text"}], + [], + description); + window.removeEventListener("dragstart", onDragStart, {capture: true}); + })(); + + // -------- Test dragging regular text of text/plain to <textarea> + await (async function test_dragging_from_span_element_to_textarea_element() { + const description = "dragging text in non-editable <span> to <textarea>"; + container.innerHTML = "<span>Static</span><textarea></textarea>"; + const span = document.querySelector("div#container > span"); + const textarea = document.querySelector("div#container > textarea"); + selection.setBaseAndExtent(span.firstChild, 2, span.firstChild, 5); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), + span.textContent.substring(2, 5), + `${description}: dataTransfer should have selected text as "text/plain"`); + compareHTML(aEvent.dataTransfer.getData("text/html"), + span.outerHTML.replace(/>.+</, `>${span.textContent.substring(2, 5)}<`), + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: textarea, + } + ) + ) { + is(textarea.value, span.textContent.substring(2, 5), + `${description}: <textarea>.value should be modified`); + is(beforeinputEvents.length, 1, + `${description}: one "beforeinput" event should be fired on <textarea>`); + checkInputEvent(beforeinputEvents[0], textarea, "insertFromDrop", span.textContent.substring(2, 5), null, [], description); + is(inputEvents.length, 1, + `${description}: one "input" event should be fired on <textarea>`); + checkInputEvent(inputEvents[0], textarea, "insertFromDrop", span.textContent.substring(2, 5), null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <textarea>`); + } + document.removeEventListener("drop", onDrop); + })(); + + + // -------- Test dragging contenteditable to <textarea> + await (async function test_dragging_contenteditable_to_textarea_element() { + const description = "dragging text in contenteditable to <textarea>"; + container.innerHTML = "<div contenteditable>Some <b>bold</b> text</div><textarea></textarea>"; + const contenteditable = document.querySelector("div#container > div"); + const textarea = document.querySelector("div#container > textarea"); + const selectionContainers = [contenteditable.firstChild, contenteditable.firstChild.nextSibling.nextSibling]; + selection.setBaseAndExtent(selectionContainers[0], 2, selectionContainers[1], 2); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "me bold t", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "me <b>bold</b> t", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: textarea, + } + ) + ) { + is(contenteditable.innerHTML, "Soext", + `${description}: Dragged range should be removed from contenteditable`); + is(textarea.value, "me bold t", + `${description}: <textarea>.value should be modified`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on contenteditable and <textarea>`); + checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, + [{startContainer: selectionContainers[0], startOffset: 2, + endContainer: selectionContainers[1], endOffset: 2}], + description); + checkInputEvent(beforeinputEvents[1], textarea, "insertFromDrop", "me bold t", null, [], description); + is(inputEvents.length, 2, + `${description}: 2 "input" events should be fired on contenteditable and <textarea>`); + checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, [], description); + checkInputEvent(inputEvents[1], textarea, "insertFromDrop", "me bold t", null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <textarea>`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging contenteditable to <textarea> (canceling "deleteByDrag") + await (async function test_dragging_from_contenteditable_to_textarea_and_canceling_delete_by_drag() { + const description = 'dragging text in contenteditable to <textarea> (canceling "deleteByDrag")'; + container.innerHTML = "<div contenteditable>Some <b>bold</b> text</div><textarea></textarea>"; + const contenteditable = document.querySelector("div#container > div"); + const textarea = document.querySelector("div#container > textarea"); + const selectionContainers = [contenteditable.firstChild, contenteditable.firstChild.nextSibling.nextSibling]; + selection.setBaseAndExtent(selectionContainers[0], 2, selectionContainers[1], 2); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "me bold t", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "me <b>bold</b> t", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultDeleteByDrag); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: textarea, + } + ) + ) { + is(contenteditable.innerHTML, "Some <b>bold</b> text", + `${description}: Dragged range shouldn't be removed from contenteditable`); + is(textarea.value, "me bold t", + `${description}: <textarea>.value should be modified`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on contenteditable and <textarea>`); + checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, + [{startContainer: selectionContainers[0], startOffset: 2, + endContainer: selectionContainers[1], endOffset: 2}], + description); + checkInputEvent(beforeinputEvents[1], textarea, "insertFromDrop", "me bold t", null, [], description); + is(inputEvents.length, 1, + `${description}: only one "input" event should be fired on <textarea>`); + checkInputEvent(inputEvents[0], textarea, "insertFromDrop", "me bold t", null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <textarea>`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultDeleteByDrag); + })(); + + // -------- Test dragging contenteditable to <textarea> (canceling "insertFromDrop") + await (async function test_dragging_from_contenteditable_to_textarea_and_canceling_insert_from_drop() { + const description = 'dragging text in contenteditable to <textarea> (canceling "insertFromDrop")'; + container.innerHTML = "<div contenteditable>Some <b>bold</b> text</div><textarea></textarea>"; + const contenteditable = document.querySelector("div#container > div"); + const textarea = document.querySelector("div#container > textarea"); + const selectionContainers = [contenteditable.firstChild, contenteditable.firstChild.nextSibling.nextSibling]; + selection.setBaseAndExtent(selectionContainers[0], 2, selectionContainers[1], 2); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "me bold t", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "me <b>bold</b> t", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultInsertFromDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: textarea, + } + ) + ) { + is(contenteditable.innerHTML, "Soext", + `${description}: Dragged range should be removed from contenteditable`); + is(textarea.value, "", + `${description}: <textarea>.value shouldn't be modified`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on contenteditable and <textarea>`); + checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, + [{startContainer: selectionContainers[0], startOffset: 2, + endContainer: selectionContainers[1], endOffset: 2}], + description); + checkInputEvent(beforeinputEvents[1], textarea, "insertFromDrop", "me bold t", null, [], description); + is(inputEvents.length, 1, + `${description}: only one "input" event should be fired on contenteditable`); + checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <textarea>`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultInsertFromDrop); + })(); + + // -------- Test dragging contenteditable to same contenteditable + await (async function test_dragging_from_contenteditable_to_itself() { + const description = "dragging text in contenteditable to same contenteditable"; + container.innerHTML = "<div contenteditable><b>bold</b> <span>MMMM</span></div>"; + const contenteditable = document.querySelector("div#container > div"); + const b = document.querySelector("div#container > div > b"); + const span = document.querySelector("div#container > div > span"); + const lastTextNode = span.firstChild; + const selectionContainers = [b.firstChild, b.firstChild]; + selection.setBaseAndExtent(selectionContainers[0], 1, selectionContainers[1], 3); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "ol", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: span, + } + ) + ) { + todo_is(contenteditable.innerHTML, "<b>bd</b> <span>MM<b>ol</b>MM</span>", + `${description}: dragged range should be removed from contenteditable`); + todo_isnot(contenteditable.innerHTML, "<b>bd</b> <span>MMMM</span><b>ol</b>", + `${description}: dragged range should be removed from contenteditable`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on contenteditable`); + checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, + [{startContainer: selectionContainers[0], startOffset: 1, + endContainer: selectionContainers[1], endOffset: 3}], + description); + checkInputEvent(beforeinputEvents[1], contenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [{startContainer: lastTextNode, startOffset: 4, + endContainer: lastTextNode, endOffset: 4}], // XXX unexpected + description); + is(inputEvents.length, 2, + `${description}: 2 "input" events should be fired on contenteditable`); + checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, [], description); + checkInputEvent(inputEvents[1], contenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [], + description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on contenteditable`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging contenteditable to same contenteditable (canceling "deleteByDrag") + await (async function test_dragging_from_contenteditable_to_itself_and_canceling_delete_by_drag() { + const description = 'dragging text in contenteditable to same contenteditable (canceling "deleteByDrag")'; + container.innerHTML = "<div contenteditable><b>bold</b> <span>MMMM</span></div>"; + const contenteditable = document.querySelector("div#container > div"); + const b = document.querySelector("div#container > div > b"); + const span = document.querySelector("div#container > div > span"); + const lastTextNode = span.firstChild; + const selectionContainers = [b.firstChild, b.firstChild]; + selection.setBaseAndExtent(selectionContainers[0], 1, selectionContainers[1], 3); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "ol", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultDeleteByDrag); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: span, + } + ) + ) { + todo_is(contenteditable.innerHTML, "<b>bold</b> <span>MM<b>ol</b>MM</span>", + `${description}: dragged range shouldn't be removed from contenteditable`); + todo_isnot(contenteditable.innerHTML, "<b>bold</b> <span>MMMM</span><b>ol</b>", + `${description}: dragged range shouldn't be removed from contenteditable`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on contenteditable`); + checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, + [{startContainer: selectionContainers[0], startOffset: 1, + endContainer: selectionContainers[1], endOffset: 3}], + description); + checkInputEvent(beforeinputEvents[1], contenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [{startContainer: lastTextNode, startOffset: 4, + endContainer: lastTextNode, endOffset: 4}], // XXX unexpected + description); + is(inputEvents.length, 1, + `${description}: only one "input" event should be fired on contenteditable`); + checkInputEvent(inputEvents[0], contenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [], + description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on contenteditable`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultDeleteByDrag); + })(); + + // -------- Test dragging contenteditable to same contenteditable (canceling "insertFromDrop") + await (async function test_dragging_from_contenteditable_to_itself_and_canceling_insert_from_drop() { + const description = 'dragging text in contenteditable to same contenteditable (canceling "insertFromDrop")'; + container.innerHTML = "<div contenteditable><b>bold</b> <span>MMMM</span></div>"; + const contenteditable = document.querySelector("div#container > div"); + const b = document.querySelector("div#container > div > b"); + const span = document.querySelector("div#container > div > span"); + const lastTextNode = span.firstChild; + const selectionContainers = [b.firstChild, b.firstChild]; + selection.setBaseAndExtent(selectionContainers[0], 1, selectionContainers[1], 3); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "ol", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultInsertFromDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: span, + } + ) + ) { + is(contenteditable.innerHTML, "<b>bd</b> <span>MMMM</span>", + `${description}: dragged range should be removed from contenteditable`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on contenteditable`); + checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, + [{startContainer: selectionContainers[0], startOffset: 1, + endContainer: selectionContainers[1], endOffset: 3}], + description); + checkInputEvent(beforeinputEvents[1], contenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [{startContainer: lastTextNode, startOffset: 4, + endContainer: lastTextNode, endOffset: 4}], + description); + is(inputEvents.length, 1, + `${description}: only one "input" event should be fired on contenteditable`); + checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on contenteditable`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultInsertFromDrop); + })(); + + // -------- Test copy-dragging contenteditable to same contenteditable + await (async function test_copy_dragging_from_contenteditable_to_itself() { + const description = "copy-dragging text in contenteditable to same contenteditable"; + container.innerHTML = "<div contenteditable><b>bold</b> <span>MMMM</span></div>"; + document.documentElement.scrollTop; + const contenteditable = document.querySelector("div#container > div"); + const b = document.querySelector("div#container > div > b"); + const span = document.querySelector("div#container > div > span"); + const lastTextNode = span.firstChild; + selection.setBaseAndExtent(b.firstChild, 1, b.firstChild, 3); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "ol", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: span, + dragEvent: kModifiersToCopy, + } + ) + ) { + todo_is(contenteditable.innerHTML, "<b>bold</b> <span>MM<b>ol</b>MM</span>", + `${description}: dragged range shouldn't be removed from contenteditable`); + todo_isnot(contenteditable.innerHTML, "<b>bold</b> <span>MMMM</span><b>ol</b>", + `${description}: dragged range shouldn't be removed from contenteditable`); + is(beforeinputEvents.length, 1, + `${description}: only 1 "beforeinput" events should be fired on contenteditable`); + checkInputEvent(beforeinputEvents[0], contenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [{startContainer: lastTextNode, startOffset: 4, + endContainer: lastTextNode, endOffset: 4}], // XXX unexpected + description); + is(inputEvents.length, 1, + `${description}: only 1 "input" events should be fired on contenteditable`); + checkInputEvent(inputEvents[0], contenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [], + description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on contenteditable`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging contenteditable to other contenteditable + await (async function test_dragging_from_contenteditable_to_other_contenteditable() { + const description = "dragging text in contenteditable to other contenteditable"; + container.innerHTML = '<div contenteditable><b>bold</b></div><hr><div contenteditable style="min-height: 3em;"></div>'; + const contenteditable = document.querySelector("div#container > div"); + const b = document.querySelector("div#container > div > b"); + const otherContenteditable = document.querySelector("div#container > div ~ div"); + const selectionContainers = [b.firstChild, b.firstChild]; + selection.setBaseAndExtent(selectionContainers[0], 1, selectionContainers[1], 3); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "ol", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: otherContenteditable, + } + ) + ) { + is(contenteditable.innerHTML, "<b>bd</b>", + `${description}: dragged range should be removed from contenteditable`); + is(otherContenteditable.innerHTML, "<b>ol</b>", + `${description}: dragged content should be inserted into other contenteditable`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on contenteditable`); + checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, + [{startContainer: selectionContainers[0], startOffset: 1, + endContainer: selectionContainers[1], endOffset: 3}], + description); + checkInputEvent(beforeinputEvents[1], otherContenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [{startContainer: otherContenteditable, startOffset: 0, + endContainer: otherContenteditable, endOffset: 0}], + description); + is(inputEvents.length, 2, + `${description}: 2 "input" events should be fired on contenteditable`); + checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, [], description); + checkInputEvent(inputEvents[1], otherContenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [], + description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on other contenteditable`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging contenteditable to other contenteditable (canceling "deleteByDrag") + await (async function test_dragging_from_contenteditable_to_other_contenteditable_and_canceling_delete_by_drag() { + const description = 'dragging text in contenteditable to other contenteditable (canceling "deleteByDrag")'; + container.innerHTML = '<div contenteditable><b>bold</b></div><hr><div contenteditable style="min-height: 3em;"></div>'; + const contenteditable = document.querySelector("div#container > div"); + const b = document.querySelector("div#container > div > b"); + const otherContenteditable = document.querySelector("div#container > div ~ div"); + const selectionContainers = [b.firstChild, b.firstChild]; + selection.setBaseAndExtent(selectionContainers[0], 1, selectionContainers[1], 3); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "ol", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultDeleteByDrag); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: otherContenteditable, + } + ) + ) { + is(contenteditable.innerHTML, "<b>bold</b>", + `${description}: dragged range shouldn't be removed from contenteditable`); + is(otherContenteditable.innerHTML, "<b>ol</b>", + `${description}: dragged content should be inserted into other contenteditable`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on contenteditable`); + checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, + [{startContainer: selectionContainers[0], startOffset: 1, + endContainer: selectionContainers[1], endOffset: 3}], + description); + checkInputEvent(beforeinputEvents[1], otherContenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [{startContainer: otherContenteditable, startOffset: 0, + endContainer: otherContenteditable, endOffset: 0}], + description); + is(inputEvents.length, 1, + `${description}: only one "input" event should be fired on other contenteditable`); + checkInputEvent(inputEvents[0], otherContenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [], + description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on other contenteditable`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultDeleteByDrag); + })(); + + // -------- Test dragging contenteditable to other contenteditable (canceling "insertFromDrop") + await (async function test_dragging_from_contenteditable_to_other_contenteditable_and_canceling_insert_from_drop() { + const description = 'dragging text in contenteditable to other contenteditable (canceling "insertFromDrop")'; + container.innerHTML = '<div contenteditable><b>bold</b></div><hr><div contenteditable style="min-height: 3em;"></div>'; + const contenteditable = document.querySelector("div#container > div"); + const b = document.querySelector("div#container > div > b"); + const otherContenteditable = document.querySelector("div#container > div ~ div"); + const selectionContainers = [b.firstChild, b.firstChild]; + selection.setBaseAndExtent(selectionContainers[0], 1, selectionContainers[1], 3); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "ol", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultInsertFromDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: otherContenteditable, + } + ) + ) { + is(contenteditable.innerHTML, "<b>bd</b>", + `${description}: dragged range should be removed from contenteditable`); + is(otherContenteditable.innerHTML, "", + `${description}: dragged content shouldn't be inserted into other contenteditable`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on contenteditable`); + checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, + [{startContainer: selectionContainers[0], startOffset: 1, + endContainer: selectionContainers[1], endOffset: 3}], + description); + checkInputEvent(beforeinputEvents[1], otherContenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [{startContainer: otherContenteditable, startOffset: 0, + endContainer: otherContenteditable, endOffset: 0}], + description); + is(inputEvents.length, 1, + `${description}: only one "input" event should be fired on contenteditable`); + checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on other contenteditable`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultInsertFromDrop); + })(); + + // -------- Test copy-dragging contenteditable to other contenteditable + await (async function test_copy_dragging_from_contenteditable_to_other_contenteditable() { + const description = "copy-dragging text in contenteditable to other contenteditable"; + container.innerHTML = '<div contenteditable><b>bold</b></div><hr><div contenteditable style="min-height: 3em;"></div>'; + const contenteditable = document.querySelector("div#container > div"); + const b = document.querySelector("div#container > div > b"); + const otherContenteditable = document.querySelector("div#container > div ~ div"); + selection.setBaseAndExtent(b.firstChild, 1, b.firstChild, 3); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "ol", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: otherContenteditable, + dragEvent: kModifiersToCopy, + } + ) + ) { + is(contenteditable.innerHTML, "<b>bold</b>", + `${description}: dragged range shouldn't be removed from contenteditable`); + is(otherContenteditable.innerHTML, "<b>ol</b>", + `${description}: dragged content should be inserted into other contenteditable`); + is(beforeinputEvents.length, 1, + `${description}: only one "beforeinput" events should be fired on other contenteditable`); + checkInputEvent(beforeinputEvents[0], otherContenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [{startContainer: otherContenteditable, startOffset: 0, + endContainer: otherContenteditable, endOffset: 0}], + description); + is(inputEvents.length, 1, + `${description}: only one "input" events should be fired on other contenteditable`); + checkInputEvent(inputEvents[0], otherContenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [], + description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on other contenteditable`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging nested contenteditable to contenteditable + await (async function test_dragging_from_nested_contenteditable_to_contenteditable() { + const description = "dragging text in nested contenteditable to contenteditable"; + container.innerHTML = '<div contenteditable><p><br></p><div contenteditable="false"><p contenteditable><b>bold</b></p></div></div>'; + const contenteditable = document.querySelector("div#container > div"); + const otherContenteditable = document.querySelector("div#container > div > div > p"); + const b = document.querySelector("div#container > div > div > p > b"); + contenteditable.focus(); + const selectionContainers = [b.firstChild, b.firstChild]; + selection.setBaseAndExtent(selectionContainers[0], 1, selectionContainers[1], 3); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "ol", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: contenteditable.firstChild, + } + ) + ) { + is(contenteditable.innerHTML, '<p><b>ol</b></p><div contenteditable="false"><p contenteditable=""><b>bd</b></p></div>', + `${description}: dragged range should be moved from nested contenteditable to the contenteditable`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on contenteditable`); + checkInputEvent(beforeinputEvents[0], otherContenteditable, "deleteByDrag", null, null, + [{startContainer: selectionContainers[0], startOffset: 1, + endContainer: selectionContainers[1], endOffset: 3}], + description); + checkInputEvent(beforeinputEvents[1], contenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [{startContainer: contenteditable.firstChild, startOffset: 0, + endContainer: contenteditable.firstChild, endOffset: 0}], + description); + is(inputEvents.length, 2, + `${description}: 2 "input" events should be fired on contenteditable`); + checkInputEvent(inputEvents[0], otherContenteditable, "deleteByDrag", null, null, [], description); + checkInputEvent(inputEvents[1], contenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [], + description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on contenteditable`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging nested contenteditable to contenteditable (canceling "deleteByDrag") + await (async function test_dragging_from_nested_contenteditable_to_contenteditable_canceling_delete_by_drag() { + const description = 'dragging text in nested contenteditable to contenteditable (canceling "deleteByDrag")'; + container.innerHTML = '<div contenteditable><p><br></p><div contenteditable="false"><p contenteditable><b>bold</b></p></div></div>'; + const contenteditable = document.querySelector("div#container > div"); + const otherContenteditable = document.querySelector("div#container > div > div > p"); + const b = document.querySelector("div#container > div > div > p > b"); + contenteditable.focus(); + const selectionContainers = [b.firstChild, b.firstChild]; + selection.setBaseAndExtent(selectionContainers[0], 1, selectionContainers[1], 3); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "ol", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultDeleteByDrag); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: contenteditable.firstChild, + } + ) + ) { + is(contenteditable.innerHTML, '<p><b>ol</b></p><div contenteditable="false"><p contenteditable=""><b>bold</b></p></div>', + `${description}: dragged range should be copied from nested contenteditable to the contenteditable`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on contenteditable`); + checkInputEvent(beforeinputEvents[0], otherContenteditable, "deleteByDrag", null, null, + [{startContainer: selectionContainers[0], startOffset: 1, + endContainer: selectionContainers[1], endOffset: 3}], + description); + checkInputEvent(beforeinputEvents[1], contenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [{startContainer: contenteditable.firstChild, startOffset: 0, + endContainer: contenteditable.firstChild, endOffset: 0}], + description); + is(inputEvents.length, 1, + `${description}: only one "input" event should be fired on contenteditable`); + checkInputEvent(inputEvents[0], contenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [], + description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on contenteditable`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultDeleteByDrag); + })(); + + // -------- Test dragging nested contenteditable to contenteditable (canceling "insertFromDrop") + await (async function test_dragging_from_nested_contenteditable_to_contenteditable_canceling_insert_from_drop() { + const description = 'dragging text in nested contenteditable to contenteditable (canceling "insertFromDrop")'; + container.innerHTML = '<div contenteditable><p><br></p><div contenteditable="false"><p contenteditable><b>bold</b></p></div></div>'; + const contenteditable = document.querySelector("div#container > div"); + const otherContenteditable = document.querySelector("div#container > div > div > p"); + const b = document.querySelector("div#container > div > div > p > b"); + contenteditable.focus(); + const selectionContainers = [b.firstChild, b.firstChild]; + selection.setBaseAndExtent(selectionContainers[0], 1, selectionContainers[1], 3); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "ol", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultInsertFromDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: contenteditable.firstChild, + } + ) + ) { + is(contenteditable.innerHTML, '<p><br></p><div contenteditable="false"><p contenteditable=""><b>bd</b></p></div>', + `${description}: dragged range should be removed from nested contenteditable`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on contenteditable`); + checkInputEvent(beforeinputEvents[0], otherContenteditable, "deleteByDrag", null, null, + [{startContainer: selectionContainers[0], startOffset: 1, + endContainer: selectionContainers[1], endOffset: 3}], + description); + checkInputEvent(beforeinputEvents[1], contenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [{startContainer: contenteditable.firstChild, startOffset: 0, + endContainer: contenteditable.firstChild, endOffset: 0}], + description); + is(inputEvents.length, 1, + `${description}: only one "input" event should be fired on nested contenteditable`); + checkInputEvent(inputEvents[0], otherContenteditable, "deleteByDrag", null, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on contenteditable`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultInsertFromDrop); + })(); + + // -------- Test copy-dragging nested contenteditable to contenteditable + await (async function test_copy_dragging_from_nested_contenteditable_to_contenteditable() { + const description = "copy-dragging text in nested contenteditable to contenteditable"; + container.innerHTML = '<div contenteditable><p><br></p><div contenteditable="false"><p contenteditable><b>bold</b></p></div></div>'; + const contenteditable = document.querySelector("div#container > div"); + const b = document.querySelector("div#container > div > div > p > b"); + contenteditable.focus(); + selection.setBaseAndExtent(b.firstChild, 1, b.firstChild, 3); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "ol", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: contenteditable.firstChild, + dragEvent: kModifiersToCopy, + } + ) + ) { + is(contenteditable.innerHTML, '<p><b>ol</b></p><div contenteditable="false"><p contenteditable=""><b>bold</b></p></div>', + `${description}: dragged range should be moved from nested contenteditable to the contenteditable`); + is(beforeinputEvents.length, 1, + `${description}: only one "beforeinput" events should be fired on contenteditable`); + checkInputEvent(beforeinputEvents[0], contenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [{startContainer: contenteditable.firstChild, startOffset: 0, + endContainer: contenteditable.firstChild, endOffset: 0}], + description); + is(inputEvents.length, 1, + `${description}: only one "input" events should be fired on contenteditable`); + checkInputEvent(inputEvents[0], contenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [], + description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on contenteditable`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging contenteditable to nested contenteditable + await (async function test_dragging_from_contenteditable_to_nested_contenteditable() { + const description = "dragging text in contenteditable to nested contenteditable"; + container.innerHTML = '<div contenteditable><p><b>bold</b></p><div contenteditable="false"><p contenteditable><br></p></div></div>'; + const contenteditable = document.querySelector("div#container > div"); + const b = document.querySelector("div#container > div > p > b"); + const otherContenteditable = document.querySelector("div#container > div > div > p"); + contenteditable.focus(); + const selectionContainers = [b.firstChild, b.firstChild]; + selection.setBaseAndExtent(selectionContainers[0], 1, selectionContainers[1], 3); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "ol", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: otherContenteditable, + } + ) + ) { + is(contenteditable.innerHTML, '<p><b>bd</b></p><div contenteditable="false"><p contenteditable=""><b>ol</b></p></div>', + `${description}: dragged range should be moved from contenteditable to nested contenteditable`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on contenteditable and nested contenteditable`); + checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, + [{startContainer: selectionContainers[0], startOffset: 1, + endContainer: selectionContainers[1], endOffset: 3}], + description); + checkInputEvent(beforeinputEvents[1], otherContenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [{startContainer: otherContenteditable, startOffset: 0, + endContainer: otherContenteditable, endOffset: 0}], + description); + is(inputEvents.length, 2, + `${description}: 2 "input" events should be fired on contenteditable and nested contenteditable`); + checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, [], description); + checkInputEvent(inputEvents[1], otherContenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [], + description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on contenteditable`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging contenteditable to nested contenteditable (canceling "deleteByDrag") + await (async function test_dragging_from_contenteditable_to_nested_contenteditable_and_canceling_delete_by_drag() { + const description = 'dragging text in contenteditable to nested contenteditable (canceling "deleteByDrag")'; + container.innerHTML = '<div contenteditable><p><b>bold</b></p><div contenteditable="false"><p contenteditable><br></p></div></div>'; + const contenteditable = document.querySelector("div#container > div"); + const b = document.querySelector("div#container > div > p > b"); + const otherContenteditable = document.querySelector("div#container > div > div > p"); + contenteditable.focus(); + const selectionContainers = [b.firstChild, b.firstChild]; + selection.setBaseAndExtent(selectionContainers[0], 1, selectionContainers[1], 3); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "ol", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultDeleteByDrag); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: otherContenteditable, + } + ) + ) { + is(contenteditable.innerHTML, '<p><b>bold</b></p><div contenteditable="false"><p contenteditable=""><b>ol</b></p></div>', + `${description}: dragged range should be copied from contenteditable to nested contenteditable`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on contenteditable and nested contenteditable`); + checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, + [{startContainer: selectionContainers[0], startOffset: 1, + endContainer: selectionContainers[1], endOffset: 3}], + description); + checkInputEvent(beforeinputEvents[1], otherContenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [{startContainer: otherContenteditable, startOffset: 0, + endContainer: otherContenteditable, endOffset: 0}], + description); + is(inputEvents.length, 1, + `${description}: only one "input" event should be fired on contenteditable and nested contenteditable`); + checkInputEvent(inputEvents[0], otherContenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [], + description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on contenteditable`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultDeleteByDrag); + })(); + + // -------- Test dragging contenteditable to nested contenteditable (canceling "insertFromDrop") + await (async function test_dragging_from_contenteditable_to_nested_contenteditable_and_canceling_insert_from_drop() { + const description = 'dragging text in contenteditable to nested contenteditable (canceling "insertFromDrop")'; + container.innerHTML = '<div contenteditable><p><b>bold</b></p><div contenteditable="false"><p contenteditable><br></p></div></div>'; + const contenteditable = document.querySelector("div#container > div"); + const b = document.querySelector("div#container > div > p > b"); + const otherContenteditable = document.querySelector("div#container > div > div > p"); + contenteditable.focus(); + const selectionContainers = [b.firstChild, b.firstChild]; + selection.setBaseAndExtent(selectionContainers[0], 1, selectionContainers[1], 3); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "ol", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultInsertFromDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: otherContenteditable, + } + ) + ) { + is(contenteditable.innerHTML, '<p><b>bd</b></p><div contenteditable="false"><p contenteditable=""><br></p></div>', + `${description}: dragged range should be removed from contenteditable`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on contenteditable and nested contenteditable`); + checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, + [{startContainer: selectionContainers[0], startOffset: 1, + endContainer: selectionContainers[1], endOffset: 3}], + description); + checkInputEvent(beforeinputEvents[1], otherContenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [{startContainer: otherContenteditable, startOffset: 0, + endContainer: otherContenteditable, endOffset: 0}], + description); + is(inputEvents.length, 1, + `${description}: only one "input" event should be fired on contenteditable`); + checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on contenteditable`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultInsertFromDrop); + })(); + + // -------- Test copy-dragging contenteditable to nested contenteditable + await (async function test_copy_dragging_from_contenteditable_to_nested_contenteditable() { + const description = "copy-dragging text in contenteditable to nested contenteditable"; + container.innerHTML = '<div contenteditable><p><b>bold</b></p><div contenteditable="false"><p contenteditable><br></p></div></div>'; + const contenteditable = document.querySelector("div#container > div"); + const b = document.querySelector("div#container > div > p > b"); + const otherContenteditable = document.querySelector("div#container > div > div > p"); + contenteditable.focus(); + selection.setBaseAndExtent(b.firstChild, 1, b.firstChild, 3); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + is(aEvent.dataTransfer.getData("text/plain"), "ol", + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "<b>ol</b>", + `${description}: dataTransfer should have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: otherContenteditable, + dragEvent: kModifiersToCopy, + } + ) + ) { + is(contenteditable.innerHTML, '<p><b>bold</b></p><div contenteditable="false"><p contenteditable=""><b>ol</b></p></div>', + `${description}: dragged range should be moved from nested contenteditable to the contenteditable`); + is(beforeinputEvents.length, 1, + `${description}: only one "beforeinput" events should be fired on contenteditable`); + checkInputEvent(beforeinputEvents[0], otherContenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [{startContainer: otherContenteditable, startOffset: 0, + endContainer: otherContenteditable, endOffset: 0}], + description); + is(inputEvents.length, 1, + `${description}: only one "input" events should be fired on contenteditable`); + checkInputEvent(inputEvents[0], otherContenteditable, "insertFromDrop", null, + [{type: "text/html", data: "<b>ol</b>"}, + {type: "text/plain", data: "ol"}], + [], + description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on contenteditable`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging text in <input> to contenteditable + await (async function test_dragging_from_input_element_to_contenteditable() { + const description = "dragging text in <input> to contenteditable"; + container.innerHTML = '<input value="Some Text"><div contenteditable><br></div>'; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const input = document.querySelector("div#container > input"); + const contenteditable = document.querySelector("div#container > div"); + input.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(input).editor.selection, + destElement: contenteditable, + } + ) + ) { + is(input.value, "Somt", + `${description}: dragged range should be removed from <input>`); + is(contenteditable.innerHTML, "e Tex<br>", + `${description}: dragged content should be inserted into contenteditable`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on <input> and contenteditable`); + checkInputEvent(beforeinputEvents[0], input, "deleteByDrag", null, null, [], description); + checkInputEvent(beforeinputEvents[1], contenteditable, "insertFromDrop", null, + [{type: "text/plain", data: "e Tex"}], + [{startContainer: contenteditable, startOffset: 0, + endContainer: contenteditable, endOffset: 0}], + description); + is(inputEvents.length, 2, + `${description}: 2 "input" events should be fired on <input> and contenteditable`); + checkInputEvent(inputEvents[0], input, "deleteByDrag", null, null, [], description); + checkInputEvent(inputEvents[1], contenteditable, "insertFromDrop", null, + [{type: "text/plain", data: "e Tex"}], + [], + description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on other contenteditable`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging text in <input> to contenteditable (canceling "deleteByDrag") + await (async function test_dragging_from_input_element_to_contenteditable_and_canceling_delete_by_drag() { + const description = 'dragging text in <input> to contenteditable (canceling "deleteByDrag")'; + container.innerHTML = '<input value="Some Text"><div contenteditable><br></div>'; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const input = document.querySelector("div#container > input"); + const contenteditable = document.querySelector("div#container > div"); + input.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultDeleteByDrag); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(input).editor.selection, + destElement: contenteditable, + } + ) + ) { + is(input.value, "Some Text", + `${description}: dragged range shouldn't be removed from <input>`); + is(contenteditable.innerHTML, "e Tex<br>", + `${description}: dragged content should be inserted into contenteditable`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on <input> and contenteditable`); + checkInputEvent(beforeinputEvents[0], input, "deleteByDrag", null, null, [], description); + checkInputEvent(beforeinputEvents[1], contenteditable, "insertFromDrop", null, + [{type: "text/plain", data: "e Tex"}], + [{startContainer: contenteditable, startOffset: 0, + endContainer: contenteditable, endOffset: 0}], + description); + is(inputEvents.length, 1, + `${description}: only one "input" events should be fired on contenteditable`); + checkInputEvent(inputEvents[0], contenteditable, "insertFromDrop", null, + [{type: "text/plain", data: "e Tex"}], + [], + description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on other contenteditable`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultDeleteByDrag); + })(); + + // -------- Test dragging text in <input> to contenteditable (canceling "insertFromDrop") + await (async function test_dragging_from_input_element_to_contenteditable_and_canceling_insert_from_drop() { + const description = 'dragging text in <input> to contenteditable (canceling "insertFromDrop")'; + container.innerHTML = '<input value="Some Text"><div contenteditable><br></div>'; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const input = document.querySelector("div#container > input"); + const contenteditable = document.querySelector("div#container > div"); + input.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultInsertFromDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(input).editor.selection, + destElement: contenteditable, + } + ) + ) { + is(input.value, "Somt", + `${description}: dragged range should be removed from <input>`); + is(contenteditable.innerHTML, "<br>", + `${description}: dragged content shouldn't be inserted into contenteditable`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on <input> and contenteditable`); + checkInputEvent(beforeinputEvents[0], input, "deleteByDrag", null, null, [], description); + checkInputEvent(beforeinputEvents[1], contenteditable, "insertFromDrop", null, + [{type: "text/plain", data: "e Tex"}], + [{startContainer: contenteditable, startOffset: 0, + endContainer: contenteditable, endOffset: 0}], + description); + is(inputEvents.length, 1, + `${description}: only one "input" event should be fired on <input>`); + checkInputEvent(inputEvents[0], input, "deleteByDrag", null, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on other contenteditable`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultInsertFromDrop); + })(); + + // -------- Test copy-dragging text in <input> to contenteditable + await (async function test_copy_dragging_from_input_element_to_contenteditable() { + const description = "copy-dragging text in <input> to contenteditable"; + container.innerHTML = '<input value="Some Text"><div contenteditable><br></div>'; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const input = document.querySelector("div#container > input"); + const contenteditable = document.querySelector("div#container > div"); + input.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(input).editor.selection, + destElement: contenteditable, + dragEvent: kModifiersToCopy, + } + ) + ) { + is(input.value, "Some Text", + `${description}: dragged range shouldn't be removed from <input>`); + is(contenteditable.innerHTML, "e Tex<br>", + `${description}: dragged content should be inserted into contenteditable`); + is(beforeinputEvents.length, 1, + `${description}: only one "beforeinput" events should be fired on contenteditable`); + checkInputEvent(beforeinputEvents[0], contenteditable, "insertFromDrop", null, + [{type: "text/plain", data: "e Tex"}], + [{startContainer: contenteditable, startOffset: 0, + endContainer: contenteditable, endOffset: 0}], + description); + is(inputEvents.length, 1, + `${description}: only one "input" events should be fired on contenteditable`); + checkInputEvent(inputEvents[0], contenteditable, "insertFromDrop", null, + [{type: "text/plain", data: "e Tex"}], + [], + description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on other contenteditable`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging text in <textarea> to contenteditable + await (async function test_dragging_from_textarea_element_to_contenteditable() { + const description = "dragging text in <textarea> to contenteditable"; + container.innerHTML = '<textarea>Line1\nLine2</textarea><div contenteditable><br></div>'; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const textarea = document.querySelector("div#container > textarea"); + const contenteditable = document.querySelector("div#container > div"); + textarea.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), textarea.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(textarea).editor.selection, + destElement: contenteditable, + } + ) + ) { + is(textarea.value, "Linne2", + `${description}: dragged range should be removed from <textarea>`); + todo_is(contenteditable.innerHTML, "<div>e1</div><div>Li</div>", + `${description}: dragged content should be inserted into contenteditable`); + todo_isnot(contenteditable.innerHTML, "e1<br>Li<br>", + `${description}: dragged content should be inserted into contenteditable`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on <input> and contenteditable`); + checkInputEvent(beforeinputEvents[0], textarea, "deleteByDrag", null, null, [], description); + checkInputEvent(beforeinputEvents[1], contenteditable, "insertFromDrop", null, + [{type: "text/plain", data: `e1${kNativeLF}Li`}], + [{startContainer: contenteditable, startOffset: 0, + endContainer: contenteditable, endOffset: 0}], + description); + is(inputEvents.length, 2, + `${description}: 2 "input" events should be fired on <input> and contenteditable`); + checkInputEvent(inputEvents[0], textarea, "deleteByDrag", null, null, [], description); + checkInputEvent(inputEvents[1], contenteditable, "insertFromDrop", null, + [{type: "text/plain", data: `e1${kNativeLF}Li`}], + [], + description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on other contenteditable`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test copy-dragging text in <textarea> to contenteditable + await (async function test_copy_dragging_from_textarea_element_to_contenteditable() { + const description = "copy-dragging text in <textarea> to contenteditable"; + container.innerHTML = '<textarea>Line1\nLine2</textarea><div contenteditable><br></div>'; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const textarea = document.querySelector("div#container > textarea"); + const contenteditable = document.querySelector("div#container > div"); + textarea.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), textarea.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(textarea).editor.selection, + destElement: contenteditable, + dragEvent: kModifiersToCopy, + } + ) + ) { + is(textarea.value, "Line1\nLine2", + `${description}: dragged range should be removed from <textarea>`); + todo_is(contenteditable.innerHTML, "<div>e1</div><div>Li</div>", + `${description}: dragged content should be inserted into contenteditable`); + todo_isnot(contenteditable.innerHTML, "e1<br>Li<br>", + `${description}: dragged content should be inserted into contenteditable`); + is(beforeinputEvents.length, 1, + `${description}: only one "beforeinput" events should be fired on contenteditable`); + checkInputEvent(beforeinputEvents[0], contenteditable, "insertFromDrop", null, + [{type: "text/plain", data: `e1${kNativeLF}Li`}], + [{startContainer: contenteditable, startOffset: 0, + endContainer: contenteditable, endOffset: 0}], + description); + is(inputEvents.length, 1, + `${description}: only one "input" events should be fired on contenteditable`); + checkInputEvent(inputEvents[0], contenteditable, "insertFromDrop", null, + [{type: "text/plain", data: `e1${kNativeLF}Li`}], + [], + description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on other contenteditable`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging text in <input> to other <input> + await (async function test_dragging_from_input_element_to_other_input_element() { + const description = "dragging text in <input> to other <input>"; + container.innerHTML = '<input value="Some Text"><input>'; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const input = document.querySelector("div#container > input"); + const otherInput = document.querySelector("div#container > input + input"); + input.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(input).editor.selection, + destElement: otherInput, + } + ) + ) { + is(input.value, "Somt", + `${description}: dragged range should be removed from <input>`); + is(otherInput.value, "e Tex", + `${description}: dragged content should be inserted into other <input>`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on <input> and other <input>`); + checkInputEvent(beforeinputEvents[0], input, "deleteByDrag", null, null, [], description); + checkInputEvent(beforeinputEvents[1], otherInput, "insertFromDrop", "e Tex", null, [], description); + is(inputEvents.length, 2, + `${description}: 2 "input" events should be fired on <input> and other <input>`); + checkInputEvent(inputEvents[0], input, "deleteByDrag", null, null, [], description); + checkInputEvent(inputEvents[1], otherInput, "insertFromDrop", "e Tex", null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on other <input>`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging text in <input> to other <input> (canceling "deleteByDrag") + await (async function test_dragging_from_input_element_to_other_input_element_and_canceling_delete_by_drag() { + const description = 'dragging text in <input> to other <input> (canceling "deleteByDrag")'; + container.innerHTML = '<input value="Some Text"><input>'; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const input = document.querySelector("div#container > input"); + const otherInput = document.querySelector("div#container > input + input"); + input.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultDeleteByDrag); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(input).editor.selection, + destElement: otherInput, + } + ) + ) { + is(input.value, "Some Text", + `${description}: dragged range shouldn't be removed from <input>`); + is(otherInput.value, "e Tex", + `${description}: dragged content should be inserted into other <input>`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on <input> and other <input>`); + checkInputEvent(beforeinputEvents[0], input, "deleteByDrag", null, null, [], description); + checkInputEvent(beforeinputEvents[1], otherInput, "insertFromDrop", "e Tex", null, [], description); + is(inputEvents.length, 1, + `${description}: only one "input" events should be fired on other <input>`); + checkInputEvent(inputEvents[0], otherInput, "insertFromDrop", "e Tex", null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on other <input>`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultDeleteByDrag); + })(); + + // -------- Test dragging text in <input> to other <input> (canceling "insertFromDrop") + await (async function test_dragging_from_input_element_to_other_input_element_and_canceling_insert_from_drop() { + const description = 'dragging text in <input> to other <input> (canceling "insertFromDrop")'; + container.innerHTML = '<input value="Some Text"><input>'; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const input = document.querySelector("div#container > input"); + const otherInput = document.querySelector("div#container > input + input"); + input.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultInsertFromDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(input).editor.selection, + destElement: otherInput, + }, + ) + ) { + is(input.value, "Somt", + `${description}: dragged range should be removed from <input>`); + is(otherInput.value, "", + `${description}: dragged content shouldn't be inserted into other <input>`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on <input> and other <input>`); + checkInputEvent(beforeinputEvents[0], input, "deleteByDrag", null, null, [], description); + checkInputEvent(beforeinputEvents[1], otherInput, "insertFromDrop", "e Tex", null, [], description); + is(inputEvents.length, 1, + `${description}: only one "input" event should be fired on <input>`); + checkInputEvent(inputEvents[0], input, "deleteByDrag", null, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on other <input>`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultInsertFromDrop); + })(); + + // -------- Test copy-dragging text in <input> to other <input> + await (async function test_copy_dragging_from_input_element_to_other_input_element() { + const description = "copy-dragging text in <input> to other <input>"; + container.innerHTML = '<input value="Some Text"><input>'; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const input = document.querySelector("div#container > input"); + const otherInput = document.querySelector("div#container > input + input"); + input.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(input).editor.selection, + destElement: otherInput, + dragEvent: kModifiersToCopy, + } + ) + ) { + is(input.value, "Some Text", + `${description}: dragged range shouldn't be removed from <input>`); + is(otherInput.value, "e Tex", + `${description}: dragged content should be inserted into other <input>`); + is(beforeinputEvents.length, 1, + `${description}: only one "beforeinput" events should be fired on other <input>`); + checkInputEvent(beforeinputEvents[0], otherInput, "insertFromDrop", "e Tex", null, [], description); + is(inputEvents.length, 1, + `${description}: only one "input" events should be fired on other <input>`); + checkInputEvent(inputEvents[0], otherInput, "insertFromDrop", "e Tex", null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on other <input>`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging text in <input> to <textarea> + await (async function test_dragging_from_input_element_to_textarea_element() { + const description = "dragging text in <input> to other <textarea>"; + container.innerHTML = '<input value="Some Text"><textarea></textarea>'; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const input = document.querySelector("div#container > input"); + const textarea = document.querySelector("div#container > textarea"); + input.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(input).editor.selection, + destElement: textarea, + } + ) + ) { + is(input.value, "Somt", + `${description}: dragged range should be removed from <input>`); + is(textarea.value, "e Tex", + `${description}: dragged content should be inserted into <textarea>`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on <input> and <textarea>`); + checkInputEvent(beforeinputEvents[0], input, "deleteByDrag", null, null, [], description); + checkInputEvent(beforeinputEvents[1], textarea, "insertFromDrop", "e Tex", null, [], description); + is(inputEvents.length, 2, + `${description}: 2 "input" events should be fired on <input> and <textarea>`); + checkInputEvent(inputEvents[0], input, "deleteByDrag", null, null, [], description); + checkInputEvent(inputEvents[1], textarea, "insertFromDrop", "e Tex", null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <textarea>`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging text in <input> to <textarea> (canceling "deleteByDrag") + await (async function test_dragging_from_input_element_to_textarea_element_and_canceling_delete_by_drag() { + const description = 'dragging text in <input> to other <textarea> (canceling "deleteByDrag")'; + container.innerHTML = '<input value="Some Text"><textarea></textarea>'; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const input = document.querySelector("div#container > input"); + const textarea = document.querySelector("div#container > textarea"); + input.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultDeleteByDrag); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(input).editor.selection, + destElement: textarea, + } + ) + ) { + is(input.value, "Some Text", + `${description}: dragged range shouldn't be removed from <input>`); + is(textarea.value, "e Tex", + `${description}: dragged content should be inserted into <textarea>`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on <input> and <textarea>`); + checkInputEvent(beforeinputEvents[0], input, "deleteByDrag", null, null, [], description); + checkInputEvent(beforeinputEvents[1], textarea, "insertFromDrop", "e Tex", null, [], description); + is(inputEvents.length, 1, + `${description}: only one "input" event should be fired on <textarea>`); + checkInputEvent(inputEvents[0], textarea, "insertFromDrop", "e Tex", null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <textarea>`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultDeleteByDrag); + })(); + + // -------- Test dragging text in <input> to <textarea> (canceling "insertFromDrop") + await (async function test_dragging_from_input_element_to_textarea_element_and_canceling_insert_from_drop() { + const description = 'dragging text in <input> to other <textarea> (canceling "insertFromDrop")'; + container.innerHTML = '<input value="Some Text"><textarea></textarea>'; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const input = document.querySelector("div#container > input"); + const textarea = document.querySelector("div#container > textarea"); + input.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultInsertFromDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(input).editor.selection, + destElement: textarea, + } + ) + ) { + is(input.value, "Somt", + `${description}: dragged range should be removed from <input>`); + is(textarea.value, "", + `${description}: dragged content shouldn't be inserted into <textarea>`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on <input> and <textarea>`); + checkInputEvent(beforeinputEvents[0], input, "deleteByDrag", null, null, [], description); + checkInputEvent(beforeinputEvents[1], textarea, "insertFromDrop", "e Tex", null, [], description); + is(inputEvents.length, 1, + `${description}: only one "input" event should be fired on <input>`); + checkInputEvent(inputEvents[0], input, "deleteByDrag", null, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <textarea>`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultInsertFromDrop); + })(); + + // -------- Test copy-dragging text in <input> to <textarea> + await (async function test_copy_dragging_from_input_element_to_textarea_element() { + const description = "copy-dragging text in <input> to <textarea>"; + container.innerHTML = '<input value="Some Text"><textarea></textarea>'; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const input = document.querySelector("div#container > input"); + const textarea = document.querySelector("div#container > textarea"); + input.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), input.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(input).editor.selection, + destElement: textarea, + dragEvent: kModifiersToCopy, + } + ) + ) { + is(input.value, "Some Text", + `${description}: dragged range shouldn't be removed from <input>`); + is(textarea.value, "e Tex", + `${description}: dragged content should be inserted into <textarea>`); + is(beforeinputEvents.length, 1, + `${description}: only one "beforeinput" events should be fired on <textarea>`); + checkInputEvent(beforeinputEvents[0], textarea, "insertFromDrop", "e Tex", null, [], description); + is(inputEvents.length, 1, + `${description}: only one "input" events should be fired on <textarea>`); + checkInputEvent(inputEvents[0], textarea, "insertFromDrop", "e Tex", null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <textarea>`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging text in <textarea> to <input> + await (async function test_dragging_from_textarea_element_to_input_element() { + const description = "dragging text in <textarea> to <input>"; + container.innerHTML = "<textarea>Line1\nLine2</textarea><input>"; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const textarea = document.querySelector("div#container > textarea"); + const input = document.querySelector("div#container > input"); + textarea.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), textarea.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(textarea).editor.selection, + destElement: input, + } + ) + ) { + is(textarea.value, "Linne2", + `${description}: dragged range should be removed from <textarea>`); + is(input.value, "e1 Li", + `${description}: dragged content should be inserted into <input>`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on <textarea> and <input>`); + checkInputEvent(beforeinputEvents[0], textarea, "deleteByDrag", null, null, [], description); + checkInputEvent(beforeinputEvents[1], input, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(inputEvents.length, 2, + `${description}: 2 "input" events should be fired on <textarea> and <input>`); + checkInputEvent(inputEvents[0], textarea, "deleteByDrag", null, null, [], description); + checkInputEvent(inputEvents[1], input, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <textarea>`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging text in <textarea> to <input> (canceling "deleteByDrag") + await (async function test_dragging_from_textarea_element_to_input_element_and_delete_by_drag() { + const description = 'dragging text in <textarea> to <input> (canceling "deleteByDrag")'; + container.innerHTML = "<textarea>Line1\nLine2</textarea><input>"; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const textarea = document.querySelector("div#container > textarea"); + const input = document.querySelector("div#container > input"); + textarea.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), textarea.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultDeleteByDrag); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(textarea).editor.selection, + destElement: input, + } + ) + ) { + is(textarea.value, "Line1\nLine2", + `${description}: dragged range shouldn't be removed from <textarea>`); + is(input.value, "e1 Li", + `${description}: dragged content should be inserted into <input>`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on <textarea> and <input>`); + checkInputEvent(beforeinputEvents[0], textarea, "deleteByDrag", null, null, [], description); + checkInputEvent(beforeinputEvents[1], input, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(inputEvents.length, 1, + `${description}: only one "input" event should be fired on <input>`); + checkInputEvent(inputEvents[0], input, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <textarea>`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultDeleteByDrag); + })(); + + // -------- Test dragging text in <textarea> to <input> (canceling "insertFromDrop") + await (async function test_dragging_from_textarea_element_to_input_element_and_canceling_insert_from_drop() { + const description = 'dragging text in <textarea> to <input> (canceling "insertFromDrop")'; + container.innerHTML = "<textarea>Line1\nLine2</textarea><input>"; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const textarea = document.querySelector("div#container > textarea"); + const input = document.querySelector("div#container > input"); + textarea.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), textarea.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultInsertFromDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(textarea).editor.selection, + destElement: input, + } + ) + ) { + is(textarea.value, "Linne2", + `${description}: dragged range should be removed from <textarea>`); + is(input.value, "", + `${description}: dragged content shouldn't be inserted into <input>`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on <textarea> and <input>`); + checkInputEvent(beforeinputEvents[0], textarea, "deleteByDrag", null, null, [], description); + checkInputEvent(beforeinputEvents[1], input, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(inputEvents.length, 1, + `${description}: only one "input" event should be fired on <textarea>`); + checkInputEvent(inputEvents[0], textarea, "deleteByDrag", null, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <textarea>`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultInsertFromDrop); + })(); + + // -------- Test copy-dragging text in <textarea> to <input> + await (async function test_copy_dragging_from_textarea_element_to_input_element() { + const description = "copy-dragging text in <textarea> to <input>"; + container.innerHTML = "<textarea>Line1\nLine2</textarea><input>"; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const textarea = document.querySelector("div#container > textarea"); + const input = document.querySelector("div#container > input"); + textarea.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), textarea.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(textarea).editor.selection, + destElement: input, + dragEvent: kModifiersToCopy, + } + ) + ) { + is(textarea.value, "Line1\nLine2", + `${description}: dragged range shouldn't be removed from <textarea>`); + is(input.value, "e1 Li", + `${description}: dragged content should be inserted into <input>`); + is(beforeinputEvents.length, 1, + `${description}: only one "beforeinput" events should be fired on <input>`); + checkInputEvent(beforeinputEvents[0], input, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(inputEvents.length, 1, + `${description}: only one "input" events should be fired on <input>`); + checkInputEvent(inputEvents[0], input, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <textarea>`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging text in <textarea> to other <textarea> + await (async function test_dragging_from_textarea_element_to_other_textarea_element() { + const description = "dragging text in <textarea> to other <textarea>"; + container.innerHTML = "<textarea>Line1\nLine2</textarea><textarea></textarea>"; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const textarea = document.querySelector("div#container > textarea"); + const otherTextarea = document.querySelector("div#container > textarea + textarea"); + textarea.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), textarea.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(textarea).editor.selection, + destElement: otherTextarea, + } + ) + ) { + is(textarea.value, "Linne2", + `${description}: dragged range should be removed from <textarea>`); + is(otherTextarea.value, "e1\nLi", + `${description}: dragged content should be inserted into other <textarea>`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on <textarea> and other <textarea>`); + checkInputEvent(beforeinputEvents[0], textarea, "deleteByDrag", null, null, [], description); + checkInputEvent(beforeinputEvents[1], otherTextarea, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(inputEvents.length, 2, + `${description}: 2 "input" events should be fired on <textarea> and other <textarea>`); + checkInputEvent(inputEvents[0], textarea, "deleteByDrag", null, null, [], description); + checkInputEvent(inputEvents[1], otherTextarea, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <textarea>`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging text in <textarea> to other <textarea> (canceling "deleteByDrag") + await (async function test_dragging_from_textarea_element_to_other_textarea_element_and_canceling_delete_by_drag() { + const description = 'dragging text in <textarea> to other <textarea> (canceling "deleteByDrag")'; + container.innerHTML = "<textarea>Line1\nLine2</textarea><textarea></textarea>"; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const textarea = document.querySelector("div#container > textarea"); + const otherTextarea = document.querySelector("div#container > textarea + textarea"); + textarea.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), textarea.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultDeleteByDrag); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(textarea).editor.selection, + destElement: otherTextarea, + } + ) + ) { + is(textarea.value, "Line1\nLine2", + `${description}: dragged range shouldn't be removed from <textarea>`); + is(otherTextarea.value, "e1\nLi", + `${description}: dragged content should be inserted into other <textarea>`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on <textarea> and other <textarea>`); + checkInputEvent(beforeinputEvents[0], textarea, "deleteByDrag", null, null, [], description); + checkInputEvent(beforeinputEvents[1], otherTextarea, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(inputEvents.length, 1, + `${description}: only one "input" event should be fired on other <textarea>`); + checkInputEvent(inputEvents[0], otherTextarea, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <textarea>`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultDeleteByDrag); + })(); + + // -------- Test dragging text in <textarea> to other <textarea> (canceling "insertFromDrop") + await (async function test_dragging_from_textarea_element_to_other_textarea_element_and_canceling_insert_from_drop() { + const description = 'dragging text in <textarea> to other <textarea> (canceling "insertFromDrop")'; + container.innerHTML = "<textarea>Line1\nLine2</textarea><textarea></textarea>"; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const textarea = document.querySelector("div#container > textarea"); + const otherTextarea = document.querySelector("div#container > textarea + textarea"); + textarea.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), textarea.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + document.addEventListener("beforeinput", preventDefaultInsertFromDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(textarea).editor.selection, + destElement: otherTextarea, + } + ) + ) { + is(textarea.value, "Linne2", + `${description}: dragged range should be removed from <textarea>`); + is(otherTextarea.value, "", + `${description}: dragged content shouldn't be inserted into other <textarea>`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on <textarea> and other <textarea>`); + checkInputEvent(beforeinputEvents[0], textarea, "deleteByDrag", null, null, [], description); + checkInputEvent(beforeinputEvents[1], otherTextarea, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(inputEvents.length, 1, + `${description}: only one "input" event should be fired on <textarea>`); + checkInputEvent(inputEvents[0], textarea, "deleteByDrag", null, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <textarea>`); + } + document.removeEventListener("drop", onDrop); + document.removeEventListener("beforeinput", preventDefaultInsertFromDrop); + })(); + + // -------- Test copy-dragging text in <textarea> to other <textarea> + await (async function test_copy_dragging_from_textarea_element_to_other_textarea_element() { + const description = "copy-dragging text in <textarea> to other <textarea>"; + container.innerHTML = "<textarea>Line1\nLine2</textarea><textarea></textarea>"; + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + const textarea = document.querySelector("div#container > textarea"); + const otherTextarea = document.querySelector("div#container > textarea + textarea"); + textarea.setSelectionRange(3, 8); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), textarea.value.substring(3, 8), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should have not have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(textarea).editor.selection, + destElement: otherTextarea, + dragEvent: kModifiersToCopy, + } + ) + ) { + is(textarea.value, "Line1\nLine2", + `${description}: dragged range shouldn't be removed from <textarea>`); + is(otherTextarea.value, "e1\nLi", + `${description}: dragged content should be inserted into other <textarea>`); + is(beforeinputEvents.length, 1, + `${description}: only one "beforeinput" events should be fired on other <textarea>`); + checkInputEvent(beforeinputEvents[0], otherTextarea, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(inputEvents.length, 1, + `${description}: only one "input" events should be fired on other <textarea>`); + checkInputEvent(inputEvents[0], otherTextarea, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on <textarea>`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging multiple-line text in contenteditable to <input> + await (async function test_dragging_multiple_line_text_in_contenteditable_to_input_element() { + const description = "dragging multiple-line text in contenteditable to <input>"; + container.innerHTML = '<div contenteditable><div>Line1</div><div>Line2</div></div><input>'; + const contenteditable = document.querySelector("div#container > div"); + const input = document.querySelector("div#container > input"); + const selectionContainers = [contenteditable.firstChild.firstChild, contenteditable.firstChild.nextSibling.firstChild]; + selection.setBaseAndExtent(selectionContainers[0], 3, selectionContainers[1], 2); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), `e1\nLi`, + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "<div>e1</div><div>Li</div>", + `${description}: dataTransfer should have have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: input, + } + ) + ) { + is(contenteditable.innerHTML, "<div>Linne2</div>", + `${description}: dragged content should be removed from contenteditable`); + is(input.value, "e1 Li", + `${description}: dragged range should be inserted into <input>`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on <input> and contenteditable`); + checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, + [{startContainer: selectionContainers[0], startOffset: 3, + endContainer: selectionContainers[1], endOffset: 2}], + description); + checkInputEvent(beforeinputEvents[1], input, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(inputEvents.length, 2, + `${description}: 2 "input" events should be fired on <input> and contenteditable`); + checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, [], description); + checkInputEvent(inputEvents[1], input, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on other contenteditable`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test copy-dragging multiple-line text in contenteditable to <input> + await (async function test_copy_dragging_multiple_line_text_in_contenteditable_to_input_element() { + const description = "copy-dragging multiple-line text in contenteditable to <input>"; + container.innerHTML = '<div contenteditable><div>Line1</div><div>Line2</div></div><input>'; + const contenteditable = document.querySelector("div#container > div"); + const input = document.querySelector("div#container > input"); + selection.setBaseAndExtent(contenteditable.firstChild.firstChild, 3, + contenteditable.firstChild.nextSibling.firstChild, 2); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), `e1\nLi`, + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "<div>e1</div><div>Li</div>", + `${description}: dataTransfer should have have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: input, + dragEvent: kModifiersToCopy, + } + ) + ) { + is(contenteditable.innerHTML, "<div>Line1</div><div>Line2</div>", + `${description}: dragged content should be removed from contenteditable`); + is(input.value, "e1 Li", + `${description}: dragged range should be inserted into <input>`); + is(beforeinputEvents.length, 1, + `${description}: only one "beforeinput" events should be fired on contenteditable`); + checkInputEvent(beforeinputEvents[0], input, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(inputEvents.length, 1, + `${description}: only one "input" events should be fired on contenteditable`); + checkInputEvent(inputEvents[0], input, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on other contenteditable`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging multiple-line text in contenteditable to <textarea> + await (async function test_dragging_multiple_line_text_in_contenteditable_to_textarea_element() { + const description = "dragging multiple-line text in contenteditable to <textarea>"; + container.innerHTML = '<div contenteditable><div>Line1</div><div>Line2</div></div><textarea></textarea>'; + const contenteditable = document.querySelector("div#container > div"); + const textarea = document.querySelector("div#container > textarea"); + const selectionContainers = [contenteditable.firstChild.firstChild, contenteditable.firstChild.nextSibling.firstChild]; + selection.setBaseAndExtent(selectionContainers[0], 3, selectionContainers[1], 2); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), `e1\nLi`, + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "<div>e1</div><div>Li</div>", + `${description}: dataTransfer should have have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: textarea, + } + ) + ) { + is(contenteditable.innerHTML, "<div>Linne2</div>", + `${description}: dragged content should be removed from contenteditable`); + is(textarea.value, "e1\nLi", + `${description}: dragged range should be inserted into <textarea>`); + is(beforeinputEvents.length, 2, + `${description}: 2 "beforeinput" events should be fired on <textarea> and contenteditable`); + checkInputEvent(beforeinputEvents[0], contenteditable, "deleteByDrag", null, null, + [{startContainer: selectionContainers[0], startOffset: 3, + endContainer: selectionContainers[1], endOffset: 2}], + description); + checkInputEvent(beforeinputEvents[1], textarea, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(inputEvents.length, 2, + `${description}: 2 "input" events should be fired on <textarea> and contenteditable`); + checkInputEvent(inputEvents[0], contenteditable, "deleteByDrag", null, null, [], description); + checkInputEvent(inputEvents[1], textarea, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on other contenteditable`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test copy-dragging multiple-line text in contenteditable to <textarea> + await (async function test_copy_dragging_multiple_line_text_in_contenteditable_to_textarea_element() { + const description = "copy-dragging multiple-line text in contenteditable to <textarea>"; + container.innerHTML = '<div contenteditable><div>Line1</div><div>Line2</div></div><textarea></textarea>'; + const contenteditable = document.querySelector("div#container > div"); + const textarea = document.querySelector("div#container > textarea"); + selection.setBaseAndExtent(contenteditable.firstChild.firstChild, 3, + contenteditable.firstChild.nextSibling.firstChild, 2); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), `e1\nLi`, + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "<div>e1</div><div>Li</div>", + `${description}: dataTransfer should have have selected nodes as "text/html"`); + }; + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: textarea, + dragEvent: kModifiersToCopy, + } + ) + ) { + is(contenteditable.innerHTML, "<div>Line1</div><div>Line2</div>", + `${description}: dragged content should be removed from contenteditable`); + is(textarea.value, "e1\nLi", + `${description}: dragged range should be inserted into <textarea>`); + is(beforeinputEvents.length, 1, + `${description}: only one "beforeinput" events should be fired on contenteditable`); + checkInputEvent(beforeinputEvents[0], textarea, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(inputEvents.length, 1, + `${description}: only one "input" events should be fired on contenteditable`); + checkInputEvent(inputEvents[0], textarea, "insertFromDrop", `e1${kNativeLF}Li`, null, [], description); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired on other contenteditable`); + } + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging text from an <input> and reframing the <input> element before dragend. + await (async function test_dragging_from_input_element_and_reframing_input_element() { + const description = "dragging part of text in <input> element and reframing the <input> element before dragend"; + container.innerHTML = '<input value="Drag Me">'; + const input = document.querySelector("div#container > input"); + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + input.setSelectionRange(1, 4); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDragStart = aEvent => { + input.style.display = "none"; + document.documentElement.scrollTop; + input.style.display = ""; + document.documentElement.scrollTop; + }; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), + input.value.substring(1, 4), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should not have data as "text/html"`); + }; + document.addEventListener("dragStart", onDragStart); + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(input).editor.selection, + destElement: dropZone, + } + ) + ) { + is(beforeinputEvents.length, 0, + `${description}: No "beforeinput" event should be fired when dragging <input> value to non-editable drop zone`); + is(inputEvents.length, 0, + `${description}: No "input" event should be fired when dragging <input> value to non-editable drop zone`); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired`); + } + document.removeEventListener("dragStart", onDragStart); + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging text from an <textarea> and reframing the <textarea> element before dragend. + await (async function test_dragging_from_textarea_element_and_reframing_textarea_element() { + const description = "dragging part of text in <textarea> element and reframing the <textarea> element before dragend"; + container.innerHTML = "<textarea>Some Text To Drag</textarea>"; + const textarea = document.querySelector("div#container > textarea"); + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + textarea.setSelectionRange(1, 7); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDragStart = aEvent => { + textarea.style.display = "none"; + document.documentElement.scrollTop; + textarea.style.display = ""; + document.documentElement.scrollTop; + }; + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), + textarea.value.substring(1, 7), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should not have data as "text/html"`); + }; + document.addEventListener("dragStart", onDragStart); + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(textarea).editor.selection, + destElement: dropZone, + } + ) + ) { + is(beforeinputEvents.length, 0, + `${description}: No "beforeinput" event should be fired when dragging <textarea> value to non-editable drop zone`); + is(inputEvents.length, 0, + `${description}: No "input" event should be fired when dragging <textarea> value to non-editable drop zone`); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired`); + } + document.removeEventListener("dragStart", onDragStart); + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging text from an <input> and reframing the <input> element before dragstart. + await (async function test_dragging_from_input_element_and_reframing_input_element_before_dragstart() { + const description = "dragging part of text in <input> element and reframing the <input> element before dragstart"; + container.innerHTML = '<input value="Drag Me">'; + const input = document.querySelector("div#container > input"); + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + input.setSelectionRange(1, 4); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onMouseMove = aEvent => { + input.style.display = "none"; + document.documentElement.scrollTop; + input.style.display = ""; + document.documentElement.scrollTop; + }; + const onMouseDown = aEvent => { + document.addEventListener("mousemove", onMouseMove, {once: true}); + } + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), + input.value.substring(1, 4), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should not have data as "text/html"`); + }; + document.addEventListener("mousedown", onMouseDown, {once: true}); + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(input).editor.selection, + destElement: dropZone, + } + ) + ) { + is(beforeinputEvents.length, 0, + `${description}: No "beforeinput" event should be fired when dragging <input> value to non-editable drop zone`); + is(inputEvents.length, 0, + `${description}: No "input" event should be fired when dragging <input> value to non-editable drop zone`); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired`); + } + document.removeEventListener("mousedown", onMouseDown); + document.removeEventListener("mousemove", onMouseMove); + document.removeEventListener("drop", onDrop); + })(); + + // -------- Test dragging text from an <textarea> and reframing the <textarea> element before dragstart. + await (async function test_dragging_from_textarea_element_and_reframing_textarea_element_before_dragstart() { + const description = "dragging part of text in <textarea> element and reframing the <textarea> element before dragstart"; + container.innerHTML = "<textarea>Some Text To Drag</textarea>"; + const textarea = document.querySelector("div#container > textarea"); + document.documentElement.scrollTop; // Need reflow to create TextControlState and its colleagues. + textarea.setSelectionRange(1, 7); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onMouseMove = aEvent => { + textarea.style.display = "none"; + document.documentElement.scrollTop; + textarea.style.display = ""; + document.documentElement.scrollTop; + }; + const onMouseDown = aEvent => { + document.addEventListener("mousemove", onMouseMove, {once: true}); + } + const onDrop = aEvent => { + dragEvents.push(aEvent); + comparePlainText(aEvent.dataTransfer.getData("text/plain"), + textarea.value.substring(1, 7), + `${description}: dataTransfer should have selected text as "text/plain"`); + is(aEvent.dataTransfer.getData("text/html"), "", + `${description}: dataTransfer should not have data as "text/html"`); + }; + document.addEventListener("mousedown", onMouseDown, {once: true}); + document.addEventListener("drop", onDrop); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: SpecialPowers.wrap(textarea).editor.selection, + destElement: dropZone, + } + ) + ) { + is(beforeinputEvents.length, 0, + `${description}: No "beforeinput" event should be fired when dragging <textarea> value to non-editable drop zone`); + is(inputEvents.length, 0, + `${description}: No "input" event should be fired when dragging <textarea> value to non-editable drop zone`); + is(dragEvents.length, 1, + `${description}: only one "drop" event should be fired`); + } + document.removeEventListener("mousedown", onMouseDown); + document.removeEventListener("mousemove", onMouseMove); + document.removeEventListener("drop", onDrop); + })(); + + await (async function test_dragend_when_left_half_of_text_node_dragged_into_textarea() { + const description = "dragging left half of text in contenteditable into <textarea>"; + container.innerHTML = "<div contenteditable><p>abcdef</p></div><textarea></textarea>"; + const editingHost = container.querySelector("[contenteditable]"); + const textNode = editingHost.querySelector("p").firstChild; + const textarea = container.querySelector("textarea"); + selection.setBaseAndExtent(textNode, 0, textNode, textNode.length / 2); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDragEnd = aEvent => dragEvents.push(aEvent); + document.addEventListener("dragend", onDragEnd, {capture: true}); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: textarea, + } + ) + ) { + ok( + textNode.isConnected, + `${description}: the text node part of whose text is dragged should not be removed` + ); + is( + dragEvents.length, + 1, + `${description}: only one "dragend" event should be fired` + ); + is( + dragEvents[0]?.target, + textNode, + `${description}: "dragend" should be fired on the text node which the mouse button down on` + ); + } + document.removeEventListener("dragend", onDragEnd, {capture: true}); + })(); + + await (async function test_dragend_when_right_half_of_text_node_dragged_into_textarea() { + const description = "dragging right half of text in contenteditable into <textarea>"; + container.innerHTML = "<div contenteditable><p>abcdef</p></div><textarea></textarea>"; + const editingHost = container.querySelector("[contenteditable]"); + const textNode = editingHost.querySelector("p").firstChild; + const textarea = container.querySelector("textarea"); + selection.setBaseAndExtent(textNode, textNode.length / 2, textNode, textNode.length); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDragEnd = aEvent => dragEvents.push(aEvent); + document.addEventListener("dragend", onDragEnd, {capture: true}); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: textarea, + } + ) + ) { + ok( + textNode.isConnected, + `${description}: the text node part of whose text is dragged should not be removed` + ); + is( + dragEvents.length, + 1, + `${description}: only one "dragend" event should be fired` + ); + is( + dragEvents[0]?.target, + textNode, + `${description}: "dragend" should be fired on the text node which the mouse button down on` + ); + } + document.removeEventListener("dragend", onDragEnd, {capture: true}); + })(); + + await (async function test_dragend_when_middle_part_of_text_node_dragged_into_textarea() { + const description = "dragging middle of text in contenteditable into <textarea>"; + container.innerHTML = "<div contenteditable><p>abcdef</p></div><textarea></textarea>"; + const editingHost = container.querySelector("[contenteditable]"); + const textNode = editingHost.querySelector("p").firstChild; + const textarea = container.querySelector("textarea"); + selection.setBaseAndExtent(textNode, "ab".length, textNode, "abcd".length); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDragEnd = aEvent => dragEvents.push(aEvent); + document.addEventListener("dragend", onDragEnd, {capture: true}); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: textarea, + } + ) + ) { + ok( + textNode.isConnected, + `${description}: the text node part of whose text is dragged should not be removed` + ); + is( + dragEvents.length, + 1, + `${description}: only one "dragend" event should be fired` + ); + is( + dragEvents[0]?.target, + textNode, + `${description}: "dragend" should be fired on the text node which the mouse button down on` + ); + } + document.removeEventListener("dragend", onDragEnd, {capture: true}); + })(); + + await (async function test_dragend_when_all_of_text_node_dragged_into_textarea() { + const description = "dragging all of text in contenteditable into <textarea>"; + container.innerHTML = "<div contenteditable><p>abcdef</p></div><textarea></textarea>"; + const editingHost = container.querySelector("[contenteditable]"); + const textNode = editingHost.querySelector("p").firstChild; + const textarea = container.querySelector("textarea"); + selection.setBaseAndExtent(textNode, 0, textNode, textNode.length); + beforeinputEvents = []; + inputEvents = []; + dragEvents = []; + const onDragEnd = aEvent => dragEvents.push(aEvent); + document.addEventListener("dragend", onDragEnd, {capture: true}); + if ( + await trySynthesizePlainDragAndDrop( + description, + { + srcSelection: selection, + destElement: textarea, + } + ) + ) { + ok( + !textNode.isConnected, + `${description}: the text node whose all text is dragged should've been removed from the contenteditable` + ); + is( + dragEvents.length, + 1, + `${description}: only one "dragend" event should be fired` + ); + is( + dragEvents[0]?.target, + editingHost, + `${description}: "dragend" should be fired on the editing host which is parent of the removed text node` + ); + } + document.removeEventListener("dragend", onDragEnd, {capture: true}); + })(); + + document.removeEventListener("beforeinput", onBeforeinput); + document.removeEventListener("input", onInput); + SimpleTest.finish(); +} + +SimpleTest.waitForFocus(doTest); + +</script> +</body> +</html> diff --git a/editor/libeditor/tests/test_execCommandPaste_noTarget.html b/editor/libeditor/tests/test_execCommandPaste_noTarget.html new file mode 100644 index 0000000000..6586ca768d --- /dev/null +++ b/editor/libeditor/tests/test_execCommandPaste_noTarget.html @@ -0,0 +1,45 @@ +<!DOCTYPE HTML> +<html> +<head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<script> + + add_task(async function() { + let seenPaste = false; + let seenCopy = false; + document.addEventListener("copy", function oncpy(e) { + e.clipboardData.setData("text/plain", "my text"); + e.preventDefault(); + seenCopy = true; + }, {once: true}); + document.addEventListener("paste", function onpst(e) { + is(e.clipboardData.getData("text/plain"), "my text", + "The correct text was read from the clipboard"); + e.preventDefault(); + seenPaste = true; + }, {once: true}); + + ok(SpecialPowers.wrap(document).execCommand("copy"), + "Call should succeed"); + ok(seenCopy, "Successfully copied the text to the clipboard"); + ok(SpecialPowers.wrap(document).execCommand("paste"), + "Call should succeed"); + ok(seenPaste, "Successfully read text from the clipboard"); + + // Check that reading text from the clipboard in non-privileged contexts + // still doesn't work. + function onpstfail(e) { + ok(false, "Should not see paste event triggered by non-privileged call"); + } + document.addEventListener("paste", onpstfail); + ok(!document.execCommand("paste"), "Call should fail"); + document.removeEventListener("paste", onpstfail); + }); + +</script> +</body> +</html> diff --git a/editor/libeditor/tests/test_focus_caret_navigation_between_nested_editors.html b/editor/libeditor/tests/test_focus_caret_navigation_between_nested_editors.html new file mode 100644 index 0000000000..c0ff01ae10 --- /dev/null +++ b/editor/libeditor/tests/test_focus_caret_navigation_between_nested_editors.html @@ -0,0 +1,207 @@ +<!DOCTYPE> +<html> +<head> +<title>Test for bug1318312</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +<style type="text/css"> +</style> +</head> +<body> +<div id="outerEditor" contenteditable><p>editable in outer editor</p> +<div id="staticInEditor" contenteditable="false"><p>non-editable in outer editor</p> +<div id="innerEditor" contenteditable><p>editable in inner editor</p></div></div></div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var outerEditor = document.getElementById("outerEditor"); +var innerEditor = document.getElementById("innerEditor"); + +function getNodeDescription(node) { + if (!node) { + return "null"; + } + switch (node.nodeType) { + case Node.TEXT_NODE: + case Node.COMMENT_NODE: + case Node.CDATA_SECTION_NODE: + return `${node.nodeName} "${node.data}"`; + case Node.ELEMENT_NODE: + return `<${node.nodeName.toLowerCase()}>`; + default: + return `${node.nodeName}`; + } +} + +function getRangeDescription(range) { + if (range === null) { + return "null"; + } + if (range === undefined) { + return "undefined"; + } + return range.startContainer == range.endContainer && + range.startOffset == range.endOffset + ? `(${getNodeDescription(range.startContainer)}, ${range.startOffset})` + : `(${getNodeDescription(range.startContainer)}, ${ + range.startOffset + }) - (${getNodeDescription(range.endContainer)}, ${range.endOffset})`; +} + +function runTests() { + outerEditor.focus(); + is(document.activeElement, outerEditor, + "outerEditor should have focus"); + + // Move cursor into the innerEditor with ArrowDown key. Then, focus shouldn't + // be moved to innerEditor from outerEditor. + // Note that Chrome moves focus in this case. However, we should align the + // behavior to Chrome for now because we don't support move caret from a + // selection limiter to outside of it. + (() => { + const description = "Press ArrowDown from start of the outer editor"; + synthesizeKey("KEY_ArrowDown"); + is( + document.activeElement, + outerEditor, + `${description}: The outer editor should keep having focus` + ); + is( + getRangeDescription(getSelection().getRangeAt(0)), + '(#text "editable in inner editor", 0)', + `${description}: Caret should be moved to start of the inner editor` + ); + })(); + + (() => { + const description = 'Typing "a" at start of the inner editor'; + sendString("a"); + is( + document.activeElement, + outerEditor, + `${description}: The outer editor should keep having focus` + ); + is( + getRangeDescription(getSelection().getRangeAt(0)), + '(#text "aeditable in inner editor", 1)', + `${description}: Caret should be moved to after typed character, "a"` + ); + })(); + + (() => { + const description = 'Pressing Enter next to the typed character "a"'; + synthesizeKey("KEY_Enter"); + is( + document.activeElement, + outerEditor, + `${description}: The outer editor should keep having focus` + ); + is( + innerEditor.innerHTML, + "<p>a</p><p>editable in inner editor</p>", + `${description}: The paragraph in the inner editor should be split after "a"` + ); + is( + getRangeDescription(getSelection().getRangeAt(0)), + '(#text "editable in inner editor", 0)', + `${description}: Caret should be moved to start of the second paragraph` + ); + })(); + + (() => { + const description = 'Pressing Backspace at start of the second paragraph'; + synthesizeKey("KEY_Backspace"); + is( + document.activeElement, + outerEditor, + `${description}: The outer editor should keep having focus` + ); + is( + innerEditor.innerHTML, + "<p>aeditable in inner editor</p>", + `${description}: The paragraphs should be joined` + ); + is( + getRangeDescription(getSelection().getRangeAt(0)), + '(#text "aeditable in inner editor", 1)', + `${description}: Caret should be moved to end of text which was in the first paragraph` + ); + })(); + + (() => { + const description = 'Pressing Shift-ArrowLeft from next to the first character, "a"'; + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}); + is( + document.activeElement, + outerEditor, + `${description}: The outer editor should keep having focus` + ); + is( + getRangeDescription(getSelection().getRangeAt(0)), + '(#text "aeditable in inner editor", 0) - (#text "aeditable in inner editor", 1)', + `${description}: The first character, "a", should be selected` + ); + })(); + + (() => { + const description = 'Pressing Delete to delete selected "a"'; + synthesizeKey("KEY_Delete"); + is( + document.activeElement, + outerEditor, + `${description}: The outer editor should keep having focus` + ); + is( + innerEditor.innerHTML, + "<p>editable in inner editor</p>", + `${description}: The selected "a" should be deleted` + ); + is( + getRangeDescription(getSelection().getRangeAt(0)), + '(#text "editable in inner editor", 0)', + `${description}: Selection should be collapsed at start of the inner editor` + ); + })(); + + (() => { + const description = 'Pressing ArrowUp from start of the inner editor'; + synthesizeKey("KEY_ArrowUp"); + is( + document.activeElement, + outerEditor, + `${description}: The outer editor should keep having focus` + ); + is( + getRangeDescription(getSelection().getRangeAt(0)), + '(#text "editable in outer editor", 0)', + `${description}: Caret should be moved to start of the outer editor` + ); + })(); + + // However, clicking in innerEditor should move focus. + (() => { + const description = 'Clicking the inner editor when caret is in the outer editor'; + synthesizeMouseAtCenter(innerEditor, {}); + is( + document.activeElement, + innerEditor, + `${description}: The inner editor should get focus` + ); + is( + getNodeDescription(getSelection().focusNode), + '#text "editable in inner editor"', + `${description}: Caret should move into the inner editor` + ); + })(); + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_focused_document_element_becoming_editable.html b/editor/libeditor/tests/test_focused_document_element_becoming_editable.html new file mode 100644 index 0000000000..98ddf54f2b --- /dev/null +++ b/editor/libeditor/tests/test_focused_document_element_becoming_editable.html @@ -0,0 +1,157 @@ +<!doctype html> +<html> +<head> +<meta chareset="utf-8"> +<title>Testing non-editable root becomes editable after getting focus</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> +<script> +SimpleTest.waitForExplicitFinish(); +addEventListener("load", async () => { + await SimpleTest.promiseFocus(window); + + await (async () => { + const iframe = document.createElement("iframe"); + document.body.appendChild(iframe); + await new Promise(resolve => { + iframe.addEventListener("load", async () => { + const doc = iframe.contentDocument; + const win = iframe.contentWindow; + win.focus(); + doc.documentElement.focus(); + doc.designMode = "on"; + await new Promise(r => win.requestAnimationFrame(() => win.requestAnimationFrame(r))); + is( + SpecialPowers.getDOMWindowUtils(win).IMEStatus, + SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED, + "IME should be enabled in the design mode document" + ); + is( + SpecialPowers.unwrap(SpecialPowers.getDOMWindowUtils(win).nodeObservedByIMEContentObserver), + doc.body, + "The <body> should be observed by IMEContentObserver in design mode" + ); + doc.designMode = "off"; + iframe.remove(); + resolve(); + }, {once: true}); + info("Waiting for load of sub-document for testing design mode"); + iframe.srcdoc = "<!doctype html><html><meta charset=\"utf-8\"></head><body></body></html>"; + }); + })(); + + await (async () => { + const iframe = document.createElement("iframe"); + document.body.appendChild(iframe); + await new Promise(resolve => { + iframe.addEventListener("load", async () => { + const doc = iframe.contentDocument; + const win = iframe.contentWindow; + win.focus() + doc.documentElement.focus(); + doc.documentElement.contentEditable = "true"; + await new Promise(r => win.requestAnimationFrame(() => win.requestAnimationFrame(r))); + is( + SpecialPowers.getDOMWindowUtils(win).IMEStatus, + SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED, + "IME should be enabled when the <html> element whose contenteditable is set to true" + ); + is( + SpecialPowers.unwrap(SpecialPowers.getDOMWindowUtils(win).nodeObservedByIMEContentObserver), + doc.documentElement, + "The <html> should be observed by IMEContentObserver when <html contenteditable=\"true\">" + ); + iframe.remove(); + resolve(); + }, {once: true}); + info("Waiting for load of sub-document for testing <html> element becomes editable"); + iframe.srcdoc = "<!doctype html><html><meta charset=\"utf-8\"></head><body></body></html>"; + }); + })(); + + await (async () => { + const iframe = document.createElement("iframe"); + document.body.appendChild(iframe); + await new Promise(resolve => { + iframe.addEventListener("load", async () => { + const doc = iframe.contentDocument; + const win = iframe.contentWindow; + win.focus(); + doc.body.focus(); + doc.body.contentEditable = "true"; + await new Promise(r => win.requestAnimationFrame(() => win.requestAnimationFrame(r))); + if (doc.activeElement === doc.body && doc.hasFocus()) { + todo_is( + SpecialPowers.getDOMWindowUtils(win).IMEStatus, + SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED, + "IME should be enabled when the <body> element whose contenteditable is set to true and it has focus" + ); + todo_is( + SpecialPowers.unwrap(SpecialPowers.getDOMWindowUtils(win).nodeObservedByIMEContentObserver), + doc.body, + "The <body> should be observed by IMEContentObserver when <body contenteditable=\"true\"> and it has focus" + ); + } else { + is( + SpecialPowers.getDOMWindowUtils(win).IMEStatus, + SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_DISABLED, + "IME should be disabled when the <body> element whose contenteditable is set to true but it does not have focus" + ); + is( + SpecialPowers.unwrap(SpecialPowers.getDOMWindowUtils(win).nodeObservedByIMEContentObserver), + null, + "Nobody should be observed by IMEContentObserver when <body contenteditable=\"true\"> but it does not have focus" + ); + } + iframe.remove(); + resolve(); + }, {once: true}); + info("Waiting for load of sub-document for testing <body> element becomes editable"); + iframe.srcdoc = "<!doctype html><html><meta charset=\"utf-8\"></head><body></body></html>"; + }); + })(); + + await (async () => { + const iframe = document.createElement("iframe"); + document.body.appendChild(iframe); + await new Promise(resolve => { + iframe.addEventListener("load", async () => { + const doc = iframe.contentDocument; + const win = iframe.contentWindow; + win.focus(); + const editingHost = doc.createElement("div"); + doc.documentElement.remove(); + doc.appendChild(editingHost); + editingHost.focus(); + is( + SpecialPowers.unwrap(SpecialPowers.focusManager.focusedElement), + editingHost, + "The <div contenteditable> should have focus because of only child of the Document node" + ); + editingHost.contentEditable = "true"; + await new Promise(r => win.requestAnimationFrame(() => win.requestAnimationFrame(r))); + is( + SpecialPowers.getDOMWindowUtils(win).IMEStatus, + SpecialPowers.Ci.nsIDOMWindowUtils.IME_STATUS_ENABLED, + "IME should be enabled in the root element" + ); + is( + SpecialPowers.unwrap(SpecialPowers.getDOMWindowUtils(win).nodeObservedByIMEContentObserver), + editingHost, + "The <div contenteditable> should be observed by IMEContentObserver" + ); + iframe.srcdoc = ""; + resolve(); + }, {once: true}); + info("Waiting for load of sub-document for testing root <div> element becomes editable"); + iframe.srcdoc = "<!doctype html><html><meta charset=\"utf-8\"></head><body></body></html>"; + }); + })(); + + SimpleTest.finish(); +}, false); +</script> +</body> +</html> diff --git a/editor/libeditor/tests/test_handle_new_lines.html b/editor/libeditor/tests/test_handle_new_lines.html new file mode 100644 index 0000000000..0e5825aaad --- /dev/null +++ b/editor/libeditor/tests/test_handle_new_lines.html @@ -0,0 +1,129 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test for TextEditor::HandleNewLinesInStringForSingleLineEditor()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none;"> + +</div> + +<div id="container"></div> + +<textarea id="toCopyPlaintext" style="display: none;"></textarea> + +<pre id="test"> + +<script class="testbody" type="application/javascript"> +SimpleTest.waitForExplicitFinish(); + +async function copyPlaintext(aText) { + return new Promise(resolve => { + SimpleTest.waitForClipboard( + aText.replace(/\r\n?/g, "\n"), + () => { + let element = document.getElementById("toCopyPlaintext"); + element.style.display = "block"; + element.focus(); + element.value = aText; + synthesizeKey("a", {accelKey: true}); + synthesizeKey("c", {accelKey: true}); + }, + () => { + ok(true, `Succeeded to copy "${aText.replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/ /g, "\u00A0")}" to clipboard`); + let element = document.getElementById("toCopyPlaintext"); + element.style.display = "none"; + resolve(); + }, + () => { + SimpleTest.finish(); + }); + }); +} + +async function doTests() { + // nsIEditor::eNewlinesPasteIntact (0): + // only remove the leading and trailing newlines. + // nsIEditor::eNewlinesPasteToFirst (1) or any other value: + // remove the first newline and all characters following it. + // nsIEditor::eNewlinesReplaceWithSpaces (2, Firefox default): + // replace newlines with spaces. + // nsIEditor::eNewlinesStrip (3): + // remove newlines from the string. + // nsIEditor::eNewlinesReplaceWithCommas (4, Thunderbird default): + // replace newlines with commas. + // nsIEditor::eNewlinesStripSurroundingWhitespace (5): + // collapse newlines and surrounding whitespace characters and + // remove them from the string. + + // value: setting or pasting text. + // expected: array of final values for each above pref value. + // setValue: expected result when HTMLInputElement.value is set to the value. + // pasteValue: expected result when pasting the value from clipboard. + // + // Note that HTMLInputElement strips both \r and \n. Therefore, each expected + // result is different from pasting the value. + const kTests = [ + { value: "\nabc\ndef\n", + expected: [{ setValue: "abcdef", pasteValue: "abc\ndef" }, + { setValue: "abcdef", pasteValue: "abc" }, + { setValue: "abcdef", pasteValue: " abc def" }, + { setValue: "abcdef", pasteValue: "abcdef" }, + { setValue: "abcdef", pasteValue: "abc,def" }, + { setValue: "abcdef", pasteValue: "abcdef" }], + }, + { value: "\n abc \n def \n", + expected: [{ setValue: " abc def ", pasteValue: " abc \n def " }, + { setValue: " abc def ", pasteValue: " abc " }, + { setValue: " abc def ", pasteValue: " abc def " }, + { setValue: " abc def ", pasteValue: " abc def " }, + { setValue: " abc def ", pasteValue: " abc , def " }, + { setValue: " abc def ", pasteValue: "abcdef" }], + }, + { value: " abc \n def ", + expected: [{ setValue: " abc def ", pasteValue: " abc \n def " }, + { setValue: " abc def ", pasteValue: " abc " }, + { setValue: " abc def ", pasteValue: " abc def " }, + { setValue: " abc def ", pasteValue: " abc def " }, + { setValue: " abc def ", pasteValue: " abc , def " }, + { setValue: " abc def ", pasteValue: " abcdef " }], + }, + ]; + + let container = document.getElementById("container"); + for (let i = 0; i <= 5; i++) { + await SpecialPowers.pushPrefEnv({"set": [["editor.singleLine.pasteNewlines", i]]}); + container.innerHTML = `<input id="input${i}" type="text">`; + let input = document.getElementById(`input${i}`); + input.focus(); + let editor = SpecialPowers.wrap(input).editor; + for (const kLineBreaker of ["\n", "\r", "\r\n"]) { + for (let kTest of kTests) { + let value = kTest.value.replace(/\n/g, kLineBreaker); + input.value = value; + is(editor.rootElement.firstChild.wholeText, kTest.expected[i].setValue, + `Setting value to "${value.replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/ /g, "\u00A0")}" when pref is ${i}`); + input.value = ""; + + await copyPlaintext(value); + input.focus(); + synthesizeKey("v", {accelKey: true}); + is(editor.rootElement.firstChild.wholeText, kTest.expected[i].pasteValue, + `Pasting "${value.replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/ /g, "\u00A0")}" when pref is ${i}`); + input.value = ""; + } + } + } + + SimpleTest.finish(); +} + +SimpleTest.waitForFocus(doTests); +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_htmleditor_keyevent_handling.html b/editor/libeditor/tests/test_htmleditor_keyevent_handling.html new file mode 100644 index 0000000000..58666beb35 --- /dev/null +++ b/editor/libeditor/tests/test_htmleditor_keyevent_handling.html @@ -0,0 +1,766 @@ +<html> +<head> + <title>Test for key event handler of HTML editor</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> +</head> +<body> +<div id="display"> + <div id="htmlEditor" contenteditable="true"><br></div> +</div> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + +/* eslint-disable no-nested-ternary */ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests, window); + +var htmlEditor = document.getElementById("htmlEditor"); + +const kIsMac = navigator.platform.includes("Mac"); +const kIsWin = navigator.platform.includes("Win"); +const kIsLinux = navigator.platform.includes("Linux") || navigator.platform.includes("SunOS"); + +async function runTests() { + document.execCommand("stylewithcss", false, "true"); + document.execCommand("defaultParagraphSeparator", false, "div"); + + var fm = SpecialPowers.Services.focus; + + var capturingPhase = { fired: false, prevented: false }; + var bubblingPhase = { fired: false, prevented: false }; + + var listener = { + handleEvent: function _hv(aEvent) { + is(aEvent.type, "keypress", "unexpected event is handled"); + switch (aEvent.eventPhase) { + case aEvent.CAPTURING_PHASE: + capturingPhase.fired = true; + capturingPhase.prevented = aEvent.defaultPrevented; + break; + case aEvent.BUBBLING_PHASE: + bubblingPhase.fired = true; + bubblingPhase.prevented = aEvent.defaultPrevented; + aEvent.preventDefault(); // prevent the browser default behavior + break; + default: + ok(false, "event is handled in unexpected phase"); + } + }, + }; + + function check(aDescription, + aFiredOnCapture, aFiredOnBubbling, aPreventedOnBubbling) { + function getDesciption(aExpected) { + return aDescription + (aExpected ? " wasn't " : " was "); + } + is(capturingPhase.fired, aFiredOnCapture, + getDesciption(aFiredOnCapture) + "fired on capture phase"); + is(bubblingPhase.fired, aFiredOnBubbling, + getDesciption(aFiredOnBubbling) + "fired on bubbling phase"); + + // If the event is fired on bubbling phase and it was already prevented + // on capture phase, it must be prevented on bubbling phase too. + if (capturingPhase.prevented) { + todo(false, aDescription + + " was consumed already, so, we cannot test the editor behavior actually"); + aPreventedOnBubbling = true; + } + + is(bubblingPhase.prevented, aPreventedOnBubbling, + getDesciption(aPreventedOnBubbling) + "prevented on bubbling phase"); + } + + SpecialPowers.addSystemEventListener(window, "keypress", listener, true); + SpecialPowers.addSystemEventListener(window, "keypress", listener, false); + + // eslint-disable-next-line complexity + async function doTest( + aElement, + aDescription, + aIsReadonly, + aIsTabbable, + aIsPlaintext + ) { + function reset(aText) { + capturingPhase.fired = false; + capturingPhase.prevented = false; + bubblingPhase.fired = false; + bubblingPhase.prevented = false; + aElement.innerHTML = aText; + var sel = window.getSelection(); + var range = document.createRange(); + range.setStart(aElement, aElement.childNodes.length); + sel.removeAllRanges(); + sel.addRange(range); + } + + function resetForIndent(aText) { + capturingPhase.fired = false; + capturingPhase.prevented = false; + bubblingPhase.fired = false; + bubblingPhase.prevented = false; + aElement.innerHTML = aText; + var sel = window.getSelection(); + var range = document.createRange(); + var target = document.getElementById("target").firstChild; + range.setStart(target, target.length); + sel.removeAllRanges(); + sel.addRange(range); + } + + if (document.activeElement) { + document.activeElement.blur(); + } + + aDescription += ": "; + + aElement.focus(); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, aDescription + "failed to move focus"); + + // Backspace key: + // If native key bindings map the key combination to something, it's consumed. + // If editor is readonly, it doesn't consume. + // If editor is editable, it consumes backspace and shift+backspace. + // Otherwise, editor doesn't consume the event. + reset(""); + synthesizeKey("KEY_Backspace"); + check(aDescription + "Backspace", true, true, true); + + reset(""); + synthesizeKey("KEY_Backspace", {shiftKey: true}); + check(aDescription + "Shift+Backspace", true, true, true); + + reset(""); + synthesizeKey("KEY_Backspace", {ctrlKey: true}); + check(aDescription + "Ctrl+Backspace", true, true, aIsReadonly || kIsLinux); + + reset(""); + synthesizeKey("KEY_Backspace", {altKey: true}); + check(aDescription + "Alt+Backspace", true, true, aIsReadonly || kIsMac); + + reset(""); + synthesizeKey("KEY_Backspace", {metaKey: true}); + check(aDescription + "Meta+Backspace", true, true, aIsReadonly || kIsMac); + + // Delete key: + // If native key bindings map the key combination to something, it's consumed. + // If editor is readonly, it doesn't consume. + // If editor is editable, delete is consumed. + // Otherwise, editor doesn't consume the event. + reset(""); + synthesizeKey("KEY_Delete"); + check(aDescription + "Delete", true, true, !aIsReadonly || kIsMac || kIsLinux); + + reset(""); + synthesizeKey("KEY_Delete", {shiftKey: true}); + check(aDescription + "Shift+Delete", true, true, kIsMac || kIsLinux); + + reset(""); + synthesizeKey("KEY_Delete", {ctrlKey: true}); + check(aDescription + "Ctrl+Delete", true, true, kIsLinux); + + reset(""); + synthesizeKey("KEY_Delete", {altKey: true}); + check(aDescription + "Alt+Delete", true, true, kIsMac); + + reset(""); + synthesizeKey("KEY_Delete", {metaKey: true}); + check(aDescription + "Meta+Delete", true, true, false); + + // Return key: + // If editor is readonly, it doesn't consume. + // If editor is editable and not single line editor, it consumes Return + // and Shift+Return. + // Otherwise, editor doesn't consume the event. + reset("a"); + synthesizeKey("KEY_Enter"); + check(aDescription + "Return", + true, true, !aIsReadonly); + is(aElement.innerHTML, aIsReadonly ? "a" : "<div>a</div><br>", + aDescription + "Return"); + + reset("a"); + synthesizeKey("KEY_Enter", {shiftKey: true}); + check(aDescription + "Shift+Return", + true, true, !aIsReadonly); + is(aElement.innerHTML, aIsReadonly ? "a" : "a<br><br>", + aDescription + "Shift+Return"); + + reset("a"); + synthesizeKey("KEY_Enter", {ctrlKey: true}); + check(aDescription + "Ctrl+Return", true, true, false); + is(aElement.innerHTML, "a", aDescription + "Ctrl+Return"); + + reset("a"); + synthesizeKey("KEY_Enter", {altKey: true}); + check(aDescription + "Alt+Return", true, true, false); + is(aElement.innerHTML, "a", aDescription + "Alt+Return"); + + reset("a"); + synthesizeKey("KEY_Enter", {metaKey: true}); + check(aDescription + "Meta+Return", true, true, false); + is(aElement.innerHTML, "a", aDescription + "Meta+Return"); + + // Tab key: + // If editor is tabbable, editor doesn't consume all tab key events. + // Otherwise, editor consumes tab key event without any modifier keys. + reset("a"); + synthesizeKey("KEY_Tab"); + check(aDescription + "Tab", + true, true, !aIsTabbable && !aIsReadonly); + is(aElement.innerHTML, + (() => { + if (aIsTabbable || aIsReadonly) { + return "a"; + } + if (aIsPlaintext) { + return "a\t"; + } + return SpecialPowers.getBoolPref("editor.white_space_normalization.blink_compatible") + ? "a " + : "a <br>"; + })(), + aDescription + "Tab"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Tab)"); + + reset("a"); + synthesizeKey("KEY_Tab", {shiftKey: true}); + check(aDescription + "Shift+Tab", true, true, false); + is(aElement.innerHTML, "a", aDescription + "Shift+Tab"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Shift+Tab)"); + + // Ctrl+Tab should be consumed by tabbrowser at keydown, so, keypress + // event should never be fired. + reset("a"); + synthesizeKey("KEY_Tab", {ctrlKey: true}); + check(aDescription + "Ctrl+Tab", false, false, false); + is(aElement.innerHTML, "a", aDescription + "Ctrl+Tab"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Ctrl+Tab)"); + + reset("a"); + synthesizeKey("KEY_Tab", {altKey: true}); + check(aDescription + "Alt+Tab", true, true, false); + is(aElement.innerHTML, "a", aDescription + "Alt+Tab"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Alt+Tab)"); + + reset("a"); + synthesizeKey("KEY_Tab", {metaKey: true}); + check(aDescription + "Meta+Tab", true, true, false); + is(aElement.innerHTML, "a", aDescription + "Meta+Tab"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Meta+Tab)"); + + // Indent/Outdent tests: + // UL + resetForIndent("<ul><li id=\"target\">ul list item</li></ul>"); + synthesizeKey("KEY_Tab"); + check(aDescription + "Tab on UL", + true, true, !aIsTabbable && !aIsReadonly); + is(aElement.innerHTML, + aIsReadonly || aIsTabbable ? + "<ul><li id=\"target\">ul list item</li></ul>" : + aIsPlaintext ? "<ul><li id=\"target\">ul list item\t</li></ul>" : + "<ul><ul><li id=\"target\">ul list item</li></ul></ul>", + aDescription + "Tab on UL"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Tab on UL)"); + synthesizeKey("KEY_Tab", {shiftKey: true}); + check(aDescription + "Shift+Tab after Tab on UL", + true, true, !aIsTabbable && !aIsReadonly && !aIsPlaintext); + is(aElement.innerHTML, + aIsReadonly || aIsTabbable || (!aIsPlaintext) ? + "<ul><li id=\"target\">ul list item</li></ul>" : + "<ul><li id=\"target\">ul list item\t</li></ul>", + aDescription + "Shift+Tab after Tab on UL"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Shift+Tab after Tab on UL)"); + + resetForIndent("<ul><li id=\"target\">ul list item</li></ul>"); + synthesizeKey("KEY_Tab", {shiftKey: true}); + check(aDescription + "Shift+Tab on UL", + true, true, !aIsTabbable && !aIsReadonly && !aIsPlaintext); + is(aElement.innerHTML, + aIsReadonly || aIsTabbable || aIsPlaintext ? + "<ul><li id=\"target\">ul list item</li></ul>" : "ul list item", + aDescription + "Shift+Tab on UL"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Shift+Tab on UL)"); + + // Ctrl+Tab should be consumed by tabbrowser at keydown, so, keypress + // event should never be fired. + resetForIndent("<ul><li id=\"target\">ul list item</li></ul>"); + synthesizeKey("KEY_Tab", {ctrlKey: true}); + check(aDescription + "Ctrl+Tab on UL", false, false, false); + is(aElement.innerHTML, "<ul><li id=\"target\">ul list item</li></ul>", + aDescription + "Ctrl+Tab on UL"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Ctrl+Tab on UL)"); + + resetForIndent("<ul><li id=\"target\">ul list item</li></ul>"); + synthesizeKey("KEY_Tab", {altKey: true}); + check(aDescription + "Alt+Tab on UL", true, true, false); + is(aElement.innerHTML, "<ul><li id=\"target\">ul list item</li></ul>", + aDescription + "Alt+Tab on UL"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Alt+Tab on UL)"); + + resetForIndent("<ul><li id=\"target\">ul list item</li></ul>"); + synthesizeKey("KEY_Tab", {metaKey: true}); + check(aDescription + "Meta+Tab on UL", true, true, false); + is(aElement.innerHTML, "<ul><li id=\"target\">ul list item</li></ul>", + aDescription + "Meta+Tab on UL"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Meta+Tab on UL)"); + + // OL + resetForIndent("<ol><li id=\"target\">ol list item</li></ol>"); + synthesizeKey("KEY_Tab"); + check(aDescription + "Tab on OL", + true, true, !aIsTabbable && !aIsReadonly); + is(aElement.innerHTML, + aIsReadonly || aIsTabbable ? + "<ol><li id=\"target\">ol list item</li></ol>" : + aIsPlaintext ? "<ol><li id=\"target\">ol list item\t</li></ol>" : + "<ol><ol><li id=\"target\">ol list item</li></ol></ol>", + aDescription + "Tab on OL"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Tab on OL)"); + synthesizeKey("KEY_Tab", {shiftKey: true}); + check(aDescription + "Shift+Tab after Tab on OL", + true, true, !aIsTabbable && !aIsReadonly && !aIsPlaintext); + is(aElement.innerHTML, + aIsReadonly || aIsTabbable || (!aIsPlaintext) ? + "<ol><li id=\"target\">ol list item</li></ol>" : + "<ol><li id=\"target\">ol list item\t</li></ol>", + aDescription + "Shift+Tab after Tab on OL"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Shift+Tab after Tab on OL)"); + + resetForIndent("<ol><li id=\"target\">ol list item</li></ol>"); + synthesizeKey("KEY_Tab", {shiftKey: true}); + check(aDescription + "Shift+Tab on OL", + true, true, !aIsTabbable && !aIsReadonly && !aIsPlaintext); + is(aElement.innerHTML, + aIsReadonly || aIsTabbable || aIsPlaintext ? + "<ol><li id=\"target\">ol list item</li></ol>" : "ol list item", + aDescription + "Shfit+Tab on OL"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Shift+Tab on OL)"); + + // Ctrl+Tab should be consumed by tabbrowser at keydown, so, keypress + // event should never be fired. + resetForIndent("<ol><li id=\"target\">ol list item</li></ol>"); + synthesizeKey("KEY_Tab", {ctrlKey: true}); + check(aDescription + "Ctrl+Tab on OL", false, false, false); + is(aElement.innerHTML, "<ol><li id=\"target\">ol list item</li></ol>", + aDescription + "Ctrl+Tab on OL"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Ctrl+Tab on OL)"); + + resetForIndent("<ol><li id=\"target\">ol list item</li></ol>"); + synthesizeKey("KEY_Tab", {altKey: true}); + check(aDescription + "Alt+Tab on OL", true, true, false); + is(aElement.innerHTML, "<ol><li id=\"target\">ol list item</li></ol>", + aDescription + "Alt+Tab on OL"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Alt+Tab on OL)"); + + resetForIndent("<ol><li id=\"target\">ol list item</li></ol>"); + synthesizeKey("KEY_Tab", {metaKey: true}); + check(aDescription + "Meta+Tab on OL", true, true, false); + is(aElement.innerHTML, "<ol><li id=\"target\">ol list item</li></ol>", + aDescription + "Meta+Tab on OL"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Meta+Tab on OL)"); + + // TD + resetForIndent("<table><tr><td id=\"target\">td</td></tr></table>"); + synthesizeKey("KEY_Tab"); + check(aDescription + "Tab on TD", + true, true, !aIsTabbable && !aIsReadonly); + is(aElement.innerHTML, + aIsTabbable || aIsReadonly ? + "<table><tbody><tr><td id=\"target\">td</td></tr></tbody></table>" : + aIsPlaintext ? "<table><tbody><tr><td id=\"target\">td\t</td></tr></tbody></table>" : + "<table><tbody><tr><td id=\"target\">td</td></tr><tr><td style=\"vertical-align: top;\"><br></td></tr></tbody></table>", + aDescription + "Tab on TD"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Tab on TD)"); + synthesizeKey("KEY_Tab", {shiftKey: true}); + check(aDescription + "Shift+Tab after Tab on TD", + true, true, !aIsTabbable && !aIsReadonly && !aIsPlaintext); + is(aElement.innerHTML, + aIsTabbable || aIsReadonly ? + "<table><tbody><tr><td id=\"target\">td</td></tr></tbody></table>" : + aIsPlaintext ? "<table><tbody><tr><td id=\"target\">td\t</td></tr></tbody></table>" : + "<table><tbody><tr><td id=\"target\">td</td></tr><tr><td style=\"vertical-align: top;\"><br></td></tr></tbody></table>", + aDescription + "Shift+Tab after Tab on TD"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Shift+Tab after Tab on TD)"); + + resetForIndent("<table><tr><td id=\"target\">td</td></tr></table>"); + synthesizeKey("KEY_Tab", {shiftKey: true}); + check(aDescription + "Shift+Tab on TD", true, true, false); + is(aElement.innerHTML, + "<table><tbody><tr><td id=\"target\">td</td></tr></tbody></table>", + aDescription + "Shift+Tab on TD"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Shift+Tab on TD)"); + + // Ctrl+Tab should be consumed by tabbrowser at keydown, so, keypress + // event should never be fired. + resetForIndent("<table><tr><td id=\"target\">td</td></tr></table>"); + synthesizeKey("KEY_Tab", {ctrlKey: true}); + check(aDescription + "Ctrl+Tab on TD", false, false, false); + is(aElement.innerHTML, + "<table><tbody><tr><td id=\"target\">td</td></tr></tbody></table>", + aDescription + "Ctrl+Tab on TD"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Ctrl+Tab on TD)"); + + resetForIndent("<table><tr><td id=\"target\">td</td></tr></table>"); + synthesizeKey("KEY_Tab", {altKey: true}); + check(aDescription + "Alt+Tab on TD", true, true, false); + is(aElement.innerHTML, + "<table><tbody><tr><td id=\"target\">td</td></tr></tbody></table>", + aDescription + "Alt+Tab on TD"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Alt+Tab on TD)"); + + resetForIndent("<table><tr><td id=\"target\">td</td></tr></table>"); + synthesizeKey("KEY_Tab", {metaKey: true}); + check(aDescription + "Meta+Tab on TD", true, true, false); + is(aElement.innerHTML, + "<table><tbody><tr><td id=\"target\">td</td></tr></tbody></table>", + aDescription + "Meta+Tab on TD"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Meta+Tab on TD)"); + + // TH + resetForIndent("<table><tr><th id=\"target\">th</th></tr></table>"); + synthesizeKey("KEY_Tab"); + check(aDescription + "Tab on TH", + true, true, !aIsTabbable && !aIsReadonly); + is(aElement.innerHTML, + aIsTabbable || aIsReadonly ? + "<table><tbody><tr><th id=\"target\">th</th></tr></tbody></table>" : + aIsPlaintext ? "<table><tbody><tr><th id=\"target\">th\t</th></tr></tbody></table>" : + "<table><tbody><tr><th id=\"target\">th</th></tr><tr><td style=\"vertical-align: top;\"><br></td></tr></tbody></table>", + aDescription + "Tab on TH"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Tab on TH)"); + synthesizeKey("KEY_Tab", {shiftKey: true}); + check(aDescription + "Shift+Tab after Tab on TH", + true, true, !aIsTabbable && !aIsReadonly && !aIsPlaintext); + is(aElement.innerHTML, + aIsTabbable || aIsReadonly ? + "<table><tbody><tr><th id=\"target\">th</th></tr></tbody></table>" : + aIsPlaintext ? "<table><tbody><tr><th id=\"target\">th\t</th></tr></tbody></table>" : + "<table><tbody><tr><th id=\"target\">th</th></tr><tr><td style=\"vertical-align: top;\"><br></td></tr></tbody></table>", + aDescription + "Shift+Tab after Tab on TH"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Shift+Tab after Tab on TH)"); + + resetForIndent("<table><tr><th id=\"target\">th</th></tr></table>"); + synthesizeKey("KEY_Tab", {shiftKey: true}); + check(aDescription + "Shift+Tab on TH", true, true, false); + is(aElement.innerHTML, + "<table><tbody><tr><th id=\"target\">th</th></tr></tbody></table>", + aDescription + "Shift+Tab on TH"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Shift+Tab on TH)"); + + // Ctrl+Tab should be consumed by tabbrowser at keydown, so, keypress + // event should never be fired. + resetForIndent("<table><tr><th id=\"target\">th</th></tr></table>"); + synthesizeKey("KEY_Tab", {ctrlKey: true}); + check(aDescription + "Ctrl+Tab on TH", false, false, false); + is(aElement.innerHTML, + "<table><tbody><tr><th id=\"target\">th</th></tr></tbody></table>", + aDescription + "Ctrl+Tab on TH"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Ctrl+Tab on TH)"); + + resetForIndent("<table><tr><th id=\"target\">th</th></tr></table>"); + synthesizeKey("KEY_Tab", {altKey: true}); + check(aDescription + "Alt+Tab on TH", true, true, false); + is(aElement.innerHTML, + "<table><tbody><tr><th id=\"target\">th</th></tr></tbody></table>", + aDescription + "Alt+Tab on TH"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Alt+Tab on TH)"); + + resetForIndent("<table><tr><th id=\"target\">th</th></tr></table>"); + synthesizeKey("KEY_Tab", {metaKey: true}); + check(aDescription + "Meta+Tab on TH", true, true, false); + is(aElement.innerHTML, + "<table><tbody><tr><th id=\"target\">th</th></tr></tbody></table>", + aDescription + "Meta+Tab on TH"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Meta+Tab on TH)"); + + // Esc key: + // In all cases, esc key events are not consumed + reset("abc"); + synthesizeKey("KEY_Escape"); + check(aDescription + "Esc", true, true, false); + + reset("abc"); + synthesizeKey("KEY_Escape", {shiftKey: true}); + check(aDescription + "Shift+Esc", true, true, false); + + reset("abc"); + synthesizeKey("KEY_Escape", {ctrlKey: true}); + check(aDescription + "Ctrl+Esc", true, true, false); + + reset("abc"); + synthesizeKey("KEY_Escape", {altKey: true}); + check(aDescription + "Alt+Esc", true, true, false); + + reset("abc"); + synthesizeKey("KEY_Escape", {metaKey: true}); + check(aDescription + "Meta+Esc", true, true, false); + + // typical typing tests: + reset(""); + sendString("M"); + check(aDescription + "M", true, true, !aIsReadonly); + sendString("o"); + check(aDescription + "o", true, true, !aIsReadonly); + sendString("z"); + check(aDescription + "z", true, true, !aIsReadonly); + sendString("i"); + check(aDescription + "i", true, true, !aIsReadonly); + sendString("l"); + check(aDescription + "l", true, true, !aIsReadonly); + sendString("l"); + check(aDescription + "l", true, true, !aIsReadonly); + sendString("a"); + check(aDescription + "a", true, true, !aIsReadonly); + sendString(" "); + check(aDescription + "' '", true, true, !aIsReadonly); + is(aElement.innerHTML, + (() => { + if (aIsReadonly) { + return ""; + } + if (aIsPlaintext) { + return "Mozilla "; + } + return SpecialPowers.getBoolPref("editor.white_space_normalization.blink_compatible") + ? "Mozilla " + : "Mozilla <br>"; + })(), + aDescription + "typed \"Mozilla \""); + + // typing non-BMP character: + async function test_typing_surrogate_pair( + aTestPerSurrogateKeyPress, + aTestIllFormedUTF16KeyValue = false + ) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.event.keypress.dispatch_once_per_surrogate_pair", !aTestPerSurrogateKeyPress], + ["dom.event.keypress.key.allow_lone_surrogate", aTestIllFormedUTF16KeyValue], + ], + }); + reset(""); + let events = []; + function pushIntoEvents(aEvent) { + events.push(aEvent); + } + function getEventData(aKeyboardEventOrInputEvent) { + if (!aKeyboardEventOrInputEvent) { + return "{}"; + } + switch (aKeyboardEventOrInputEvent.type) { + case "keydown": + case "keypress": + case "keyup": + return `{ type: "${aKeyboardEventOrInputEvent.type}", key="${ + aKeyboardEventOrInputEvent.key + }", charCode=0x${ + aKeyboardEventOrInputEvent.charCode.toString(16).toUpperCase() + } }`; + default: + return `{ type: "${aKeyboardEventOrInputEvent.type}", inputType="${ + aKeyboardEventOrInputEvent.inputType + }", data="${aKeyboardEventOrInputEvent.data}" }`; + } + } + function getEventArrayData(aEvents) { + if (!aEvents.length) { + return "[]"; + } + let result = "[\n"; + for (const e of aEvents) { + result += ` ${getEventData(e)}\n`; + } + return result + "]"; + } + aElement.addEventListener("keydown", pushIntoEvents); + aElement.addEventListener("keypress", pushIntoEvents); + aElement.addEventListener("keyup", pushIntoEvents); + aElement.addEventListener("beforeinput", pushIntoEvents); + aElement.addEventListener("input", pushIntoEvents); + synthesizeKey("\uD842\uDFB7"); + aElement.removeEventListener("keydown", pushIntoEvents); + aElement.removeEventListener("keypress", pushIntoEvents); + aElement.removeEventListener("keyup", pushIntoEvents); + aElement.removeEventListener("beforeinput", pushIntoEvents); + aElement.removeEventListener("input", pushIntoEvents); + const settingDescription = + `aTestPerSurrogateKeyPress=${ + aTestPerSurrogateKeyPress + }, aTestIllFormedUTF16KeyValue=${aTestIllFormedUTF16KeyValue}`; + const allowIllFormedUTF16 = + aTestPerSurrogateKeyPress && aTestIllFormedUTF16KeyValue; + + check(`${aDescription}, ${settingDescription}a surrogate pair`, true, true, !aIsReadonly); + is( + aElement.textContent, + !aIsReadonly ? "\uD842\uDFB7" : "", + `${aDescription}, ${settingDescription}, The typed surrogate pair should've been inserted` + ); + if (aIsReadonly) { + is( + getEventArrayData(events), + getEventArrayData( + aTestPerSurrogateKeyPress + ? ( + allowIllFormedUTF16 + ? [ + { type: "keydown", key: "\uD842\uDFB7", charCode: 0 }, + { type: "keypress", key: "\uD842", charCode: 0xD842 }, + { type: "keypress", key: "\uDFB7", charCode: 0xDFB7 }, + { type: "keyup", key: "\uD842\uDFB7", charCode: 0 }, + ] + : [ + { type: "keydown", key: "\uD842\uDFB7", charCode: 0 }, + { type: "keypress", key: "\uD842\uDFB7", charCode: 0xD842 }, + { type: "keypress", key: "", charCode: 0xDFB7 }, + { type: "keyup", key: "\uD842\uDFB7", charCode: 0 }, + ] + ) + : [ + { type: "keydown", key: "\uD842\uDFB7", charCode: 0 }, + { type: "keypress", key: "\uD842\uDFB7", charCode: 0x20BB7 }, + { type: "keyup", key: "\uD842\uDFB7", charCode: 0 }, + ] + ), + `${aDescription}, ${ + settingDescription + }, Typing a surrogate pair in readonly editor should not cause input events` + ); + } else { + is( + getEventArrayData(events), + getEventArrayData( + aTestPerSurrogateKeyPress + ? ( + allowIllFormedUTF16 + ? [ + { type: "keydown", key: "\uD842\uDFB7", charCode: 0 }, + { type: "keypress", key: "\uD842", charCode: 0xD842 }, + { type: "beforeinput", data: "\uD842", inputType: "insertText" }, + { type: "input", data: "\uD842", inputType: "insertText" }, + { type: "keypress", key: "\uDFB7", charCode: 0xDFB7 }, + { type: "beforeinput", data: "\uDFB7", inputType: "insertText" }, + { type: "input", data: "\uDFB7", inputType: "insertText" }, + { type: "keyup", key: "\uD842\uDFB7", charCode: 0 }, + ] + : [ + { type: "keydown", key: "\uD842\uDFB7", charCode: 0 }, + { type: "keypress", key: "\uD842\uDFB7", charCode: 0xD842 }, + { type: "beforeinput", data: "\uD842\uDFB7", inputType: "insertText" }, + { type: "input", data: "\uD842\uDFB7", inputType: "insertText" }, + { type: "keypress", key: "", charCode: 0xDFB7 }, + { type: "keyup", key: "\uD842\uDFB7", charCode: 0 }, + ] + ) + : [ + { type: "keydown", key: "\uD842\uDFB7", charCode: 0 }, + { type: "keypress", key: "\uD842\uDFB7", charCode: 0x20BB7 }, + { type: "beforeinput", data: "\uD842\uDFB7", inputType: "insertText" }, + { type: "input", data: "\uD842\uDFB7", inputType: "insertText" }, + { type: "keyup", key: "\uD842\uDFB7", charCode: 0 }, + ] + ), + `${aDescription}, ${ + settingDescription + }, Typing a surrogate pair in editor should cause input events` + ); + } + } + await test_typing_surrogate_pair(true, true); + await test_typing_surrogate_pair(true, false); + await test_typing_surrogate_pair(false); + } + + await doTest(htmlEditor, "contenteditable=\"true\"", false, true, false); + + const nsIEditor = SpecialPowers.Ci.nsIEditor; + var editor = SpecialPowers.wrap(window).docShell.editor; + var flags = editor.flags; + // readonly + editor.flags = flags | nsIEditor.eEditorReadonlyMask; + await doTest(htmlEditor, "readonly HTML editor", true, true, false); + + // non-tabbable + editor.flags = flags & ~(nsIEditor.eEditorAllowInteraction); + await doTest(htmlEditor, "non-tabbable HTML editor", false, false, false); + + // readonly and non-tabbable + editor.flags = + (flags | nsIEditor.eEditorReadonlyMask) & + ~(nsIEditor.eEditorAllowInteraction); + await doTest(htmlEditor, "readonly and non-tabbable HTML editor", + true, false, false); + + // plaintext + editor.flags = flags | nsIEditor.eEditorPlaintextMask; + await doTest(htmlEditor, "HTML editor but plaintext mode", false, true, true); + + // plaintext and non-tabbable + editor.flags = (flags | nsIEditor.eEditorPlaintextMask) & + ~(nsIEditor.eEditorAllowInteraction); + await doTest(htmlEditor, "non-tabbable HTML editor but plaintext mode", + false, false, true); + + + // readonly and plaintext + editor.flags = flags | nsIEditor.eEditorPlaintextMask | + nsIEditor.eEditorReadonlyMask; + await doTest(htmlEditor, "readonly HTML editor but plaintext mode", + true, true, true); + + // readonly, plaintext and non-tabbable + editor.flags = (flags | nsIEditor.eEditorPlaintextMask | + nsIEditor.eEditorReadonlyMask) & + ~(nsIEditor.eEditorAllowInteraction); + await doTest(htmlEditor, "readonly and non-tabbable HTML editor but plaintext mode", + true, false, true); + + SpecialPowers.removeSystemEventListener(window, "keypress", listener, true); + SpecialPowers.removeSystemEventListener(window, "keypress", listener, false); + + SimpleTest.finish(); +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_htmleditor_tab_key_handling.html b/editor/libeditor/tests/test_htmleditor_tab_key_handling.html new file mode 100644 index 0000000000..2d428611af --- /dev/null +++ b/editor/libeditor/tests/test_htmleditor_tab_key_handling.html @@ -0,0 +1,112 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<title>Testing indentation with `Tab` key</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script> +"use strict"; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + function initEditor(aEditingHostTag) { + const editor = document.createElement(aEditingHostTag); + editor.setAttribute("contenteditable", "true"); + document.body.insertBefore(editor, document.body.firstChild); + editor.getBoundingClientRect(); + const htmlEditor = SpecialPowers.wrap(window).docShell.editingSession.getEditorForWindow(window); + htmlEditor.flags &= ~SpecialPowers.Ci.nsIEditor.eEditorAllowInteraction; + return editor; + } + + (function test_tab_in_paragraph() { + const editor = initEditor("div"); + editor.innerHTML = "<p>abc</p><p>def</p>"; + editor.getBoundingClientRect(); + editor.focus(); + getSelection().collapse(editor.querySelector("p").firstChild, 1); + synthesizeKey("KEY_Tab"); + is( + editor.innerHTML.replace(/ /g, " "), + `<p>a bc</p><p>def</p>`, + "4 white-spaces should be inserted into the paragraph by Tab" + ); + + let blurred = false; + editor.addEventListener("blur", () => { + blurred = true; + }, {once: true}); + synthesizeKey("KEY_Tab", {shiftKey: true}); + is( + editor.innerHTML.replace(/ /g, " "), + `<p>a bc</p><p>def</p>`, + "The spaces in the paragraph shouldn't be removed by Shift-Tab" + ); + ok(blurred, "Shift-Tab should cause moving focus"); + editor.remove(); + })(); + + (function test_tab_in_body_text() { + const editor = initEditor("div"); + editor.innerHTML = "abc"; + editor.getBoundingClientRect(); + editor.focus(); + getSelection().collapse(editor.firstChild, 1); + synthesizeKey("KEY_Tab"); + is( + editor.innerHTML.replace(/ /g, " "), + `a bc`, + "4 white-spaces should be inserted into the body text by Tab" + ); + editor.remove(); + })(); + + (function test_tab_in_li() { + const editor = initEditor("div"); + for (const list of ["ol", "ul"]) { + editor.innerHTML = `abc<${list}><li>def</li><li>ghi</li><li>jkl</li></${list}>`; + editor.getBoundingClientRect(); + editor.focus(); + getSelection().collapse(editor.querySelector("li + li").firstChild, 1); + synthesizeKey("KEY_Tab"); + is( + editor.innerHTML, + `abc<${list}><li>def</li><${list}><li>ghi</li></${list}><li>jkl</li></${list}>`, + `The list item containing caret should be moved into new sub-<${list}> by Tab` + ); + synthesizeKey("KEY_Tab", {shiftKey: true}); + is( + editor.innerHTML, + `abc<${list}><li>def</li><li>ghi</li><li>jkl</li></${list}>`, + `The list item containing caret should be moved into parent <${list}> by Shift-Tab` + ); + } + editor.remove(); + })(); + + (function test_tab_in_nested_editing_host_in_li() { + const editor = initEditor("div"); + editor.innerHTML = `abc<ul><li>def</li><li><span contenteditable="false">g<span contenteditable="true">h</span>i</span></li><li>jkl</li></ul>`; + editor.getBoundingClientRect(); + editor.focus(); + getSelection().collapse(editor.querySelector("span[contenteditable=true]").firstChild, 1); + synthesizeKey("KEY_Tab"); + is( + editor.innerHTML, + `abc<ul><li>def</li><li><span contenteditable="false">g<span contenteditable="true">h</span>i</span></li><li>jkl</li></ul>`, + `The list item containing caret should be modified by Tab` + ); + editor.remove(); + })(); + + // TODO: Add table cell cases. + + SimpleTest.finish(); +}); +</script> +</body> +</html> diff --git a/editor/libeditor/tests/test_htmleditor_toggle_text_direction.html b/editor/libeditor/tests/test_htmleditor_toggle_text_direction.html new file mode 100644 index 0000000000..9dfa80f113 --- /dev/null +++ b/editor/libeditor/tests/test_htmleditor_toggle_text_direction.html @@ -0,0 +1,73 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<title>Text direction switch target of HTMLEditor</title> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +<script> +"use strict"; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(async () => { + await (async function test_in_contenteditable() { + document.body.innerHTML = "<div><div contenteditable>editable text</div></div>"; + const editingHost = document.querySelector("div[contenteditable]"); + editingHost.focus(); + SpecialPowers.doCommand(window, "cmd_switchTextDirection"); + is( + editingHost.getAttribute("dir"), + "rtl", + "test_in_contenteditable: dir attr of the editing host should be set" + ); + is( + editingHost.parentElement.getAttribute("dir"), + null, + "test_in_contenteditable: dir attr of the parent div of the editing host should not be set" + ); + is( + document.body.getAttribute("dir"), + null, + "test_in_contenteditable: dir attr of the <body> should not be set", + ); + is( + document.documentElement.getAttribute("dir"), + null, + "test_in_contenteditable: dir attr of the <html> should not be set", + ); + })(); + + await (async function test_in_designMode() { + document.body.innerHTML = "<div>abc</div>"; + document.designMode = "on"; + getSelection().collapse(document.querySelector("div").firstChild, 0); + SpecialPowers.doCommand(window, "cmd_switchTextDirection"); + is( + document.querySelector("div").getAttribute("dir"), + null, + "test_in_designMode: dir attr of the <div> should not be set", + ); + is( + document.body.getAttribute("dir"), + "rtl", + "test_in_designMode: dir attr of the <body> should be set", + ); + is( + document.documentElement.getAttribute("dir"), + null, + "test_in_designMode: dir attr of the <html> should not be set", + ); + document.designMode = "off"; + document.body.removeAttribute("dir"); + document.body.innerHTML = ""; + document.documentElement.removeAttribute("dir"); + })(); + + SimpleTest.finish(); +}); +</script> +</head> +<body> +</body> +</html> diff --git a/editor/libeditor/tests/test_initial_selection_and_caret_of_designMode.html b/editor/libeditor/tests/test_initial_selection_and_caret_of_designMode.html new file mode 100644 index 0000000000..5c08370321 --- /dev/null +++ b/editor/libeditor/tests/test_initial_selection_and_caret_of_designMode.html @@ -0,0 +1,57 @@ +<!doctype html> +<head> + <title>Test for initial selection and caret at turning on the designMode with user interaction</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<p>foo</p> +<button onclick="document.designMode = 'on'">click</button> +<script> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + synthesizeMouseAtCenter(document.querySelector("button"), {}); + + is( + document.activeElement, + document.body, + "The <body> element should be active" + ); + is( + getSelection().focusNode, + document.body.firstChild.firstChild, + "The focus node should be the text node in the first paragraph" + ); + is( + getSelection().anchorNode, + document.body.firstChild.firstChild, + "The anchor node should be the text node in the first paragraph" + ); + is( + getSelection().focusOffset, + 0, + "The focus offset should be 0" + ); + is( + getSelection().anchorOffset, + 0, + "The anchor offset should be 0" + ); + + function getHTMLEditor() { + return SpecialPowers.wrap(window).docShell.editingSession?.getEditorForWindow(window); + } + ok( + getHTMLEditor().selectionController.getCaretEnabled(), + "The caret should be enabled" + ); + ok( + getHTMLEditor().selectionController.caretVisible, + "The caret should be visible" + ); + + document.designMode = "off"; + + SimpleTest.finish(); +}); +</script> diff --git a/editor/libeditor/tests/test_inlineTableEditing.html b/editor/libeditor/tests/test_inlineTableEditing.html new file mode 100644 index 0000000000..411b5f6510 --- /dev/null +++ b/editor/libeditor/tests/test_inlineTableEditing.html @@ -0,0 +1,62 @@ +<!DOCTYPE html> +<html> +<head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<div contenteditable></div> +<pre id="test"> + +<script class="testbody" type="application/javascript"> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + const editableInnerHTML = +`<table> + <tr><td>ABCDEFG</td><td>HIJKLMN</td></tr> + <tr><td>ABCDEFG</td><td>HIJKLMN</td></tr> + <tr><td>ABCDEFG</td><td>HIJKLMN</td></tr> +</table>`; + + document.execCommand("enableObjectResizing", false, "false"); + document.execCommand("enableInlineTableEditing", false, "true"); + + function doTest(aDescription, aTable) { + synthesizeMouseAtCenter(aTable, {}); + isnot( + aTable.getAttribute("_moz_resizing"), + "true", + `${aDescription}: _moz_resizing attribute shouldn't be true without object resizing` + ); + + const tr2 = aTable.querySelector("tr + tr"); + synthesizeMouse(tr2, 0, tr2.clientHeight / 2, {}); + ok( + !tr2.isConnected, + `${aDescription}: The second <tr> element should've been removed by a click` + ); + } + + const editingHost = document.querySelector("div[contenteditable]"); + editingHost.innerHTML = editableInnerHTML; + doTest("Testing in Light DOM", editingHost.querySelector("table")); + + editingHost.remove(); + + const shadowHost = document.createElement("div"); + document.body.insertBefore(shadowHost, document.body.firstChild); + const shadowRoot = shadowHost.attachShadow({mode: "open"}); + shadowRoot.appendChild(document.createElement("div")); + shadowRoot.firstChild.setAttribute("contenteditable", ""); + shadowRoot.firstChild.innerHTML = editableInnerHTML; + doTest("Testing in Shadow DOM", shadowRoot.firstChild.querySelector("table")); + + shadowHost.remove(); + + SimpleTest.finish(); +}); +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_inline_style_cache.html b/editor/libeditor/tests/test_inline_style_cache.html new file mode 100644 index 0000000000..7a4f0f9172 --- /dev/null +++ b/editor/libeditor/tests/test_inline_style_cache.html @@ -0,0 +1,151 @@ +<!DOCTYPE html> +<html> +<head> + <title>Tests for inline style cache</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none;"> + +</div> + +<div id="editor" contenteditable></div> +<pre id="test"> + +<script class="testbody" type="application/javascript"> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var editor = document.getElementById("editor"); + editor.focus(); + + document.execCommand("defaultParagraphSeparator", false, "div"); + + var selection = window.getSelection(); + + // #01-01 Typing something after setting some styles should insert some nodes to insert text. + editor.innerHTML = "beforeafter"; + selection.collapse(editor.firstChild, "before".length); + document.execCommand("bold"); + document.execCommand("italic"); + document.execCommand("strikethrough"); + sendString("test"); + + is(editor.innerHTML, "before<b><i><strike>test</strike></i></b>after", + "#01-01 At typing something after setting some styles, should cause inserting some nodes to apply the style"); + + // #01-02 Typing something after removing some characters after setting some styles should work as without removing some character. + editor.innerHTML = "beforeafter"; + selection.collapse(editor.firstChild, "before".length); + document.execCommand("bold"); + document.execCommand("italic"); + document.execCommand("strikethrough"); + synthesizeKey("KEY_Delete"); + sendString("test"); + + todo_is( + editor.innerHTML, + "beforetestfter", + "#01-02-1 At typing something after Delete after setting style, the style should not be preserved for compatibility with the other browsers" + ); + is( + editor.innerHTML, + "before<b><i><strike>test</strike></i></b>fter", + "#01-02-1 At typing something after Delete after setting style, the style should not be preserved, but it's okay to do it for backward compatibility" + ); + + editor.innerHTML = "beforeafter"; + selection.collapse(editor.firstChild, "before".length); + document.execCommand("bold"); + document.execCommand("italic"); + document.execCommand("strikethrough"); + synthesizeKey("KEY_Backspace"); + sendString("test"); + + is(editor.innerHTML, "befor<b><i><strike>test</strike></i></b>after", + "#01-02-2 At typing something after Backspace after setting style, should cause inserting some nodes to apply the style"); + + // #03-01 Replacing in <b style="font-weight: normal;"> shouldn't cause new <b>. + editor.innerHTML = "<b style=\"font-weight: normal;\">beforeselectionafter</b>"; + selection.collapse(editor.firstChild.firstChild, "before".length); + selection.extend(editor.firstChild.firstChild, "beforeselection".length); + sendString("test"); + + is(editor.innerHTML, "<b style=\"font-weight: normal;\">beforetestafter</b>", + "#03-01 Replacing text in styled inline elements should respect the styles"); + + // #03-02 Typing something after removing selected text in <b style="font-weight: normal;"> shouldn't cause new <b>. + editor.innerHTML = "<b style=\"font-weight: normal;\">beforeselectionafter</b>"; + selection.collapse(editor.firstChild.firstChild, "before".length); + selection.extend(editor.firstChild.firstChild, "beforeselection".length); + synthesizeKey("KEY_Backspace"); + sendString("test"); + + is(editor.innerHTML, "<b style=\"font-weight: normal;\">beforetestafter</b>", + "#03-02 Inserting text after removing text in styled inline elements should respect the styles"); + + // #03-03 Typing something after typing Enter at selected text in <b style="font-weight: normal;"> shouldn't cause new <b>. + editor.innerHTML = "<b style=\"font-weight: normal;\">beforeselectionafter</b>"; + selection.collapse(editor.firstChild.firstChild, "before".length); + selection.extend(editor.firstChild.firstChild, "beforeselection".length); + synthesizeKey("KEY_Enter"); + sendString("test"); + + is(editor.innerHTML, "<div><b style=\"font-weight: normal;\">before</b></div><div><b style=\"font-weight: normal;\">testafter</b></div>", + "#03-03-1 Inserting text after typing Enter at selected text in styled inline elements should respect the styles"); + + editor.innerHTML = "<p><b style=\"font-weight: normal;\">beforeselectionafter</b></p>"; + selection.collapse(editor.firstChild.firstChild.firstChild, "before".length); + selection.extend(editor.firstChild.firstChild.firstChild, "beforeselection".length); + synthesizeKey("KEY_Enter"); + sendString("test"); + + is(editor.innerHTML, "<p><b style=\"font-weight: normal;\">before</b></p><p><b style=\"font-weight: normal;\">testafter</b></p>", + "#03-03-2 Inserting text after typing Enter at selected text in styled inline elements should respect the styles"); + + // #04-01 Replacing in some styled inline elements shouldn't cause new same elements. + editor.innerHTML = "<strike style=\"text-decoration: none;\"><i style=\"font-style: normal;\"><b style=\"font-weight: normal;\">beforeselectionafter</b></i></strike>"; + selection.collapse(editor.firstChild.firstChild.firstChild.firstChild, "before".length); + selection.extend(editor.firstChild.firstChild.firstChild.firstChild, "beforeselection".length); + sendString("test"); + + is(editor.innerHTML, "<strike style=\"text-decoration: none;\"><i style=\"font-style: normal;\"><b style=\"font-weight: normal;\">beforetestafter</b></i></strike>", + "#04-01 Replacing text in styled inline elements should respect the styles"); + + // #04-02 Typing something after removing selected text in some styled inline elements shouldn't cause new same elements. + editor.innerHTML = "<strike style=\"text-decoration: none;\"><i style=\"font-style: normal;\"><b style=\"font-weight: normal;\">beforeselectionafter</b>"; + selection.collapse(editor.firstChild.firstChild.firstChild.firstChild, "before".length); + selection.extend(editor.firstChild.firstChild.firstChild.firstChild, "beforeselection".length); + synthesizeKey("KEY_Backspace"); + sendString("test"); + + is(editor.innerHTML, "<strike style=\"text-decoration: none;\"><i style=\"font-style: normal;\"><b style=\"font-weight: normal;\">beforetestafter</b></i></strike>", + "#04-02 Inserting text after removing text in styled inline elements should respect the styles"); + + // #04-03 Typing something after typing Enter at selected text in some styled inline elements shouldn't cause new same elements. + editor.innerHTML = "<strike style=\"text-decoration: none;\"><i style=\"font-style: normal;\"><b style=\"font-weight: normal;\">beforeselectionafter</b>"; + selection.collapse(editor.firstChild.firstChild.firstChild.firstChild, "before".length); + selection.extend(editor.firstChild.firstChild.firstChild.firstChild, "beforeselection".length); + synthesizeKey("KEY_Enter"); + sendString("test"); + + is(editor.innerHTML, "<div><strike style=\"text-decoration: none;\"><i style=\"font-style: normal;\"><b style=\"font-weight: normal;\">before</b></i></strike></div><div><strike style=\"text-decoration: none;\"><i style=\"font-style: normal;\"><b style=\"font-weight: normal;\">testafter</b></i></strike></div>", + "#04-03-1 Inserting text after typing Enter at selected text in styled inline elements should respect the styles"); + + editor.innerHTML = "<p><strike style=\"text-decoration: none;\"><i style=\"font-style: normal;\"><b style=\"font-weight: normal;\">beforeselectionafter</b></p>"; + selection.collapse(editor.firstChild.firstChild.firstChild.firstChild.firstChild, "before".length); + selection.extend(editor.firstChild.firstChild.firstChild.firstChild.firstChild, "beforeselection".length); + synthesizeKey("KEY_Enter"); + sendString("test"); + + is(editor.innerHTML, "<p><strike style=\"text-decoration: none;\"><i style=\"font-style: normal;\"><b style=\"font-weight: normal;\">before</b></i></strike></p><p><strike style=\"text-decoration: none;\"><i style=\"font-style: normal;\"><b style=\"font-weight: normal;\">testafter</b></i></strike></p>", + "#04-03-2 Inserting text after typing Enter at selected text in styled inline elements should respect the styles"); + + SimpleTest.finish(); +}); +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_insertHTML_starting_with_multiple_comment_nodes.html b/editor/libeditor/tests/test_insertHTML_starting_with_multiple_comment_nodes.html new file mode 100644 index 0000000000..c3b440659f --- /dev/null +++ b/editor/libeditor/tests/test_insertHTML_starting_with_multiple_comment_nodes.html @@ -0,0 +1,40 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<title>Insert HTML containing comment nodes</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<script> +"use strict"; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + const editor = document.querySelector("div[contenteditable]"); + getSelection().collapse(editor.querySelector("li").firstChild, 1); + document.execCommand("insertHTML", false, "<!-- 1 --><!-- 2 -->b"); + is( + editor.innerHTML, + "<ul><li>a<!-- 1 --><!-- 2 -->bc</li></ul>", + "The HTML fragment should be inserted as-is" + ); + document.execCommand("undo"); + is( + editor.innerHTML, + "<ul><li>ac</li></ul>", + "Undoing should work as expected" + ); + document.execCommand("redo"); + is( + editor.innerHTML, + "<ul><li>a<!-- 1 --><!-- 2 -->bc</li></ul>", + "Redoing should work as expected" + ); + SimpleTest.finish(); +}); +</script> +</head> +<body> +<div contenteditable><ul><li>ac</li></ul></div> +</body> +</html> diff --git a/editor/libeditor/tests/test_insertParagraph_in_h2_and_li.html b/editor/libeditor/tests/test_insertParagraph_in_h2_and_li.html new file mode 100644 index 0000000000..0af77108c6 --- /dev/null +++ b/editor/libeditor/tests/test_insertParagraph_in_h2_and_li.html @@ -0,0 +1,167 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=449243 +--> +<head> + <title>Test for Bug 449243</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=449243">Mozilla Bug 449243</a> +<p id="display"></p> +<div id="content" contenteditable> + <h2>This is a title</h2> + <ul> + <li>this is a</li> + <li>bullet list</li> + </ul> + <ol> + <li>this is a</li> + <li>numbered list</li> + </ol> +</div> + +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 449243 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + +const CARET_BEGIN = 0; +const CARET_MIDDLE = 1; +const CARET_END = 2; + +function split(element, caretPos, nbKeyPresses) { + // put the caret on the requested position + const offset = (() => { + switch (caretPos) { + case CARET_BEGIN: + return 0; + case CARET_MIDDLE: + return Math.floor(element.textContent.length / 2); + case CARET_END: + return element.textContent.length; + } + return 0; + })(); + getSelection().collapse(element.firstChild, offset); + + // simulates a [Return] keypress + for (let i = 0; i < nbKeyPresses; i++) { + synthesizeKey("KEY_Enter"); + } +} + +function undo(nbKeyPresses) { + for (let i = 0; i < nbKeyPresses; i++) { + document.execCommand("Undo"); + } +} + +function getNewElement(element) { + return element.nextElementSibling; +} + +function runTests() { + const content = document.querySelector("[contenteditable]"); + const header = content.querySelector("h2"); + const ulItem = content.querySelector("ul > li:last-child"); + const olItem = content.querySelector("ol > li:last-child"); + content.focus(); + + // beginning of selection: split current node + split(header, CARET_BEGIN, 1); + is( + getNewElement(header)?.nodeName, + header.nodeName, + "Pressing [Return] at the beginning of a header " + + "should create another header." + ); + split(ulItem, CARET_BEGIN, 2); + is( + getNewElement(ulItem)?.nodeName, + ulItem.nodeName, + "Pressing [Return] at the beginning of an unordered list item " + + "should create another list item." + ); + split(olItem, CARET_BEGIN, 2); + is( + getNewElement(olItem)?.nodeName, + olItem.nodeName, + "Pressing [Return] at the beginning of an ordered list item " + + "should create another list item." + ); + undo(3); + + // middle of selection: split current node + split(header, CARET_MIDDLE, 1); + is( + getNewElement(header)?.nodeName, + header.nodeName, + "Pressing [Return] at the middle of a header " + + "should create another header." + ); + split(ulItem, CARET_MIDDLE, 2); + is( + getNewElement(ulItem)?.nodeName, + ulItem.nodeName, + "Pressing [Return] at the middle of an unordered list item " + + "should create another list item." + ); + split(olItem, CARET_MIDDLE, 2); + is( + getNewElement(olItem)?.nodeName, + olItem.nodeName, + "Pressing [Return] at the middle of an ordered list item " + + "should create another list item." + ); + undo(3); + + // end of selection: create a new div/paragraph + function testEndOfSelection(expected, defaultParagraphSeparator) { + split(header, CARET_END, 1); + is( + content.querySelector("h2+*")?.nodeName, + expected.toUpperCase(), + `Pressing [Return] at the end of a header should create a new <${ + expected + }> (defaultParagraphSeparator: <${defaultParagraphSeparator}>)` + ); + split(ulItem, CARET_END, 2); + is( + content.querySelector("ul+*")?.nodeName, + expected.toUpperCase(), + `Pressing [Return] twice at the end of an unordered list item should create a new <${ + expected + }> (defaultParagraphSeparator: <${defaultParagraphSeparator}>)` + ); + split(olItem, CARET_END, 2); + is( + content.querySelector("ol+*")?.nodeName, + expected.toUpperCase(), + `Pressing [Return] twice at the end of an ordered list item should create a new <${ + expected + }> (defaultParagraphSeparator: <${defaultParagraphSeparator}>)` + ); + undo(3); + } + + document.execCommand("defaultParagraphSeparator", false, "div"); + testEndOfSelection("div", "div"); + document.execCommand("defaultParagraphSeparator", false, "p"); + testEndOfSelection("p", "p"); + document.execCommand("defaultParagraphSeparator", false, "br"); + testEndOfSelection("p", "br"); + + // done + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_insertParagraph_in_inline_editing_host.html b/editor/libeditor/tests/test_insertParagraph_in_inline_editing_host.html new file mode 100644 index 0000000000..ae276fd3ad --- /dev/null +++ b/editor/libeditor/tests/test_insertParagraph_in_inline_editing_host.html @@ -0,0 +1,67 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test "insertParagraph" command in inline editing host</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> +</head> +<body> + +<span contenteditable>foobar</span> +<hr> +<p><span contenteditable>foobar</span></p> +<hr> +<div><span contenteditable>foobar</span></div> +<hr> +<div contenteditable><p contenteditable="false"><span contenteditable>foobar</span></p></div> + +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> + +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + var selection = document.getSelection(); + var editors = document.querySelectorAll("span[contenteditable]"); + + var editor = editors.item(0); + editor.focus(); + selection.collapse(editor.firstChild, 3); + document.execCommand("insertParagraph", false); + is(editor.innerHTML, "foo<br>bar", + "insertParagraph should insert <br> element (inline editing host is in <body>)"); + + editor = editors.item(1); + editor.focus(); + selection.collapse(editor.firstChild, 3); + document.execCommand("insertParagraph", false); + is(editor.parentNode.innerHTML, "<span contenteditable=\"\">foo<br>bar</span>", + "insertParagraph should insert <br> element (inline editing host is in <p>)"); + + editor = editors.item(2); + editor.focus(); + selection.collapse(editor.firstChild, 3); + document.execCommand("insertParagraph", false); + is(editor.parentNode.innerHTML, "<span contenteditable=\"\">foo<br>bar</span>", + "insertParagraph should insert <br> element (inline editing host is in <div>)"); + + editor = editors.item(3); + editor.focus(); + selection.collapse(editor.firstChild, 3); + document.execCommand("insertParagraph", false); + is(editor.parentNode.parentNode.innerHTML, "<p contenteditable=\"false\"><span contenteditable=\"\">foo<br>bar</span></p>", + "insertParagraph should insert <br> element (inline editing host is in <p contenteditable=\"false\"> in <div contenteditable>)"); + + SimpleTest.finish(); +}); + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_insertText_around_text_node_in_plaintext_mode.html b/editor/libeditor/tests/test_insertText_around_text_node_in_plaintext_mode.html new file mode 100644 index 0000000000..1acf319456 --- /dev/null +++ b/editor/libeditor/tests/test_insertText_around_text_node_in_plaintext_mode.html @@ -0,0 +1,92 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<title>Test insertText when caret is around a text node</title> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script> +"use strict"; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(async () => { + function getHTMLEditor() { + let editingSession = SpecialPowers.wrap(window).docShell.editingSession; + if (!editingSession) { + return null; + } + let editor = editingSession.getEditorForWindow(window); + if (!editor) { + return null; + } + return editor.QueryInterface(SpecialPowers.Ci.nsIHTMLEditor); + } + + document.designMode = "on"; + document.body.focus(); + await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))); + const editor = getHTMLEditor(); + editor.flags |= SpecialPowers.Ci.nsIEditor.eEditorPlaintextMask; + + (function test_collapsed_before_first_text_child() { + document.body.innerHTML = "bc<br>"; + getSelection().collapse(document.body, 0); + document.execCommand("insertText", false, "a"); + is( + document.body.firstChild.data, + "abc", + "test_collapsed_before_first_text_child: Text should be inserted into start of the first text child" + ); + })(); + + (function test_collapsed_before_first_text_child() { + document.body.innerHTML = "bc<br>"; + getSelection().collapse(document.body, 0); + document.execCommand("insertText", false, "a"); + is( + document.body.firstChild.data, + "abc", + "test_collapsed_before_first_text_child: Text should be inserted into start of the first text child" + ); + })(); + + (function test_collapsed_after_last_text_child() { + document.body.innerHTML = "ab"; + getSelection().collapse(document.body, 1); + document.execCommand("insertText", false, "c"); + is( + document.body.firstChild.data, + "abc", + "test_collapsed_after_last_text_child: Text should be inserted into end of the last text child" + ); + })(); + + (function test_collapsed_after_text_child() { + document.body.innerHTML = "ab<br>"; + getSelection().collapse(document.body, 1); + document.execCommand("insertText", false, "c"); + is( + document.body.firstChild.data, + "abc", + "test_collapsed_after_text_child: Text should be inserted into end of the previous text child" + ); + })(); + + (function test_collapsed_at_text_child() { + document.body.innerHTML = "<img>bc"; + getSelection().collapse(document.body, 1); + document.execCommand("insertText", false, "a"); + is( + document.body.firstChild.nextSibling.data, + "abc", + "test_collapsed_at_text_child: Text should be inserted into start of the text child" + ); + })(); + + document.designMode = "off"; + SimpleTest.finish(); +}); +</script> +<body></body> +</html> diff --git a/editor/libeditor/tests/test_join_split_node_direction_change_command.html b/editor/libeditor/tests/test_join_split_node_direction_change_command.html new file mode 100644 index 0000000000..a68396b700 --- /dev/null +++ b/editor/libeditor/tests/test_join_split_node_direction_change_command.html @@ -0,0 +1,57 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +<script> +"use strict"; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(async () => { + let iframe = document.querySelector("iframe"); + async function resetIframe() { + iframe?.remove(); + iframe = document.createElement("iframe"); + document.body.appendChild(iframe); + iframe.srcdoc = "<body></body>"; + if (iframe.contentDocument?.readyState != "complete") { + await new Promise(resolve => iframe.addEventListener("load", resolve, {once: true})); + } + iframe.contentWindow.focus(); + } + + await resetIframe(); + iframe.contentDocument.body.innerHTML = "<div contenteditable><br></div>"; + ok( + iframe.contentDocument.queryCommandSupported("enableCompatibleJoinSplitDirection"), + "command should be supported" + ); + ok( + iframe.contentDocument.queryCommandEnabled("enableCompatibleJoinSplitDirection"), + "command should be enabled" + ); + ok( + iframe.contentDocument.queryCommandState("enableCompatibleJoinSplitDirection"), + "command state should be true" + ); + is( + iframe.contentDocument.queryCommandValue("enableCompatibleJoinSplitDirection"), + "", + "command value should be empty string" + ); + ok( + !iframe.contentDocument.execCommand("enableCompatibleJoinSplitDirection", false, "false"), + "command to disable it should return false" + ); + ok( + iframe.contentDocument.queryCommandState("enableCompatibleJoinSplitDirection"), + "command state should be true even after executing the command to disable it" + ); + + SimpleTest.finish(); +}); +</script> +</head> +<body></body> +</html> diff --git a/editor/libeditor/tests/test_keypress_untrusted_event.html b/editor/libeditor/tests/test_keypress_untrusted_event.html new file mode 100644 index 0000000000..e8151b53e6 --- /dev/null +++ b/editor/libeditor/tests/test_keypress_untrusted_event.html @@ -0,0 +1,105 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=622245 +--> +<head> + <title>Test for untrusted keypress events</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=622245">Mozilla Bug 622245</a> +<p id="display"></p> +<div id="content"> +<input id="i"><br> +<textarea id="t"></textarea><br> +<div id="d" contenteditable style="min-height: 1em;"></div> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 674770 **/ +SimpleTest.waitForExplicitFinish(); + +var input = document.getElementById("i"); +var textarea = document.getElementById("t"); +var div = document.getElementById("d"); + +addLoadEvent(function() { + input.focus(); + + SimpleTest.executeSoon(function() { + input.addEventListener("keypress", + function(aEvent) { + is(aEvent.target, input, + "The keypress event target isn't the input element"); + + SimpleTest.executeSoon(function() { + is(input.value, "", + "Did keypress event cause modifying the input element?"); + textarea.focus(); + SimpleTest.executeSoon(runTextareaTest); + }); + }, {once: true}); + var keypress = new KeyboardEvent("keypress", { + bubbles: true, + cancelable: true, + view: document.defaultView, + keyCode: 0, + charCode:"a".charCodeAt(0), + }); + input.dispatchEvent(keypress); + }); +}); + +function runTextareaTest() { + textarea.addEventListener("keypress", + function(aEvent) { + is(aEvent.target, textarea, + "The keypress event target isn't the textarea element"); + + SimpleTest.executeSoon(function() { + is(textarea.value, "", + "Did keypress event cause modifying the textarea element?"); + div.focus(); + SimpleTest.executeSoon(runContentediableTest); + }); + }, {once: true}); + var keypress = new KeyboardEvent("keypress", { + bubbles: true, + cancelable: true, + view: document.defaultView, + keyCode: 0, + charCode:"b".charCodeAt(0), + }); + textarea.dispatchEvent(keypress); +} + +function runContentediableTest() { + div.addEventListener("keypress", + function(aEvent) { + is(aEvent.target, div, + "The keypress event target isn't the div element"); + + SimpleTest.executeSoon(function() { + is(div.innerHTML, "", + "Did keypress event cause modifying the div element?"); + + SimpleTest.finish(); + }); + }, {once: true}); + var keypress = new KeyboardEvent("keypress", { + bubbles: true, + cancelable: true, + view: document.defaultView, + keyCode: 0, + charCode:"c".charCodeAt(0), + }); + div.dispatchEvent(keypress); +} + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_label_contenteditable.html b/editor/libeditor/tests/test_label_contenteditable.html new file mode 100644 index 0000000000..43bf9d4292 --- /dev/null +++ b/editor/libeditor/tests/test_label_contenteditable.html @@ -0,0 +1,18 @@ +<!doctype html> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<label style="display: block" contenteditable> + Foo +</label> +<script> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + let label = document.querySelector("label"); + synthesizeMouseAtCenter(label, {}); + is(document.activeElement, label, "Label should get focus"); + synthesizeKey("x", {}); + is(label.innerText.trim(), "Foox", "Should not select the whole label"); + SimpleTest.finish(); +}); +</script> diff --git a/editor/libeditor/tests/test_middle_click_paste.html b/editor/libeditor/tests/test_middle_click_paste.html new file mode 100644 index 0000000000..eaa918c194 --- /dev/null +++ b/editor/libeditor/tests/test_middle_click_paste.html @@ -0,0 +1,680 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test for paste with middle button click</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none;"> + +</div> + +<div id="container"></div> + +<textarea id="toCopyPlaintext" style="display: none;"></textarea> +<iframe id="toCopyHTMLContent" srcdoc="<body></body>" style="display: none;"></iframe> + +<pre id="test"> + +<script class="testbody" type="application/javascript"> +SimpleTest.waitForExplicitFinish(); + +// TODO: This file should test complicated cases too. +// E.g., pasting into existing content, e.g., pasting invalid child +// element for the parent elements at insertion point. + +async function copyPlaintext(aText) { + return new Promise(resolve => { + SimpleTest.waitForClipboard(aText, + () => { + let element = document.getElementById("toCopyPlaintext"); + element.style.display = "block"; + element.focus(); + element.value = aText; + synthesizeKey("a", {accelKey: true}); + synthesizeKey("c", {accelKey: true}); + }, + () => { + ok(true, `Succeeded to copy "${aText}" to clipboard`); + let element = document.getElementById("toCopyPlaintext"); + element.style.display = "none"; + resolve(); + }, + () => { + ok(false, `Failed to copy "${aText}" to clipboard`); + SimpleTest.finish(); + }); + }); +} + +async function copyHTMLContent(aInnerHTML) { + let iframe = document.getElementById("toCopyHTMLContent"); + iframe.style.display = "block"; + iframe.contentDocument.body.scrollTop; + iframe.contentDocument.body.innerHTML = aInnerHTML; + iframe.contentWindow.focus(); + iframe.contentWindow.getSelection().selectAllChildren(iframe.contentDocument.body); + return new Promise(resolve => { + SimpleTest.waitForClipboard( + () => { return true; }, + () => { + synthesizeKey("c", {accelKey: true}, iframe.contentWindow); + }, + () => { + ok(true, `Succeeded to copy "${aInnerHTML}" to clipboard as HTML`); + iframe.style.display = "none"; + resolve(); + }, + () => { + ok(false, `Failed to copy "${aInnerHTML}" to clipboard`); + SimpleTest.finish(); + }, + "text/html"); + }); +} + +function checkInputEvent(aEvent, aInputType, aData, aDataTransfer, aTargetRanges, aDescription) { + ok(aEvent instanceof InputEvent, + `"${aEvent.type}" event should be dispatched with InputEvent interface ${aDescription}`); + is(aEvent.cancelable, aEvent.type === "beforeinput", + `"${aEvent.type}" event should ${aEvent.type === "beforeinput" ? "be" : "be never"} cancelable ${aDescription}`); + is(aEvent.bubbles, true, + `"${aEvent.type}" event should always bubble ${aDescription}`); + is(aEvent.inputType, aInputType, + `inputType of "${aEvent.type}" event should be "${aInputType}" ${aDescription}`); + is(aEvent.data, aData, + `data of "${aEvent.type}" event should be ${aData} ${aDescription}`); + if (aDataTransfer === null) { + is(aEvent.dataTransfer, null, + `dataTransfer of "${aEvent.type}" event should be null ${aDescription}`); + } else { + for (let dataTransfer of aDataTransfer) { + is(aEvent.dataTransfer.getData(dataTransfer.type), dataTransfer.data, + `dataTransfer of "${aEvent.type}" should have "${dataTransfer.data}" whose type is "${dataTransfer.type}" ${aDescription}`); + } + } + let targetRanges = aEvent.getTargetRanges(); + if (aTargetRanges.length === 0) { + is(targetRanges.length, 0, + `getTargetRange() of "${aEvent.type}" event should return empty array: ${aDescription}`); + } else { + is(targetRanges.length, aTargetRanges.length, + `getTargetRange() of "${aEvent.type}" event should return static range array: ${aDescription}`); + if (targetRanges.length == aTargetRanges.length) { + for (let i = 0; i < targetRanges.length; i++) { + is(targetRanges[i].startContainer, aTargetRanges[i].startContainer, + `startContainer of getTargetRanges()[${i}] of "${aEvent.type}" event does not match: ${aDescription}`); + is(targetRanges[i].startOffset, aTargetRanges[i].startOffset, + `startOffset of getTargetRanges()[${i}] of "${aEvent.type}" event does not match: ${aDescription}`); + is(targetRanges[i].endContainer, aTargetRanges[i].endContainer, + `endContainer of getTargetRanges()[${i}] of "${aEvent.type}" event does not match: ${aDescription}`); + is(targetRanges[i].endOffset, aTargetRanges[i].endOffset, + `endOffset of getTargetRanges()[${i}] of "${aEvent.type}" event does not match: ${aDescription}`); + } + } + } +} + +async function doTextareaTests(aTextarea) { + let beforeInputEvents = []; + let inputEvents = []; + function onBeforeInput(aEvent) { + beforeInputEvents.push(aEvent); + } + function onInput(aEvent) { + inputEvents.push(aEvent); + } + aTextarea.addEventListener("beforeinput", onBeforeInput); + aTextarea.addEventListener("input", onInput); + + await copyPlaintext("abc\ndef\nghi"); + aTextarea.focus(); + beforeInputEvents = []; + inputEvents = []; + synthesizeMouseAtCenter(aTextarea, {button: 1, ctrlKey: true}); + is(aTextarea.value, + "> abc\n> def\n> ghi\n\n", + "Pasted each line should start with \"> \""); + is(beforeInputEvents.length, 1, + 'One "beforeinput" event should be fired #1'); + checkInputEvent(beforeInputEvents[0], "insertFromPasteAsQuotation", "abc\ndef\nghi", null, [], "#1"); + is(inputEvents.length, 1, + 'One "input" event should be fired #1'); + checkInputEvent(inputEvents[0], "insertFromPasteAsQuotation", "abc\ndef\nghi", null, [], "#1"); + aTextarea.value = ""; + + await copyPlaintext("> abc\n> def\n> ghi"); + aTextarea.focus(); + beforeInputEvents = []; + inputEvents = []; + synthesizeMouseAtCenter(aTextarea, {button: 1, ctrlKey: true}); + is(aTextarea.value, + ">> abc\n>> def\n>> ghi\n\n", + "Pasted each line should be start with \">> \" when already quoted one level"); + is(beforeInputEvents.length, 1, + 'One "beforeinput" event should be fired #2'); + checkInputEvent(beforeInputEvents[0], "insertFromPasteAsQuotation", "> abc\n> def\n> ghi", null, [], "#2"); + is(inputEvents.length, 1, + 'One "input" event should be fired #2'); + checkInputEvent(inputEvents[0], "insertFromPasteAsQuotation", "> abc\n> def\n> ghi", null, [], "#2"); + aTextarea.value = ""; + + await copyPlaintext("> abc\n> def\n\nghi"); + aTextarea.focus(); + beforeInputEvents = []; + inputEvents = []; + synthesizeMouseAtCenter(aTextarea, {button: 1, ctrlKey: true}); + is(aTextarea.value, + ">> abc\n>> def\n> \n> ghi\n\n", + "Pasted each line should be start with \">> \" when already quoted one level"); + is(beforeInputEvents.length, 1, + 'One "beforeinput" event should be fired #3'); + checkInputEvent(beforeInputEvents[0], "insertFromPasteAsQuotation", "> abc\n> def\n\nghi", null, [], "#3"); + is(inputEvents.length, 1, + 'One "input" event should be fired #3'); + checkInputEvent(inputEvents[0], "insertFromPasteAsQuotation", "> abc\n> def\n\nghi", null, [], "#3"); + aTextarea.value = ""; + + await copyPlaintext("abc\ndef\n\n"); + aTextarea.focus(); + beforeInputEvents = []; + inputEvents = []; + synthesizeMouseAtCenter(aTextarea, {button: 1, ctrlKey: true}); + is(aTextarea.value, + "> abc\n> def\n> \n", + "If pasted text ends with \"\\n\", only the last line should not started with \">\""); + is(beforeInputEvents.length, 1, + 'One "beforeinput" event should be fired #4'); + checkInputEvent(beforeInputEvents[0], "insertFromPasteAsQuotation", "abc\ndef\n\n", null, [], "#4"); + is(inputEvents.length, 1, + 'One "input" event should be fired #4'); + checkInputEvent(inputEvents[0], "insertFromPasteAsQuotation", "abc\ndef\n\n", null, [], "#4"); + aTextarea.value = ""; + + await copyPlaintext("abc\ndef\n\n"); + aTextarea.addEventListener("paste", (event) => { event.preventDefault(); }, {once: true}); + aTextarea.focus(); + beforeInputEvents = []; + inputEvents = []; + synthesizeMouseAtCenter(aTextarea, {button: 1, ctrlKey: true}); + is(aTextarea.value, "", + 'Pasting as quote should have been canceled if "paste" event was canceled'); + is(beforeInputEvents.length, 0, + 'No "beforeinput" event should be fired since "paste" event was canceled #5'); + is(inputEvents.length, 0, + 'No "input" event should be fired since "paste" was canceled #5'); + aTextarea.value = ""; + + await copyPlaintext("abc\ndef\n\n"); + aTextarea.addEventListener("beforeinput", (event) => { event.preventDefault(); }, {once: true}); + aTextarea.focus(); + beforeInputEvents = []; + inputEvents = []; + synthesizeMouseAtCenter(aTextarea, {button: 1, ctrlKey: true}); + is(aTextarea.value, "", + 'Pasting as quote should have been canceled if "beforeinput" event was canceled'); + is(beforeInputEvents.length, 1, + 'One "beforeinput" event should be fired #5'); + checkInputEvent(beforeInputEvents[0], "insertFromPasteAsQuotation", "abc\ndef\n\n", null, [], "#6"); + is(inputEvents.length, 0, + 'No "input" event should be fired since "beforeinput" was canceled #6'); + aTextarea.value = ""; + + let pasteEventCount = 0; + function pasteEventLogger(event) { + pasteEventCount++; + } + aTextarea.addEventListener("paste", pasteEventLogger); + + await copyPlaintext("abc"); + aTextarea.focus(); + document.body.addEventListener("click", (event) => { event.preventDefault(); }, {capture: true, once: true}); + beforeInputEvents = []; + inputEvents = []; + synthesizeMouseAtCenter(aTextarea, {button: 1}); + is(aTextarea.value, "abc", + "If 'click' event is consumed at capturing phase of the <body>, paste should not be canceled"); + is(pasteEventCount, 1, + "If 'click' event is consumed at capturing phase of the <body>, 'paste' event should still be fired"); + is(beforeInputEvents.length, 1, + '"beforeinput" event should be fired when the "click" event is canceled'); + checkInputEvent(beforeInputEvents[0], "insertFromPaste", "abc", null, [], 'when the "click" event is canceled'); + is(inputEvents.length, 1, + '"input" event should be fired when the "click" event is canceled'); + checkInputEvent(inputEvents[0], "insertFromPaste", "abc", null, [], 'when the "click" event is canceled'); + aTextarea.value = ""; + + await copyPlaintext("abc"); + aTextarea.focus(); + aTextarea.addEventListener("mouseup", (event) => { event.preventDefault(); }, {once: true}); + pasteEventCount = 0; + beforeInputEvents = []; + inputEvents = []; + synthesizeMouseAtCenter(aTextarea, {button: 1}); + is(aTextarea.value, "abc", + "Even if 'mouseup' event is consumed, paste should be done"); + is(pasteEventCount, 1, + "Even if 'mouseup' event is consumed, 'paste' event should be fired once"); + is(beforeInputEvents.length, 1, + 'One "beforeinput" event should be fired even if "mouseup" event is canceled'); + checkInputEvent(beforeInputEvents[0], "insertFromPaste", "abc", null, [], 'even if "mouseup" event is canceled'); + is(inputEvents.length, 1, + 'One "input" event should be fired even if "mouseup" event is canceled'); + checkInputEvent(inputEvents[0], "insertFromPaste", "abc", null, [], 'even if "mouseup" event is canceled'); + aTextarea.value = ""; + + await copyPlaintext("abc"); + aTextarea.focus(); + aTextarea.addEventListener("click", (event) => { event.preventDefault(); }, {once: true}); + pasteEventCount = 0; + beforeInputEvents = []; + inputEvents = []; + synthesizeMouseAtCenter(aTextarea, {button: 1}); + is(aTextarea.value, "abc", + "If 'click' event handler is added to the <textarea>, paste should not be canceled"); + is(pasteEventCount, 1, + "If 'click' event handler is added to the <textarea>, 'paste' event should be fired once"); + is(beforeInputEvents.length, 1, + 'One "beforeinput" event should be fired even if "click" event is canceled in bubbling phase'); + checkInputEvent(beforeInputEvents[0], "insertFromPaste", "abc", null, [], 'even if "click" event is canceled in bubbling phase'); + is(inputEvents.length, 1, + 'One "input" event should be fired even if "click" event is canceled in bubbling phase'); + checkInputEvent(inputEvents[0], "insertFromPaste", "abc", null, [], 'even if "click" event is canceled in bubbling phase'); + aTextarea.value = ""; + + await copyPlaintext("abc"); + aTextarea.focus(); + aTextarea.addEventListener("auxclick", (event) => { event.preventDefault(); }, {once: true}); + pasteEventCount = 0; + beforeInputEvents = []; + inputEvents = []; + synthesizeMouseAtCenter(aTextarea, {button: 1}); + is(aTextarea.value, "", + "If 'auxclick' event is consumed, paste should be canceled"); + is(pasteEventCount, 0, + "If 'auxclick' event is consumed, 'paste' event should not be fired once"); + is(beforeInputEvents.length, 0, + 'No "beforeinput" event should be fired if "auxclick" event is canceled'); + is(inputEvents.length, 0, + 'No "input" event should be fired if "auxclick" event is canceled'); + aTextarea.value = ""; + + await copyPlaintext("abc"); + aTextarea.focus(); + aTextarea.addEventListener("paste", (event) => { event.preventDefault(); }, {once: true}); + pasteEventCount = 0; + beforeInputEvents = []; + inputEvents = []; + synthesizeMouseAtCenter(aTextarea, {button: 1}); + is(aTextarea.value, "", + "If 'paste' event is consumed, paste should be canceled"); + is(pasteEventCount, 1, + 'One "paste" event should be fired for making it possible to consume'); + is(beforeInputEvents.length, 0, + 'No "beforeinput" event should be fired if "paste" event is canceled'); + is(inputEvents.length, 0, + 'No "input" event should be fired if "paste" event is canceled'); + aTextarea.value = ""; + + await copyPlaintext("abc"); + aTextarea.focus(); + aTextarea.addEventListener("beforeinput", (event) => { event.preventDefault(); }, {once: true}); + pasteEventCount = 0; + beforeInputEvents = []; + inputEvents = []; + synthesizeMouseAtCenter(aTextarea, {button: 1}); + is(aTextarea.value, "", + "If 'beforeinput' event is consumed, paste should be canceled"); + is(pasteEventCount, 1, + 'One "paste" event should be fired before "beforeinput" event is consumed'); + is(beforeInputEvents.length, 1, + 'One "beforeinput" event should be fired for making it possible to consume'); + checkInputEvent(beforeInputEvents[0], "insertFromPaste", "abc", null, [], 'when "beforeinput" is canceled in bubbling phase'); + is(inputEvents.length, 0, + 'No "input" event should be fired if "paste" event is canceled'); + aTextarea.value = ""; + + aTextarea.removeEventListener("paste", pasteEventLogger); + aTextarea.removeEventListener("beforeinput", onBeforeInput); + aTextarea.removeEventListener("input", onInput); +} + +async function doContenteditableTests(aEditableDiv) { + let beforeInputEvents = []; + let inputEvents = []; + let selectionRanges = []; + function onBeforeInput(aEvent) { + beforeInputEvents.push(aEvent); + let selection = document.getSelection(); + selectionRanges = []; + for (let i = 0; i < selection.rangeCount; i++) { + let range = selection.getRangeAt(i); + selectionRanges.push({startContainer: range.startContainer, startOffset: range.startOffset, + endContainer: range.endContainer, endOffset: range.endOffset}); + } + } + function onInput(aEvent) { + inputEvents.push(aEvent); + } + aEditableDiv.addEventListener("beforeinput", onBeforeInput); + aEditableDiv.addEventListener("input", onInput); + + await copyPlaintext("abc\ndef\nghi"); + aEditableDiv.focus(); + beforeInputEvents = []; + inputEvents = []; + synthesizeMouseAtCenter(aEditableDiv, {button: 1, ctrlKey: true}); + is(aEditableDiv.innerHTML, + "<blockquote type=\"cite\">abc<br>def<br>ghi</blockquote>", + "Pasted plaintext should be in <blockquote> element and each linebreaker should be <br> element"); + is(beforeInputEvents.length, 1, + 'One "beforeinput" event should be fired on the editing host'); + checkInputEvent(beforeInputEvents[0], "insertFromPasteAsQuotation", null, + [{type: "text/plain", data: "abc\ndef\nghi"}], selectionRanges, "(contenteditable)"); + is(inputEvents.length, 1, + 'One "input" event should be fired on the editing host'); + checkInputEvent(inputEvents[0], "insertFromPasteAsQuotation", null, + [{type: "text/plain", data: "abc\ndef\nghi"}], [], "(contenteditable)"); + aEditableDiv.innerHTML = ""; + + let pasteEventCount = 0; + function pasteEventLogger(event) { + pasteEventCount++; + } + aEditableDiv.addEventListener("paste", pasteEventLogger); + + await copyPlaintext("abc"); + aEditableDiv.focus(); + window.addEventListener("click", (event) => { event.preventDefault(); }, {capture: true, once: true}); + beforeInputEvents = []; + inputEvents = []; + synthesizeMouseAtCenter(aEditableDiv, {button: 1}); + is(aEditableDiv.innerHTML, "abc", + "If 'click' event is consumed at capturing phase of the window, paste should not be canceled"); + is(pasteEventCount, 1, + "If 'click' event is consumed at capturing phase of the window, 'paste' event should be fired once"); + is(beforeInputEvents.length, 1, + '"beforeinput" event should still be fired when the "click" event is canceled (contenteditable)'); + checkInputEvent(beforeInputEvents[0], "insertFromPaste", null, + [{type: "text/plain", data: "abc"}], selectionRanges, 'when the "click" event is canceled (contenteditable)'); + is(inputEvents.length, 1, + '"input" event should still be fired when the "click" event is canceled (contenteditable)'); + checkInputEvent(inputEvents[0], "insertFromPaste", null, + [{type: "text/plain", data: "abc"}], [], 'when the "click" event is canceled (contenteditable)'); + aEditableDiv.innerHTML = ""; + + await copyPlaintext("abc"); + aEditableDiv.focus(); + aEditableDiv.addEventListener("mouseup", (event) => { event.preventDefault(); }, {once: true}); + pasteEventCount = 0; + beforeInputEvents = []; + inputEvents = []; + synthesizeMouseAtCenter(aEditableDiv, {button: 1}); + is(aEditableDiv.innerHTML, "abc", + "Even if 'mouseup' event is consumed, paste should be done"); + is(pasteEventCount, 1, + "Even if 'mouseup' event is consumed, 'paste' event should be fired once"); + is(beforeInputEvents.length, 1, + 'One "beforeinput" event should be fired even if "mouseup" event is canceled (contenteditable)'); + checkInputEvent(beforeInputEvents[0], "insertFromPaste", null, [{type: "text/plain", data: "abc"}], selectionRanges, + 'even if "mouseup" event is canceled (contenteditable)'); + is(inputEvents.length, 1, + 'One "input" event should be fired even if "mouseup" event is canceled (contenteditable)'); + checkInputEvent(inputEvents[0], "insertFromPaste", null, [{type: "text/plain", data: "abc"}], [], + 'even if "mouseup" event is canceled (contenteditable)'); + aEditableDiv.innerHTML = ""; + + await copyPlaintext("abc"); + aEditableDiv.focus(); + aEditableDiv.addEventListener("click", (event) => { event.preventDefault(); }, {once: true}); + pasteEventCount = 0; + beforeInputEvents = []; + inputEvents = []; + synthesizeMouseAtCenter(aEditableDiv, {button: 1}); + is(aEditableDiv.innerHTML, "abc", + "Even if 'click' event handler is added to the editing host, paste should not be canceled"); + is(pasteEventCount, 1, + "Even if 'click' event handler is added to the editing host, 'paste' event should be fired"); + is(beforeInputEvents.length, 1, + 'One "beforeinput" event should be fired even if "click" event is canceled in bubbling phase (contenteditable)'); + checkInputEvent(beforeInputEvents[0], "insertFromPaste", null, [{type: "text/plain", data: "abc"}], selectionRanges, + 'even if "click" event is canceled in bubbling phase (contenteditable)'); + is(inputEvents.length, 1, + 'One "input" event should be fired even if "click" event is canceled in bubbling phase (contenteditable)'); + checkInputEvent(inputEvents[0], "insertFromPaste", null, [{type: "text/plain", data: "abc"}], [], + 'even if "click" event is canceled in bubbling phase (contenteditable)'); + aEditableDiv.innerHTML = ""; + + await copyPlaintext("abc"); + aEditableDiv.focus(); + aEditableDiv.addEventListener("auxclick", (event) => { event.preventDefault(); }, {once: true}); + pasteEventCount = 0; + beforeInputEvents = []; + inputEvents = []; + synthesizeMouseAtCenter(aEditableDiv, {button: 1}); + is(aEditableDiv.innerHTML, "", + "If 'auxclick' event is consumed, paste should be canceled"); + is(pasteEventCount, 0, + "If 'auxclick' event is consumed, 'paste' event should not be fired"); + is(beforeInputEvents.length, 0, + 'No "beforeinput" event should be fired if "auxclick" event is canceled (contenteditable)'); + is(inputEvents.length, 0, + 'No "input" event should be fired if "auxclick" event is canceled (contenteditable)'); + aEditableDiv.innerHTML = ""; + + await copyPlaintext("abc"); + aEditableDiv.focus(); + aEditableDiv.addEventListener("paste", (event) => { event.preventDefault(); }, {once: true}); + pasteEventCount = 0; + beforeInputEvents = []; + inputEvents = []; + synthesizeMouseAtCenter(aEditableDiv, {button: 1}); + is(aEditableDiv.innerHTML, "", + "If 'paste' event is consumed, paste should be canceled"); + is(pasteEventCount, 1, + 'One "paste" event should be fired for making it possible to consume'); + is(beforeInputEvents.length, 0, + 'No "beforeinput" event should be fired if "paste" event is canceled (contenteditable)'); + is(inputEvents.length, 0, + 'No "input" event should be fired if "paste" event is canceled (contenteditable)'); + aEditableDiv.innerHTML = ""; + + await copyPlaintext("abc"); + aEditableDiv.focus(); + aEditableDiv.addEventListener("beforeinput", (event) => { event.preventDefault(); }, {once: true}); + pasteEventCount = 0; + beforeInputEvents = []; + inputEvents = []; + synthesizeMouseAtCenter(aEditableDiv, {button: 1}); + is(aEditableDiv.innerHTML, "", + "If 'paste' event is consumed, paste should be canceled"); + is(pasteEventCount, 1, + 'One "paste" event should be fired before "beforeinput" event'); + is(beforeInputEvents.length, 1, + 'One "beforeinput" event should be fired for making it possible to consume (contenteditable)'); + checkInputEvent(beforeInputEvents[0], "insertFromPaste", null, [{type: "text/plain", data: "abc"}], selectionRanges, + 'when "beforeinput" will be canceled (contenteditable)'); + is(inputEvents.length, 0, + 'No "input" event should be fired if "beforeinput" event is canceled (contenteditable)'); + aEditableDiv.innerHTML = ""; + + // If clipboard event is disabled, InputEvent.dataTransfer should have only empty string. + await SpecialPowers.pushPrefEnv({"set": [["dom.event.clipboardevents.enabled", false]]}); + await copyPlaintext("abc"); + aEditableDiv.focus(); + pasteEventCount = 0; + beforeInputEvents = []; + inputEvents = []; + synthesizeMouseAtCenter(aEditableDiv, {button: 1}); + is(aEditableDiv.innerHTML, "abc", + "Even if clipboard event is disabled, paste should be done"); + is(pasteEventCount, 0, + "If clipboard event is disabled, 'paste' event shouldn't be fired once"); + is(beforeInputEvents.length, 1, + 'One "beforeinput" event should be fired even if clipboard event is disabled (contenteditable)'); + checkInputEvent(beforeInputEvents[0], "insertFromPaste", null, [{type: "text/plain", data: ""}], selectionRanges, + "when clipboard event is disabled (contenteditable)"); + is(inputEvents.length, 1, + 'One "input" event should be fired even if clipboard event is disabled (contenteditable)'); + checkInputEvent(inputEvents[0], "insertFromPaste", null, [{type: "text/plain", data: ""}], [], + "when clipboard event is disabled (contenteditable)"); + await SpecialPowers.pushPrefEnv({"set": [["dom.event.clipboardevents.enabled", true]]}); + aEditableDiv.innerHTML = ""; + + aEditableDiv.removeEventListener("paste", pasteEventLogger); + + // Oddly, copyHTMLContent fails randomly only on Linux. Let's skip this. + if (navigator.platform.startsWith("Linux")) { + aEditableDiv.removeEventListener("input", onInput); + return; + } + + await copyHTMLContent("<p>abc</p><p>def</p><p>ghi</p>"); + aEditableDiv.focus(); + beforeInputEvents = []; + inputEvents = []; + synthesizeMouseAtCenter(aEditableDiv, {button: 1, ctrlKey: true}); + if (!navigator.appVersion.includes("Android")) { + is(aEditableDiv.innerHTML, + "<blockquote type=\"cite\"><p>abc</p><p>def</p><p>ghi</p></blockquote>", + "Pasted HTML content should be set to the <blockquote>"); + } else { + // Oddly, on Android, we use <br> elements for pasting <p> elements. + is(aEditableDiv.innerHTML, + "<blockquote type=\"cite\">abc<br><br>def<br><br>ghi</blockquote>", + "Pasted HTML content should be set to the <blockquote>"); + } + // On windows, HTML clipboard includes extra data. + // The values are from widget/windows/nsDataObj.cpp. + const kHTMLPrefix = (navigator.platform.includes("Win")) ? kTextHtmlPrefixClipboardDataWindows : ""; + const kHTMLPostfix = (navigator.platform.includes("Win")) ? kTextHtmlSuffixClipboardDataWindows : ""; + is(beforeInputEvents.length, 1, + 'One "beforeinput" event should be fired when pasting HTML'); + checkInputEvent(beforeInputEvents[0], "insertFromPasteAsQuotation", null, + [{type: "text/html", + data: `${kHTMLPrefix}<p>abc</p><p>def</p><p>ghi</p>${kHTMLPostfix}`}], + selectionRanges, + "when pasting HTML"); + is(inputEvents.length, 1, + 'One "input" event should be fired when pasting HTML'); + checkInputEvent(inputEvents[0], "insertFromPasteAsQuotation", null, + [{type: "text/html", + data: `${kHTMLPrefix}<p>abc</p><p>def</p><p>ghi</p>${kHTMLPostfix}`}], + [], + "when pasting HTML"); + aEditableDiv.innerHTML = ""; + + aEditableDiv.removeEventListener("beforeinput", onBeforeInput); + aEditableDiv.removeEventListener("input", onInput); +} + +async function doNestedEditorTests(aEditableDiv) { + await copyPlaintext("CLIPBOARD TEXT"); + aEditableDiv.innerHTML = '<p id="p">foo</p><textarea id="textarea"></textarea>'; + aEditableDiv.focus(); + let textarea = document.getElementById("textarea"); + let pasteTarget = null; + function onPaste(aEvent) { + pasteTarget = aEvent.target; + } + document.addEventListener("paste", onPaste); + + synthesizeMouseAtCenter(textarea, {button: 1}); + is(pasteTarget.getAttribute("id"), "textarea", + "Target of 'paste' event should be the clicked <textarea>"); + is(textarea.value, "CLIPBOARD TEXT", + "Clicking in <textarea> in an editable <div> should paste the clipboard text into the <textarea>"); + is(aEditableDiv.innerHTML, '<p id="p">foo</p><textarea id="textarea"></textarea>', + "Pasting in the <textarea> shouldn't be handled by the HTMLEditor"); + + textarea.value = ""; + textarea.readOnly = true; + pasteTarget = null; + synthesizeMouseAtCenter(textarea, {button: 1}); + is(pasteTarget, textarea, + "Target of 'paste' event should be the clicked <textarea> even if it's read-only"); + is(textarea.value, "", + "Clicking in read-only <textarea> in an editable <div> should not paste the clipboard text into the read-only <textarea>"); + // HTMLEditor thinks that read-only <textarea> is not modifiable. + // Therefore, HTMLEditor does not paste the text. + is(aEditableDiv.innerHTML, '<p id="p">foo</p><textarea id="textarea" readonly=""></textarea>', + "Clicking in read-only <textarea> shouldn't cause pasting the clipboard text into its parent HTMLEditor"); + + textarea.value = ""; + textarea.readOnly = false; + textarea.disabled = true; + pasteTarget = null; + synthesizeMouseAtCenter(textarea, {button: 1}); + // Although, this compares with <textarea>, I'm not sure it's proper event + // target because of disabled <textarea>. + todo_is(pasteTarget, textarea, + "Target of 'paste' event should be the clicked <textarea> even if it's disabled"); + is(textarea.value, "", + "Clicking in disabled <textarea> in an editable <div> should not paste the clipboard text into the disabled <textarea>"); + // HTMLEditor thinks that disabled <textarea> is not modifiable. + // Therefore, HTMLEditor does not paste the text. + is(aEditableDiv.innerHTML, '<p id="p">foo</p><textarea id="textarea" disabled=""></textarea>', + "Clicking in disabled <textarea> shouldn't cause pasting the clipboard text into its parent HTMLEditor"); + + document.removeEventListener("paste", onPaste); + aEditableDiv.innerHTML = ""; +} + +async function doAfterRemoveOfClickedElementTest(aEditableDiv) { + await copyPlaintext("CLIPBOARD TEXT"); + aEditableDiv.innerHTML = '<p id="p">foo<span id="span">bar</span></p>'; + aEditableDiv.focus(); + let span = document.getElementById("span"); + let pasteTarget = null; + document.addEventListener("paste", (aEvent) => { pasteTarget = aEvent.target; }, {once: true}); + document.addEventListener("auxclick", (aEvent) => { + is(aEvent.target.getAttribute("id"), "span", + "Target of auxclick event should be the <span> element"); + span.parentElement.removeChild(span); + }, {once: true}); + synthesizeMouseAtCenter(span, {button: 1}); + is(pasteTarget.getAttribute("id"), "p", + "Target of 'paste' event should be the <p> element since <span> has gone"); + // XXX Currently, pasted to start of the <p> because EventStateManager + // do not recompute event target frame. + todo_is(aEditableDiv.innerHTML, '<p id="p">fooCLIPBOARD TEXT</p>', + "Clipbpard text should looks like replacing the <span> element"); + aEditableDiv.innerHTML = ""; +} + +async function doNotStartAutoscrollInContentEditable(aEditableDiv) { + await SpecialPowers.pushPrefEnv({"set": [["general.autoScroll", true]]}); + await copyPlaintext("CLIPBOARD TEXT"); + aEditableDiv.innerHTML = '<p id="p">foo<span id="span">bar</span></p>'; + aEditableDiv.focus(); + let span = document.getElementById("span"); + synthesizeMouseAtCenter(span, {button: 1}); + ok(aEditableDiv.innerHTML.includes("CLIPBOARD TEXT"), + "Clipbpard text should be inserted"); + aEditableDiv.innerHTML = ""; +} + +async function doTests() { + await SpecialPowers.pushPrefEnv({"set": [["middlemouse.paste", true], + ["middlemouse.contentLoadURL", false], + ["dom.event.clipboardevents.enabled", true]]}); + let container = document.getElementById("container"); + container.innerHTML = "<textarea id=\"editor\"></textarea>"; + await doTextareaTests(document.getElementById("editor")); + container.innerHTML = "<div id=\"editor\" contenteditable style=\"min-height: 1em;\"></div>"; + await doContenteditableTests(document.getElementById("editor")); + await doNestedEditorTests(document.getElementById("editor")); + await doAfterRemoveOfClickedElementTest(document.getElementById("editor")); + // NOTE: The following test sets `general.autoScroll` to true. + await doNotStartAutoscrollInContentEditable(document.getElementById("editor")); + SimpleTest.finish(); +} + +SimpleTest.waitForFocus(doTests); +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_native_key_bindings_in_shadow.html b/editor/libeditor/tests/test_native_key_bindings_in_shadow.html new file mode 100644 index 0000000000..d29fb5f13f --- /dev/null +++ b/editor/libeditor/tests/test_native_key_bindings_in_shadow.html @@ -0,0 +1,50 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<title>Check whether native key bindings work in shadow DOM in editor</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +<script> +"use strict"; + +const kIsMac = navigator.platform.includes("Mac"); + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + const shadowHost = document.querySelector("div"); + const shadowRoot = shadowHost.attachShadow({mode: "open"}); + shadowRoot.innerHTML = "<div contenteditable>abc def</div>"; + const editingHost = shadowRoot.querySelector("div[contenteditable]"); + editingHost.focus(); + getSelection().collapse(editingHost.firstChild, "abc ".length); + if (kIsMac) { + synthesizeKey("KEY_ArrowLeft", {metaKey: true}); + } else { + synthesizeKey("KEY_Home"); + } + synthesizeKey("X", {shiftKey: true}); + is( + editingHost.textContent, + "Xabc def", + "X should've insert start of the editing host after typing \"Home\"" + ); + if (kIsMac) { + synthesizeKey("KEY_ArrowRight", {metaKey: true}); + } else { + synthesizeKey("KEY_End"); + } + synthesizeKey("Y", {shiftKey: true}); + is( + editingHost.textContent, + "Xabc defY", + "Y should've been inserted end of the editing host after typing \"End\"" + ); + + SimpleTest.finish(); +}); +</script> +</head> +<body><div></div></body> +</html> diff --git a/editor/libeditor/tests/test_nested_editor.html b/editor/libeditor/tests/test_nested_editor.html new file mode 100644 index 0000000000..aeb0c115c1 --- /dev/null +++ b/editor/libeditor/tests/test_nested_editor.html @@ -0,0 +1,77 @@ +<!DOCTYPE html> +<html> +<head> + <title> Test for nested contenteditable elements </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> + <template id="focus-iframe-contenteditable-in-div"> + <div contenteditable> + <iframe srcdoc="<div id='focusme' contenteditable></div>"></iframe> + </div> + </template> + + <template id="focus-contenteditable-parent-along-with-iframe"> + <div id='focusme' contenteditable></div> + <iframe srcdoc="<div contenteditable></div>"></iframe> + </template> + + <template id="focus-iframe-textarea-in-div"> + <div contenteditable> + <iframe srcdoc="<textarea id='focusme'></textarea>"></iframe> + </div> + </template> + + <template id="focus-textarea-parent-along-with-iframe"> + <textarea id='focusme' contenteditable></textarea> + <iframe srcdoc="<div contenteditable></div>"></iframe> + </template> +<script> +"use strict"; + +async function runTest() { + function findFocusme() { + return new Promise(r => { + let focusInParent = document.getElementById("focusme"); + if (focusInParent) { + r(focusInParent); + return; + } + document.querySelector("iframe").addEventListener("load", function() { + return r(document.querySelector("iframe").contentDocument.getElementById("focusme")); + }); + }); + } + + const focusme = await findFocusme(); + + focusme.focus(); + synthesizeKey("abc"); + + if (focusme.nodeName === "TEXTAREA") { + is(focusme.value, "abc"); + } else { + is(focusme.innerHTML, "abc"); + } +} + +SimpleTest.waitForExplicitFinish(); + +SimpleTest.waitForFocus(async () => { + for (const template of document.querySelectorAll("template")) { + const content = template.content.cloneNode(true); + document.body.appendChild(content); + + await runTest(); + + document.body.innerHTML = ""; + } + + SimpleTest.finish(); +}); + +</script> +</body> +</html> diff --git a/editor/libeditor/tests/test_new_plaintext_mail_with_plaintext_signature.html b/editor/libeditor/tests/test_new_plaintext_mail_with_plaintext_signature.html new file mode 100644 index 0000000000..327954aedb --- /dev/null +++ b/editor/libeditor/tests/test_new_plaintext_mail_with_plaintext_signature.html @@ -0,0 +1,94 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<title></title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +<script> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(async () => { + const iframe = document.querySelector("iframe"); + const doc = iframe.contentDocument; + const win = iframe.contentWindow; + doc.designMode = "on"; + // Ensure focus + await new Promise(resolve => { + doc.addEventListener("focus", resolve, {once: true}); + iframe.focus(); + }); + + function getHTMLEditor() { + const editingSession = SpecialPowers.wrap(win).docShell.editingSession; + if (!editingSession) { + return null; + } + const editor = editingSession.getEditorForWindow(win); + if (!editor) { + return null; + } + return editor.QueryInterface(SpecialPowers.Ci.nsIHTMLEditor); + } + + const nsIEditor = SpecialPowers.Ci.nsIEditor; + const htmlEditor = getHTMLEditor(); + // https://searchfox.org/comm-central/rev/e5e6e24d4ff9d91a457f7252ec7146169fefc5a2/mozilla/editor/composer/nsEditingSession.cpp#300-302 + htmlEditor.flags |= + nsIEditor.eEditorPlaintextMask + | nsIEditor.eEditorMailMask + | nsIEditor.eEditorEnableWrapHackMask; + + // https://searchfox.org/comm-central/rev/e5e6e24d4ff9d91a457f7252ec7146169fefc5a2/mail/components/compose/content/MsgComposeCommands.js#10984,10986-10987,10989 + doc.execCommand("defaultParagraphSeparator", false, "br"); + + // https://searchfox.org/comm-central/rev/e5e6e24d4ff9d91a457f7252ec7146169fefc5a2/mailnews/compose/src/nsMsgCompose.cpp#700 + win.getSelection().collapse(doc.body, doc.body.childNodes.length); + is( + doc.body.innerHTML, + "<br>", + "Initially, the <body> should have a <br>" + ); + + // https://searchfox.org/comm-central/rev/e5e6e24d4ff9d91a457f7252ec7146169fefc5a2/mailnews/compose/src/nsMsgCompose.cpp#705 + htmlEditor.insertLineBreak(); + is( + doc.body.innerHTML, + "<br><br>", + "Insert line break in the <body> should insert 2 <br> elements" + ); + is( + win.getSelection().focusNode, + doc.body, + "Selection container should be the <body> after the call of insertLineBreak()" + ); + is( + win.getSelection().focusOffset, + 1, + "Selection should be collapsed after the first <br> after the call of insertLineBreak()" + ); + + // https://searchfox.org/comm-central/rev/e5e6e24d4ff9d91a457f7252ec7146169fefc5a2/mailnews/compose/src/nsMsgCompose.cpp#706 + // https://searchfox.org/comm-central/rev/e5e6e24d4ff9d91a457f7252ec7146169fefc5a2/mailnews/compose/src/nsMsgCompose.cpp#371-372 + const wrapperDiv = htmlEditor.createElementWithDefaults("div"); + // https://searchfox.org/comm-central/rev/e5e6e24d4ff9d91a457f7252ec7146169fefc5a2/mailnews/compose/src/nsMsgCompose.cpp#390-391,394,401-402,404 + wrapperDiv.appendChild(doc.createTextNode("-- ")); + wrapperDiv.appendChild(htmlEditor.createElementWithDefaults("br")); + // https://searchfox.org/comm-central/rev/e5e6e24d4ff9d91a457f7252ec7146169fefc5a2/mailnews/compose/src/nsMsgCompose.cpp#390-391,394,401-402,404 + wrapperDiv.appendChild(doc.createTextNode("Plaintext signature")); + wrapperDiv.appendChild(htmlEditor.createElementWithDefaults("br")); + // https://searchfox.org/comm-central/rev/e5e6e24d4ff9d91a457f7252ec7146169fefc5a2/mailnews/compose/src/nsMsgCompose.cpp#414 + htmlEditor.insertElementAtSelection(wrapperDiv, true); + + is( + doc.body.innerHTML.trim(), + "<br><div>-- <br>Plaintext signature<br></div><br>", + "The signature block should follow a <br>" + ); + + SimpleTest.finish(); +}); +</script> +</head> +<body><iframe srcdoc='<body style="font-family: -moz-fixed; white-space: pre-wrap; width: 72ch;"></body>'></iframe></body> +</html> diff --git a/editor/libeditor/tests/test_nsIEditorMailSupport_insertAsCitedQuotation.html b/editor/libeditor/tests/test_nsIEditorMailSupport_insertAsCitedQuotation.html new file mode 100644 index 0000000000..531a8c6015 --- /dev/null +++ b/editor/libeditor/tests/test_nsIEditorMailSupport_insertAsCitedQuotation.html @@ -0,0 +1,322 @@ +<!DOCTYPE> +<html> +<head> + <title>Test for nsIEditorMailSupport.insertAsCitedQuotation()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> +<div contenteditable></div> +<script> +"use strict"; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + function testInputEvents() { + const inReadonlyMode = getEditor().flags & SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask; + const editorDescription = `(readonly=${!!inReadonlyMode})`; + + const editor = document.querySelector("div[contenteditable]"); + const selection = getSelection(); + + let beforeInputEvents = []; + let inputEvents = []; + let selectionRanges = []; + function onBeforeInput(aEvent) { + beforeInputEvents.push(aEvent); + selectionRanges = []; + for (let i = 0; i < selection.rangeCount; i++) { + let range = selection.getRangeAt(i); + selectionRanges.push({startContainer: range.startContainer, startOffset: range.startOffset, + endContainer: range.endContainer, endOffset: range.endOffset}); + } + } + function onInput(aEvent) { + inputEvents.push(aEvent); + } + editor.addEventListener("beforeinput", onBeforeInput); + editor.addEventListener("input", onInput); + + editor.innerHTML = ""; + editor.focus(); + selection.collapse(editor, 0); + + function checkInputEvent(aEvent, aInputType, aData, aDescription) { + ok(aEvent instanceof InputEvent, + `"${aEvent.type}" event should be dispatched with InputEvent interface ${aDescription}`); + // If it were cancelable whose inputType is empty string, web apps would + // block any Firefox specific modification whose inputType are not declared + // by the spec. + let expectedCancelable = aEvent.type === "beforeinput" && aInputType !== ""; + is(aEvent.cancelable, expectedCancelable, + `"${aEvent.type}" event should ${expectedCancelable ? "be" : "be never"} cancelable ${aDescription}`); + is(aEvent.bubbles, true, + `"${aEvent.type}" event should always bubble ${aDescription}`); + is(aEvent.inputType, aInputType, + `inputType of "${aEvent.type}" event should be "${aInputType}" ${aDescription}`); + is(aEvent.data, aData, + `data of "${aEvent.type}" event should be ${aData} ${aDescription}`); + is(aEvent.dataTransfer, null, + `dataTransfer of "${aEvent.type}" event should be null ${aDescription}`); + let targetRanges = aEvent.getTargetRanges(); + if (aEvent.type === "beforeinput") { + is(targetRanges.length, selectionRanges.length, + `getTargetRanges() of "beforeinput" event should return selection ranges ${aDescription}`); + if (targetRanges.length === selectionRanges.length) { + for (let i = 0; i < selectionRanges.length; i++) { + is(targetRanges[i].startContainer, selectionRanges[i].startContainer, + `startContainer of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`); + is(targetRanges[i].startOffset, selectionRanges[i].startOffset, + `startOffset of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`); + is(targetRanges[i].endContainer, selectionRanges[i].endContainer, + `endContainer of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`); + is(targetRanges[i].endOffset, selectionRanges[i].endOffset, + `endOffset of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`); + } + } + } else { + is(targetRanges.length, 0, + `getTargetRanges() of "${aEvent.type}" event should return empty array ${aDescription}`); + } + } + + // Tests when the editor is in plaintext mode. + + getEditor().flags |= SpecialPowers.Ci.nsIEditor.eEditorPlaintextMask; + + beforeInputEvents = []; + inputEvents = []; + getEditorMailSupport().insertAsCitedQuotation("this is quoted text\nAnd here is second line.", "this is cited text", false); + + ok( + selection.isCollapsed, + `Selection should be collapsed after calling nsIEditorMailSupport.insertAsCitedQuotation() of plaintext editor ${editorDescription}` + ); + is( + selection.focusNode, + editor, + `focus node of Selection should be a child of the editing host after calling nsIEditorMailSupport.insertAsCitedQuotation() of plaintext editor ${editorDescription}` + ); + is( + selection.focusOffset, + 1, + `focus offset of Selection should be next to inserted <span> element after calling nsIEditorMailSupport.insertAsCitedQuotation() of plaintext editor ${editorDescription}` + ); + is( + editor.innerHTML, + '<span style="white-space: pre-wrap;">> this is quoted text<br>> And here is second line.<br><br></span>', + `The quoted text should be inserted as plaintext into the plaintext editor ${editorDescription}` + ); + is( + beforeInputEvents.length, + 1, + `One "beforeinput" event should be fired on the editing host after calling nsIEditorMailSupport.insertAsCitedQuotation() of plaintext editor ${editorDescription}` + ); + checkInputEvent( + beforeInputEvents[0], + "insertText", "this is quoted text\nAnd here is second line.", + `after calling nsIEditorMailSupport.insertAsCitedQuotation() of plaintext editor ${editorDescription}` + ); + is( + inputEvents.length, + 1, + `One "input" event should be fired on the editing host after calling nsIEditorMailSupport.insertAsCitedQuotation() of plaintext editor ${editorDescription}` + ); + checkInputEvent( + inputEvents[0], + "insertText", "this is quoted text\nAnd here is second line.", + `after calling nsIEditorMailSupport.insertAsCitedQuotation() of plaintext editor ${editorDescription}` + ); + + // Tests when the editor is in HTML editor mode. + getEditor().flags &= ~SpecialPowers.Ci.nsIEditor.eEditorPlaintextMask; + + editor.innerHTML = ""; + + beforeInputEvents = []; + inputEvents = []; + getEditorMailSupport().insertAsCitedQuotation("this is quoted text<br>", "this is cited text", false); + + ok( + selection.isCollapsed, + `Selection should be collapsed after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as plaintext) ${editorDescription}` + ); + is( + selection.focusNode, + editor, + `focus node of Selection should be a child of the editing host after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as plaintext) ${editorDescription}` + ); + is( + selection.focusOffset, + 1, + `focus offset of Selection should be next to inserted <span> element after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as plaintext) ${editorDescription}` + ); + is( + editor.innerHTML, + '<blockquote type="cite" cite="this is cited text">this is quoted text<br></blockquote>', + `The quoted text should be inserted as plaintext into the HTML editor ${editorDescription}` + ); + is( + beforeInputEvents.length, + 1, + `One "beforeinput" event should be fired on the editing host after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as plaintext) ${editorDescription}` + ); + checkInputEvent( + beforeInputEvents[0], + "", + null, + `after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as plaintext) ${editorDescription}` + ); + is( + inputEvents.length, + 1, + `One "input" event should be fired on the editing host after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as plaintext) ${editorDescription}` + ); + checkInputEvent( + inputEvents[0], + "", + null, + `after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as plaintext) ${editorDescription}` + ); + + editor.innerHTML = ""; + + beforeInputEvents = []; + inputEvents = []; + getEditorMailSupport().insertAsCitedQuotation("this is quoted text<br>And here is second line.", "this is cited text", true); + + ok( + selection.isCollapsed, + `Selection should be collapsed after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as HTML source) ${editorDescription}` + ); + is( + selection.focusNode, + editor, + `focus node of Selection should be a child of the editing host after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as HTML source) ${editorDescription}` + ); + is( + selection.focusOffset, + 1, + `focus offset of Selection should be next to inserted <span> element after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as HTML source) ${editorDescription}` + ); + is( + editor.innerHTML, + '<blockquote type="cite" cite="this is cited text">this is quoted text<br>And here is second line.</blockquote>', + `The quoted text should be inserted as HTML source into the HTML editor ${editorDescription}` + ); + is( + beforeInputEvents.length, + 1, + `One "beforeinput" event should be fired on the editing host after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as HTML source) ${editorDescription}` + ); + checkInputEvent( + beforeInputEvents[0], + "", + null, + `after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as HTML source) ${editorDescription}` + ); + is( + inputEvents.length, + 1, + `One "input" event should be fired on the editing host after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as HTML source) ${editorDescription}` + ); + checkInputEvent( + inputEvents[0], + "", + null, + `after calling nsIEditorMailSupport.insertAsCitedQuotation() of HTMLEditor editor (inserting as HTML source) ${editorDescription}` + ); + + editor.removeEventListener("beforeinput", onBeforeInput); + editor.removeEventListener("input", onInput); + } + + function testStyleOfPlaintextMode() { + const inReadonlyMode = getEditor().flags & SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask; + const editorDescription = `(readonly=${!!inReadonlyMode})`; + + getEditor().flags |= SpecialPowers.Ci.nsIEditor.eEditorPlaintextMask; + + (function testInDiv() { + const editor = document.querySelector("div[contenteditable]"); + editor.innerHTML = ""; + editor.focus(); + getEditorMailSupport().insertAsCitedQuotation( + "this is quoted text.", + "this is cited text", + false + ); + is( + editor.firstChild.tagName, + "SPAN", + `testStyleOfPlaintextMode: testInDiv: insertAsCitedQuotation should insert a <span> element ${editorDescription}` + ); + const computedSpanStyle = getComputedStyle(editor.firstChild); + is( + computedSpanStyle.display, + "inline", + `testStyleOfPlaintextMode: testInDiv: The inserted <span> element should be "display: inline;" ${editorDescription}` + ); + is( + computedSpanStyle.whiteSpace, + "pre-wrap", + `testStyleOfPlaintextMode: testInDiv: The inserted <span> element should be "white-space: pre-wrap;" ${editorDescription}` + ); + })(); + + try { + document.body.contentEditable = true; + (function testInBody() { + getSelection().collapse(document.body, 0); + getEditorMailSupport().insertAsCitedQuotation( + "this is quoted text.", + "this is cited text", + false + ); + is( + document.body.firstChild.tagName, + "SPAN", + `testStyleOfPlaintextMode: testInBody: insertAsCitedQuotation should insert a <span> element in plaintext mode and in a <body> element ${editorDescription}` + ); + const computedSpanStyle = getComputedStyle(document.body.firstChild); + is( + computedSpanStyle.display, + "block", + `testStyleOfPlaintextMode: testInBody: The inserted <span> element should be "display: block;" in plaintext mode and in a <body> element ${editorDescription}` + ); + is( + computedSpanStyle.whiteSpace, + "pre-wrap", + `testStyleOfPlaintextMode: testInBody: The inserted <span> element should be "white-space: pre-wrap;" in plaintext mode and in a <body> element ${editorDescription}` + ); + document.body.firstChild.remove(); + })(); + } finally { + document.body.contentEditable = false; + } + } + + testInputEvents(); + testStyleOfPlaintextMode(); + + // Even if the HTMLEditor is readonly, XPCOM API should keep working. + getEditor().flags |= SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask; + testInputEvents(); + testStyleOfPlaintextMode(); + + SimpleTest.finish(); +}); + +function getEditor() { + var editingSession = SpecialPowers.wrap(window).docShell.editingSession; + return editingSession.getEditorForWindow(window); +} + +function getEditorMailSupport() { + return getEditor().QueryInterface(SpecialPowers.Ci.nsIEditorMailSupport); +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_nsIEditorMailSupport_insertTextWithQuotations.html b/editor/libeditor/tests/test_nsIEditorMailSupport_insertTextWithQuotations.html new file mode 100644 index 0000000000..921e137fa8 --- /dev/null +++ b/editor/libeditor/tests/test_nsIEditorMailSupport_insertTextWithQuotations.html @@ -0,0 +1,104 @@ +<!DOCTYPE> +<html> +<head> + <title>Test for nsIEditorMailSupport.insertTextWithQuotations()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> +<div contenteditable></div> +<iframe srcdoc="<body contenteditable></body>"></iframe> +<script> +"use strict"; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(async () => { + const iframe = document.querySelector("iframe"); + await new Promise(resolve => { + if (iframe.contentDocument?.readyState == "complete") { + resolve(); + return; + } + iframe.addEventListener("load", resolve, {once: true}); + }); + + function testInDiv() { + const inPlaintextMode = getEditor(window).flags & SpecialPowers.Ci.nsIEditor.eEditorPlaintextMask; + const inReadonlyMode = getEditor(window).flags & SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask; + const editorDescription = `(readonly=${!!inReadonlyMode}, plaintext=${!!inPlaintextMode})`; + const editor = document.querySelector("div[contenteditable]"); + editor.innerHTML = ""; + editor.focus(); + getEditorMailSupport(window).insertTextWithQuotations( + "This is Text\n\n> This is a quote." + ); + is( + editor.innerHTML, + 'This is Text<br><br><span style="white-space: pre-wrap;">> This is a quote.</span><br>', + `The <div contenteditable> should have the expected innerHTML ${editorDescription}` + ); + is( + editor.querySelector("span")?.getAttribute("_moz_quote"), + "true", + `The <span> element in the <div contenteditable> should have _moz_quote="true" ${editorDescription}` + ); + } + + function testInBody() { + const inPlaintextMode = getEditor(iframe.contentWindow).flags & SpecialPowers.Ci.nsIEditor.eEditorPlaintextMask; + const inReadonlyMode = getEditor(iframe.contentWindow).flags & SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask; + const editorDescription = `(readonly=${!!inReadonlyMode}, plaintext=${!!inPlaintextMode})`; + const editor = iframe.contentDocument.body; + editor.innerHTML = ""; + iframe.contentWindow.getSelection().collapse(document.body, 0); + getEditorMailSupport(iframe.contentWindow).insertTextWithQuotations( + "This is Text\n\n> This is a quote." + ); + is( + editor.innerHTML, + 'This is Text<br><br><span style="white-space: pre-wrap; display: block; width: 98vw;">> This is a quote.</span><br>', + `The <body> should have the expected innerHTML ${editorDescription}` + ); + is( + editor.querySelector("span")?.getAttribute("_moz_quote"), + "true", + `The <span> element in the <body> should have _moz_quote="true" ${editorDescription}` + ); + } + + for (const testReadOnly of [false, true]) { + // Even if the HTMLEditor is readonly, XPCOM API should keep working. + if (testReadOnly) { + getEditor(window).flags |= SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask; + getEditor(iframe.contentWindow).flags |= SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask; + } else { + getEditor(window).flags &= ~SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask; + getEditor(iframe.contentWindow).flags &= ~SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask; + } + + getEditor(window).flags &= ~SpecialPowers.Ci.nsIEditor.eEditorPlaintextMask; + getEditor(iframe.contentWindow).flags &= ~SpecialPowers.Ci.nsIEditor.eEditorPlaintextMask; + testInDiv(); + testInBody(); + + getEditor(window).flags |= SpecialPowers.Ci.nsIEditor.eEditorPlaintextMask; + getEditor(iframe.contentWindow).flags |= SpecialPowers.Ci.nsIEditor.eEditorPlaintextMask; + testInDiv(); + testInBody(); + } + + SimpleTest.finish(); +}); + +function getEditor(aWindow) { + const editingSession = SpecialPowers.wrap(aWindow).docShell.editingSession; + return editingSession.getEditorForWindow(aWindow); +} + +function getEditorMailSupport(aWindow) { + return getEditor(aWindow).QueryInterface(SpecialPowers.Ci.nsIEditorMailSupport); +} +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_nsIEditor_beginningOfDocument.html b/editor/libeditor/tests/test_nsIEditor_beginningOfDocument.html new file mode 100644 index 0000000000..119f35dbdd --- /dev/null +++ b/editor/libeditor/tests/test_nsIEditor_beginningOfDocument.html @@ -0,0 +1,119 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Tests of nsIEditor#beginningOfDocument()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + SimpleTest.waitForFocus(() => { + const originalBody = document.body.innerHTML; + + (function test_with_text_editor() { + for (const test of [ + { + tag: "input", + innerHTML: "<input>", + }, + { + tag: "textarea", + innerHTML: "<textarea></textarea>", + }, + ]) { + document.body.innerHTML = test.innerHTML; + const textControl = document.body.querySelector(test.tag); + const editor = SpecialPowers.wrap(textControl).editor; + editor.beginningOfDocument(); + is(textControl.selectionStart, 0, + `nsIEditor.beginningOfDocument() should set selectionStart of empty <${test.tag}> to 0`); + is(textControl.selectionEnd, 0, + `nsIEditor.beginningOfDocument() should set selectionEnd of empty <${test.tag}> to 0`); + textControl.value = "abc"; + textControl.selectionStart = 2; + textControl.selectionEnd = 3; + editor.beginningOfDocument(); + is(textControl.selectionStart, 0, + `nsIEditor.beginningOfDocument() should set selectionStart of non-empty <${test.tag}> to 0`); + is(textControl.selectionEnd, 0, + `nsIEditor.beginningOfDocument() should set selectionEnd of non-empty <${test.tag}> to 0`); + } + })(); + + function getHTMLEditor() { + const editingSession = SpecialPowers.wrap(window).docShell.editingSession; + if (!editingSession) { + return null; + } + return editingSession.getEditorForWindow(window); + } + + (function test_with_contenteditable() { + document.body.innerHTML = "<div contenteditable><p>abc</p></div>"; + getSelection().removeAllRanges(); + getHTMLEditor().beginningOfDocument(); + is(getSelection().rangeCount, 0, + "selection shouldn't be changed when there is no selection"); + getSelection().setBaseAndExtent(document.body.querySelector("p").firstChild, 1, + document.body.querySelector("p").firstChild, 3); + getHTMLEditor().beginningOfDocument(); + is(getSelection().isCollapsed, true, + "selection should be collapsed after calling nsIEditor.beginningOfDocument() with contenteditable having one paragraph"); + is(getSelection().rangeCount, 1, + "selection should has only one range after calling nsIEditor.beginningOfDocument() with contenteditable having one paragraph"); + is(getSelection().focusNode, document.body.querySelector("p").firstChild, + "selection should be collapsed into the text node after calling nsIEditor.beggingOfDocument() with content editable having one paragraph"); + is(getSelection().focusOffset, 0, + "selection should be collapsed to start of the text node after calling nsIEditor.beggingOfDocument() with content editable having one paragraph"); + + document.body.innerHTML = "<div contenteditable><p contenteditable=\"false\">abc</p><p>def</p></div>"; + getSelection().setBaseAndExtent(document.body.querySelector("p + p").firstChild, 1, + document.body.querySelector("p + p").firstChild, 3); + getHTMLEditor().beginningOfDocument(); + is(getSelection().isCollapsed, true, + "selection should be collapsed after calling nsIEditor.beginningOfDocument() with contenteditable having non-editable paragraph first"); + is(getSelection().rangeCount, 1, + "selection should has only one range after calling nsIEditor.beginningOfDocument() with contenteditable having non-editable paragraph first"); + is(getSelection().focusNode, document.body.querySelector("div[contenteditable]"), + "selection should be collapsed to start of the editing host after calling nsIEditor.beggingOfDocument() with content editable having non-editable paragraph first"); + is(getSelection().focusOffset, 0, + "selection should be collapsed to start of the editing host after calling nsIEditor.beggingOfDocument() with content editable having non-editable paragraph first"); + + document.body.innerHTML = "<div contenteditable>\n<p contenteditable=\"false\">abc</p>\n<p>def</p>\n</div>"; + getSelection().setBaseAndExtent(document.body.querySelector("p + p").firstChild, 1, + document.body.querySelector("p + p").firstChild, 3); + getHTMLEditor().beginningOfDocument(); + is(getSelection().isCollapsed, true, + "selection should be collapsed after calling nsIEditor.beginningOfDocument() with contenteditable having non-editable paragraph first and some invisible line breaks"); + is(getSelection().rangeCount, 1, + "selection should has only one range after calling nsIEditor.beginningOfDocument() with contenteditable having non-editable paragraph first and some invisible line breaks"); + is(getSelection().focusNode, document.body.querySelector("div[contenteditable]"), + "selection should be collapsed to start of the editing host after calling nsIEditor.beggingOfDocument() with content editable having non-editable paragraph first and some invisible line breaks"); + is(getSelection().focusOffset, 0, + "selection should be collapsed to start of the editing host after calling nsIEditor.beggingOfDocument() with content editable having non-editable paragraph first and some invisible line breaks"); + + document.body.innerHTML = "<div contenteditable><p>abc</p></div>def<div contenteditable><p>ghi</p></div>"; + getSelection().setBaseAndExtent(document.body.querySelector("div + div > p").firstChild, 1, + document.body.querySelector("div + div > p").firstChild, 3); + getHTMLEditor().beginningOfDocument(); + is(getSelection().isCollapsed, true, + "selection should be collapsed after calling nsIEditor.beginningOfDocument() with the 2nd contenteditable"); + is(getSelection().rangeCount, 1, + "selection should has only one range after calling nsIEditor.beginningOfDocument() with the 2nd contenteditable"); + is(getSelection().focusNode, document.body.querySelector("div + div > p").firstChild, + "selection should be collapsed to start of the first text node in the second editing host after calling nsIEditor.beggingOfDocument() with the 2nd contenteditable"); + is(getSelection().focusOffset, 0, + "selection should be collapsed to start of the first text node in the second editing host after calling nsIEditor.beggingOfDocument() with the 2nd contenteditable"); + })(); + + document.body.innerHTML = originalBody; + SimpleTest.finish(); + }); + </script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_nsIEditor_canUndo_canRedo.html b/editor/libeditor/tests/test_nsIEditor_canUndo_canRedo.html new file mode 100644 index 0000000000..fb0993daab --- /dev/null +++ b/editor/libeditor/tests/test_nsIEditor_canUndo_canRedo.html @@ -0,0 +1,183 @@ +<!DOCTYPE html> +<html> +<head> +<title>Test for nsIEditor.canUndo and nsIEditor.canRedo</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content"><input><textarea></textarea><div contenteditable></div></div> +<pre id="test"> +<script> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + function isTextEditor(aElement) { + return aElement.tagName.toLowerCase() == "input" || + aElement.tagName.toLowerCase() == "textarea"; + } + function getEditor(aElement) { + if (isTextEditor(aElement)) { + return SpecialPowers.wrap(aElement).editor; + } + return SpecialPowers.wrap(window).docShell.editingSession?.getEditorForWindow(window); + } + function setValue(aElement, aValue) { + if (isTextEditor(aElement)) { + aElement.value = aValue; + return; + } + aElement.innerHTML = aValue; + } + function getValue(aElement) { + if (isTextEditor(aElement)) { + return aElement.value; + } + return aElement.innerHTML.replace(/<br>/g, ""); + } + for (const selector of ["input", "textarea", "div[contenteditable]"]) { + const editableElement = document.querySelector(selector); + editableElement.focus(); + const editor = getEditor(editableElement); + setValue(editableElement, ""); + is( + editor.canUndo, + false, + `Editor for ${selector} shouldn't have undo transaction at start` + ); + is( + editor.canRedo, + false, + `Editor for ${selector} shouldn't have redo transaction at start` + ); + + synthesizeKey("b"); + is( + getValue(editableElement), + "b", + `Editor for ${selector} should've handled inserting "b"` + ); + is( + editor.canUndo, + true, + `Editor for ${selector} should have undo transaction after inserting "b"` + ); + is( + editor.canRedo, + false, + `Editor for ${selector} shouldn't have redo transaction after inserting "b"` + ); + + synthesizeKey("KEY_ArrowLeft"); + synthesizeKey("a"); + is( + getValue(editableElement), + "ab", + `Editor for ${selector} should've handled inserting "a" before "b"` + ); + is( + editor.canUndo, + true, + `Editor for ${selector} should have undo transaction after inserting text again` + ); + is( + editor.canRedo, + false, + `Editor for ${selector} should have redo transaction after inserting text again` + ); + + document.execCommand("undo"); + is( + getValue(editableElement), + "b", + `Editor for ${selector} should've undone inserting "a"` + ); + is( + editor.canUndo, + true, + `Editor for ${selector} should have undo transaction for inserting "b" after undoing inserting "a"` + ); + is( + editor.canRedo, + true, + `Editor for ${selector} should have redo transaction for inserting "b" after undoing inserting "a"` + ); + + document.execCommand("undo"); + is( + getValue(editableElement), + "", + `Editor for ${selector} should've undone inserting "b"` + ); + is( + editor.canUndo, + false, + `Editor for ${selector} shouldn't have undo transaction after undoing all things` + ); + is( + editor.canRedo, + true, + `Editor for ${selector} should have redo transaction after undoing all things` + ); + + document.execCommand("redo"); + is( + getValue(editableElement), + "b", + `Editor for ${selector} should've redone inserting "b"` + ); + is( + editor.canUndo, + true, + `Editor for ${selector} should have undo transaction after redoing inserted "a"` + ); + is( + editor.canRedo, + true, + `Editor for ${selector} should have redo transaction after redoing inserted "a"` + ); + + document.execCommand("redo"); + is( + getValue(editableElement), + "ab", + `Editor for ${selector} should've redone inserting "b"` + ); + is( + editor.canUndo, + true, + `Editor for ${selector} should have undo transaction after redoing all things` + ); + is( + editor.canRedo, + false, + `Editor for ${selector} shouldn't have redo transaction for after redoing all things` + ); + + document.execCommand("undo"); + synthesizeKey("c"); + is( + getValue(editableElement), + "cb", + `Editor for ${selector} should've redone inserting "b"` + ); + is( + editor.canUndo, + true, + `Editor for ${selector} should have undo transaction after inserting another undoing once` + ); + is( + editor.canRedo, + false, + `Editor for ${selector} shouldn't have redo transaction after inserting another undoing once` + ); + } + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_nsIEditor_clearUndoRedo.html b/editor/libeditor/tests/test_nsIEditor_clearUndoRedo.html new file mode 100644 index 0000000000..695826c0d8 --- /dev/null +++ b/editor/libeditor/tests/test_nsIEditor_clearUndoRedo.html @@ -0,0 +1,125 @@ +<!DOCTYPE html> +<html> +<head> +<title>Test for nsIEditor.clearUndoRedo()</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content"><input><textarea></textarea><div contenteditable></div></div> +<pre id="test"> +<script> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + function isTextEditor(aElement) { + return aElement.tagName.toLowerCase() == "input" || + aElement.tagName.toLowerCase() == "textarea"; + } + function getEditor(aElement) { + if (isTextEditor(aElement)) { + return SpecialPowers.wrap(aElement).editor; + } + return SpecialPowers.wrap(window).docShell.editingSession?.getEditorForWindow(window); + } + function setValue(aElement, aValue) { + if (isTextEditor(aElement)) { + aElement.value = aValue; + return; + } + aElement.innerHTML = aValue; + } + function getValue(aElement) { + if (isTextEditor(aElement)) { + return aElement.value; + } + return aElement.innerHTML.replace(/<br>/g, ""); + } + for (const selector of ["input", "textarea", "div[contenteditable]"]) { + const editableElement = document.querySelector(selector); + editableElement.focus(); + const editor = getEditor(editableElement); + (function test_clearing_undo_history() { + setValue(editableElement, ""); + is( + editor.canUndo, + false, + `Editor for ${selector} shouldn't have undo transaction at start` + ); + synthesizeKey("a"); + is( + getValue(editableElement), + "a", + `Editor for ${selector} should've handled typing "a"` + ); + is( + editor.canUndo, + true, + `Editor for ${selector} should have undo transaction for the inserted text` + ); + editor.clearUndoRedo(); + is( + editor.canUndo, + false, + `Editor for ${selector} shouldn't have undo transaction after calling nsIEditor.clearUndoRedo()` + ); + document.execCommand("undo"); + is( + getValue(editableElement), + "a", + `Editor for ${selector} should do noting for document.execCommand("undo")` + ); + })(); + + (function test_clearing_redo_history() { + setValue(editableElement, ""); + is( + editor.canRedo, + false, + `Editor for ${selector} shouldn't have redo transaction at start` + ); + synthesizeKey("b"); + is( + getValue(editableElement), + "b", + `Editor for ${selector} should've handled typing "b"` + ); + is( + editor.canRedo, + false, + `Editor for ${selector} shouldn't have redo transaction after inserting text` + ); + document.execCommand("undo"); + is( + getValue(editableElement), + "", + `Editor for ${selector} should've handled the typing "b" after undoing` + ); + is( + editor.canRedo, + true, + `Editor for ${selector} should have redo transaction of inserting text` + ); + editor.clearUndoRedo(); + is( + editor.canRedo, + false, + `Editor for ${selector} shouldn't have redo transaction after calling nsIEditor.clearUndoRedo()` + ); + document.execCommand("redo"); + is( + getValue(editableElement), + "", + `Editor for ${selector} should do noting for document.execCommand("redo")` + ); + })(); + } + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_nsIEditor_deleteNode.html b/editor/libeditor/tests/test_nsIEditor_deleteNode.html new file mode 100644 index 0000000000..eacdac1c1b --- /dev/null +++ b/editor/libeditor/tests/test_nsIEditor_deleteNode.html @@ -0,0 +1,242 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<title>nsIEditor.insertNode</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +<script> +"use strict"; + +function stringifyInputEvent(aEvent) { + if (!aEvent) { + return "null"; + } + return `${aEvent.type}: { inputType=${aEvent.inputType} }`; +} + +function getRangeDescription(range) { + function getNodeDescription(node) { + if (!node) { + return "null"; + } + switch (node.nodeType) { + case Node.TEXT_NODE: + return `${node.nodeName} "${node.data}"`; + case Node.ELEMENT_NODE: + return `<${node.nodeName.toLowerCase()}>`; + default: + return `${node.nodeName}`; + } + } + if (range === null) { + return "null"; + } + if (range === undefined) { + return "undefined"; + } + return range.startContainer == range.endContainer && + range.startOffset == range.endOffset + ? `(${getNodeDescription(range.startContainer)}, ${range.startOffset})` + : `(${getNodeDescription(range.startContainer)}, ${ + range.startOffset + }) - (${getNodeDescription(range.endContainer)}, ${range.endOffset})`; +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + const editingHost = document.querySelector("div[contenteditable]"); + const editor = + SpecialPowers.wrap(window).docShell.editingSession.getEditorForWindow(window); + + editingHost.focus(); + + let events = []; + editingHost.addEventListener("input", event => events.push(event)); + + (function test_delete_node_before_selection() { + editingHost.innerHTML = "<span>abc</span><span>def</span>"; + getSelection().collapse(editingHost.querySelector("span + span").firstChild, 0); + editor.deleteNode(editingHost.querySelector("span")); + is( + editingHost.innerHTML, + "<span>def</span>", + "test_delete_node_before_selection: deleteNode() should delete the node" + ); + is( + events.length, + 1, + "test_delete_node_before_selection: Only one input event should be fired when deleteNode() deletes a node" + ); + is( + stringifyInputEvent(events[0]), + stringifyInputEvent({ type: "input", inputType: "" }), + "test_delete_node_before_selection: input event should be fired when deleting a node" + ); + is( + getRangeDescription(getSelection().getRangeAt(0)), + getRangeDescription({ + startContainer: editingHost.firstChild.firstChild, + startOffset: 0, + endContainer: editingHost.firstChild.firstChild, + endOffset: 0, + }), + "test_delete_node_before_selection: selection shouldn't be updated" + ); + })(); + + (function test_delete_node_after_selection() { + events = []; + editingHost.innerHTML = "<span>abc</span><span>def</span>"; + getSelection().collapse(editingHost.querySelector("span").firstChild, 0); + editor.deleteNode(editingHost.querySelector("span + span")); + is( + editingHost.innerHTML, + "<span>abc</span>", + "test_delete_node_after_selection: deleteNode() should delete the node" + ); + is( + events.length, + 1, + "test_delete_node_after_selection: Only one input event should be fired when deleteNode() deletes a node" + ); + is( + stringifyInputEvent(events[0]), + stringifyInputEvent({ type: "input", inputType: "" }), + "test_delete_node_after_selection: input event should be fired when deleting a node" + ); + is( + getRangeDescription(getSelection().getRangeAt(0)), + getRangeDescription({ + startContainer: editingHost.firstChild.firstChild, + startOffset: 0, + endContainer: editingHost.firstChild.firstChild, + endOffset: 0, + }), + "test_delete_node_after_selection: selection shouldn't be updated" + ); + })(); + + (function test_delete_node_containing_selection() { + events = []; + editingHost.innerHTML = "<span>abc</span><span>def</span>"; + getSelection().collapse(editingHost.querySelector("span").firstChild, 0); + editor.deleteNode(editingHost.querySelector("span")); + is( + editingHost.innerHTML, + "<span>def</span>", + "test_delete_node_containing_selection: deleteNode() should delete the node" + ); + is( + events.length, + 1, + "test_delete_node_containing_selection: Only one input event should be fired when deleteNode() deletes a node" + ); + is( + stringifyInputEvent(events[0]), + stringifyInputEvent({ type: "input", inputType: "" }), + "test_delete_node_containing_selection: input event should be fired when deleting a node" + ); + is( + getRangeDescription(getSelection().getRangeAt(0)), + getRangeDescription({ + startContainer: editingHost, + startOffset: 0, + endContainer: editingHost, + endOffset: 0, + }), + "test_delete_node_containing_selection: selection should be updated whether node was" + ); + })(); + + (function test_delete_node_containing_selection_with_preserving_selection() { + events = []; + editingHost.innerHTML = "<span>abc</span><span>def</span>"; + getSelection().collapse(editingHost.querySelector("span").firstChild, 0); + editor.deleteNode(editingHost.querySelector("span"), true); + is( + editingHost.innerHTML, + "<span>def</span>", + "test_delete_node_containing_selection_with_preserving_selection: deleteNode() should delete the node" + ); + is( + events.length, + 1, + "test_delete_node_containing_selection_with_preserving_selection: Only one input event should be fired when deleteNode() deletes a node" + ); + is( + stringifyInputEvent(events[0]), + stringifyInputEvent({ type: "input", inputType: "" }), + "test_delete_node_containing_selection_with_preserving_selection: input event should be fired when deleting a node" + ); + is( + getRangeDescription(getSelection().getRangeAt(0)), + getRangeDescription({ + startContainer: editingHost, + startOffset: 0, + endContainer: editingHost, + endOffset: 0, + }), + "test_delete_node_containing_selection_with_preserving_selection: selection should be updated whether node was" + ); + })(); + + (function test_not_preserve_selection_nested_by_beforeinput() { + editingHost.innerHTML = "<span>abc</span><span>ghi</span>"; + const span = document.createElement("span"); + span.textContent = "def"; + getSelection().collapse(editingHost, 0); + editingHost.addEventListener("beforeinput", () => { + editor.insertNode(span, editingHost, 1); + }, {once: true}); + editor.deleteNode(editingHost.querySelector("span + span"), true); + is( + editingHost.innerHTML, + "<span>abc</span><span>def</span>", + "test_not_preserve_selection_nested_by_beforeinput: both insertNode() and deleteNode() should work" + ); + is( + getRangeDescription(getSelection().getRangeAt(0)), + getRangeDescription({ + startContainer: editingHost, + startOffset: 2, + endContainer: editingHost, + endOffset: 2, + }), + "test_not_preserve_selection_nested_by_beforeinput: only insertNode() called in beforeinput listener should update selection" + ); + })(); + + (function test_not_preserve_selection_nested_by_input() { + editingHost.innerHTML = "<span>abc</span><span>ghi</span>"; + const span = document.createElement("span"); + span.textContent = "def"; + getSelection().collapse(editingHost, 0); + editingHost.addEventListener("input", () => { + editor.insertNode(span, editingHost, 1); + }, {once: true}); + editor.deleteNode(editingHost.querySelector("span + span"), true); + is( + editingHost.innerHTML, + "<span>abc</span><span>def</span>", + "test_not_preserve_selection_nested_by_input: both insertNode() and deleteNode() should work" + ); + is( + getRangeDescription(getSelection().getRangeAt(0)), + getRangeDescription({ + startContainer: editingHost, + startOffset: 2, + endContainer: editingHost, + endOffset: 2, + }), + "test_not_preserve_selection_nested_by_input: only insertNode() called in input listener should update selection" + ); + })(); + + SimpleTest.finish(); +}); + +</script> +</head> +<body><div contenteditable><br></div></body> +</html> diff --git a/editor/libeditor/tests/test_nsIEditor_documentCharacterSet.html b/editor/libeditor/tests/test_nsIEditor_documentCharacterSet.html new file mode 100644 index 0000000000..18f948fdd9 --- /dev/null +++ b/editor/libeditor/tests/test_nsIEditor_documentCharacterSet.html @@ -0,0 +1,113 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta http-equiv="Content-Type" content="text/html,charset=utf-8"> + <title>Tests of nsIEditor#documentCharacterSet</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + SimpleTest.waitForFocus(() => { + const originalBody = document.body.innerHTML; + + (function test_with_text_editor() { + for (const test of [ + { + tag: "input", + innerHTML: "<input>", + }, + { + tag: "textarea", + innerHTML: "<textarea></textarea>", + }, + ]) { + document.body.innerHTML = test.innerHTML; + const textControl = document.body.querySelector(test.tag); + const editor = SpecialPowers.wrap(textControl).editor; + try { + editor.documentCharacterSet; + ok(false, `TextEditor::GetDocumentCharacterSet() for <${test.tag}> should throw an exception`); + } catch (e) { + ok(true, `TextEditor::GetDocumentCharacterSet() for <${test.tag}> should throw an exception`); + } + try { + editor.documentCharacterSet = "windows-1252"; + ok(false, `TextEditor::SetDocumentCharacterSet() for <${test.tag}> should throw an exception`); + } catch (e) { + ok(true, `TextEditor::SetDocumentCharacterSet() for <${test.tag}> should throw an exception`); + } + } + })(); + + function getHTMLEditor(win = window) { + const editingSession = SpecialPowers.wrap(win).docShell.editingSession; + if (!editingSession) { + return null; + } + return editingSession.getEditorForWindow(win); + } + + (function test_with_contenteditable() { + document.body.innerHTML = "<div contenteditable><p>abc</p></div>"; + const editor = getHTMLEditor(); + is(editor.documentCharacterSet, "UTF-8", + "HTMLEditor::GetDocumentCharacterSet() should return \"UTF-8\""); + editor.documentCharacterSet = "windows-1252"; + is(document.querySelector("meta[http-equiv]").getAttribute("content"), "text/html,charset=windows-1252", + "HTMLEditor::SetDocumentCharacterSet() should add <meta> element whose \"http-equiv\" attribute has \"windows-1252\""); + is(editor.documentCharacterSet, "windows-1252", + "HTMLEditor::GetDocumentCharacterSet() should return \"windows-1252\" after setting the value"); + editor.documentCharacterSet = "utf-8"; + is(document.querySelector("meta[http-equiv]").getAttribute("content"), "text/html,charset=utf-8", + "HTMLEditor::SetDocumentCharacterSet() should add <meta> element whose \"http-equiv\" attribute has \"utf-8\""); + is(editor.documentCharacterSet, "UTF-8", + "HTMLEditor::GetDocumentCharacterSet() should return \"UTF-8\" after setting the value"); + })(); + + (function test_with_designMode() { + while (document.querySelector("meta")) { + document.querySelector("meta").remove(); + } + document.body.innerHTML = "<iframe></iframe>"; + const editdoc = document.querySelector("iframe").contentDocument; + editdoc.designMode = "on"; + const editor = getHTMLEditor(document.querySelector("iframe").contentWindow); + + editor.documentCharacterSet = "us-ascii"; + const metaWithHttpEquiv = editdoc.getElementsByTagName("meta")[0]; + is(metaWithHttpEquiv.getAttribute("http-equiv"), "Content-Type", + "meta element should have http-equiv"); + is(metaWithHttpEquiv.getAttribute("content"), "text/html;charset=us-ascii", + "charset should be set as us-ascii"); + + const dummyMeta = editdoc.createElement("meta"); + dummyMeta.setAttribute("name", "keywords"); + dummyMeta.setAttribute("content", "test"); + metaWithHttpEquiv.parentNode.insertBefore(dummyMeta, metaWithHttpEquiv); + + editor.documentCharacterSet = "utf-8"; + + is(dummyMeta, editdoc.getElementsByTagName("meta")[0], + "<meta> element shouldn't be touched"); + isnot(dummyMeta.getAttribute("http-equiv"), "Content-Type", + "first meta element shouldn't have http-equiv"); + + is(metaWithHttpEquiv, editdoc.getElementsByTagName("meta")[1], + "The second <meta> element should be reused"); + is(metaWithHttpEquiv.getAttribute("http-equiv"), "Content-Type", + "second meta element should have http-equiv"); + is(metaWithHttpEquiv.getAttribute("content"), "text/html;charset=utf-8", + "charset should be set as utf-8"); + })(); + + document.body.innerHTML = originalBody; + SimpleTest.finish(); + }); + </script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_nsIEditor_documentIsEmpty.html b/editor/libeditor/tests/test_nsIEditor_documentIsEmpty.html new file mode 100644 index 0000000000..49c1db78a9 --- /dev/null +++ b/editor/libeditor/tests/test_nsIEditor_documentIsEmpty.html @@ -0,0 +1,150 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Tests of nsIEditor#documentIsEmpty</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + SimpleTest.waitForFocus(() => { + const originalBody = document.body.innerHTML; + + (function test_with_text_editor() { + for (const test of [ + { + tag: "input", + innerHTML: '<input><input value="abc"><input placeholder="abc">', + }, + { + tag: "textarea", + innerHTML: '<textarea></textarea><textarea>abc</textarea><textarea placeholder="abc"></textarea>', + }, + ]) { + document.body.innerHTML = test.innerHTML; + let textControl = document.body.querySelector(test.tag); + is(SpecialPowers.wrap(textControl).editor.documentIsEmpty, true, + `nsIEditor.documentIsEmpty should be true if value of <${test.tag}> is empty by default`); + textControl.focus(); + is(SpecialPowers.wrap(textControl).editor.documentIsEmpty, true, + `nsIEditor.documentIsEmpty should be true if value of <${test.tag}> is empty by default after getting focus`); + textControl.value = "abc"; + is(SpecialPowers.wrap(textControl).editor.documentIsEmpty, false, + `nsIEditor.documentIsEmpty should be false if <${test.tag}>.value is set to non-empty string`); + textControl.value = ""; + is(SpecialPowers.wrap(textControl).editor.documentIsEmpty, true, + `nsIEditor.documentIsEmpty should be true if <${test.tag}>.value is set to empty string`); + + textControl = textControl.nextSibling; + is(SpecialPowers.wrap(textControl).editor.documentIsEmpty, false, + `nsIEditor.documentIsEmpty should be false if value of <${test.tag}> is non-empty by default`); + textControl.focus(); + is(SpecialPowers.wrap(textControl).editor.documentIsEmpty, false, + `nsIEditor.documentIsEmpty should be false if value of <${test.tag}> is non-empty by default after getting focus`); + textControl.value = "def"; + is(SpecialPowers.wrap(textControl).editor.documentIsEmpty, false, + `nsIEditor.documentIsEmpty should be false if <${test.tag}>.value is set to different non-empty string`); + textControl.value = ""; + is(SpecialPowers.wrap(textControl).editor.documentIsEmpty, true, + `nsIEditor.documentIsEmpty should be true if <${test.tag}>.value is set to empty string from non-empty string`); + + textControl = textControl.nextSibling; + is(SpecialPowers.wrap(textControl).editor.documentIsEmpty, true, + `nsIEditor.documentIsEmpty should be true if value of <${test.tag}> is empty by default (placeholder isn't empty)`); + textControl.focus(); + is(SpecialPowers.wrap(textControl).editor.documentIsEmpty, true, + `nsIEditor.documentIsEmpty should be true if value of <${test.tag}> is empty by default after getting focus (placeholder isn't empty)`); + textControl.value = "abc"; + is(SpecialPowers.wrap(textControl).editor.documentIsEmpty, false, + `nsIEditor.documentIsEmpty should be false if <${test.tag}>.value is set to non-empty string (placeholder isn't empty)`); + textControl.value = ""; + is(SpecialPowers.wrap(textControl).editor.documentIsEmpty, true, + `nsIEditor.documentIsEmpty should be true if <${test.tag}>.value is set to empty string (placeholder isn't empty)`); + } + })(); + + function getHTMLEditor() { + const editingSession = SpecialPowers.wrap(window).docShell.editingSession; + if (!editingSession) { + return null; + } + return editingSession.getEditorForWindow(window); + } + + (function test_with_contenteditable() { + document.body.innerHTML = "<div contenteditable></div>"; + try { + getHTMLEditor().documentIsEmpty; + todo(false, "nsIEditor.documentIsEmpty should throw an exception when no editing host has focus"); + } catch (e) { + ok(true, "nsIEditor.documentIsEmpty should throw an exception when no editing host has focus"); + } + document.querySelector("div[contenteditable]").focus(); + todo_is(getHTMLEditor().documentIsEmpty, true, + "nsIEditor.documentIsEmpty should be true when editing host does not have contents"); + + document.body.innerHTML = "<div contenteditable><br></div>"; + document.querySelector("div[contenteditable]").focus(); + is(getHTMLEditor().documentIsEmpty, false, + "nsIEditor.documentIsEmpty should be false when editing host has only a <br> element"); + + document.body.innerHTML = "<div contenteditable><p><br></p></div>"; + document.querySelector("div[contenteditable]").focus(); + is(getHTMLEditor().documentIsEmpty, false, + "nsIEditor.documentIsEmpty should be false when editing host has only an empty paragraph"); + + document.body.innerHTML = "<div contenteditable><p>abc</p></div>"; + document.querySelector("div[contenteditable]").focus(); + is(getHTMLEditor().documentIsEmpty, false, + "nsIEditor.documentIsEmpty should be false when editing host has text in a paragraph"); + + document.body.innerHTML = "<div contenteditable>abc</div>"; + document.querySelector("div[contenteditable]").focus(); + is(getHTMLEditor().documentIsEmpty, false, + "nsIEditor.documentIsEmpty should be false when editing host has text directly"); + + document.execCommand("selectall"); + document.execCommand("delete"); + todo_is(getHTMLEditor().documentIsEmpty, true, + "nsIEditor.documentIsEmpty should be true when all contents in editing host are deleted"); + })(); + + document.designMode = "on"; + (function test_with_designMode() { + document.body.innerHTML = ""; + is(getHTMLEditor().documentIsEmpty, true, + "nsIEditor.documentIsEmpty should be true when <body> is empty in designMode"); + document.body.focus(); + is(getHTMLEditor().documentIsEmpty, true, + "nsIEditor.documentIsEmpty should be true when <body> is empty in designMode (after setting focus explicitly)"); + + document.body.innerHTML = "<div><br></div>"; + is(getHTMLEditor().documentIsEmpty, false, + "nsIEditor.documentIsEmpty should be false when <body> has only an empty paragraph in designMode"); + + document.body.innerHTML = "<div>abc</div>"; + is(getHTMLEditor().documentIsEmpty, false, + "nsIEditor.documentIsEmpty should be false when <body> has text in a paragraph in designMode"); + + document.body.innerHTML = "abc"; + is(getHTMLEditor().documentIsEmpty, false, + "nsIEditor.documentIsEmpty should be false when <body> has text directly in designMode"); + + document.execCommand("selectall"); + document.execCommand("delete"); + todo_is(getHTMLEditor().documentIsEmpty, true, + "nsIEditor.documentIsEmpty should be true when all contents in designMode are deleted"); + })(); + document.designMode = "off"; + + document.body.innerHTML = originalBody; + SimpleTest.finish(); + }); + </script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_nsIEditor_insertLineBreak.html b/editor/libeditor/tests/test_nsIEditor_insertLineBreak.html new file mode 100644 index 0000000000..02a847b640 --- /dev/null +++ b/editor/libeditor/tests/test_nsIEditor_insertLineBreak.html @@ -0,0 +1,416 @@ +<!DOCTYPE> +<html> +<head> + <title>Test for nsIEditor.insertLineBreak()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> +<div id="display"> +</div> +<input value="abcdef"> +<textarea>abcdef</textarea> +<div id="content" contenteditable></div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> +"use strict"; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.expectAssertions(0, 2); // In a11y module +SimpleTest.waitForFocus(() => { + let input = document.getElementsByTagName("input")[0]; + let textarea = document.getElementsByTagName("textarea")[0]; + let contenteditable = document.getElementById("content"); + let selection = window.getSelection(); + + let beforeInputEvents = []; + let inputEvents = []; + function onBeforeInput(event) { + beforeInputEvents.push(event); + } + function onInput(event) { + inputEvents.push(event); + } + + function checkInputEvent(aEvent, aInputType, aTargetRanges, aDescription) { + ok(aEvent != null, `aEvent is null (${aDescription})`); + ok(aEvent instanceof InputEvent, + `"${aEvent.type}" event should be dispatched with InputEvent interface (${aDescription})`); + is(aEvent.cancelable, aEvent.type === "beforeinput", + `"${aEvent.type}" event should ${aEvent.type === "beforeinput" ? "be" : "be never"} cancelable (${aDescription})`); + is(aEvent.bubbles, true, + `"${aEvent.type}" event should always bubble (${aDescription})`); + is(aEvent.inputType, aInputType, + `inputType of "${aEvent.type}" event should be "${aInputType}" ${aDescription}`); + is(aEvent.data, null, + `data of "${aEvent.type}" event should be null ${aDescription}`); + is(aEvent.dataTransfer, null, + `dataTransfer of "${aEvent.type}" event should be null ${aDescription}`); + let targetRanges = aEvent.getTargetRanges(); + if (aTargetRanges.length === 0) { + is(targetRanges.length, 0, + `getTargetRange() of "${aEvent.type}" event should return empty array: ${aDescription}`); + } else { + is(targetRanges.length, aTargetRanges.length, + `getTargetRange() of "${aEvent.type}" event should return static range array: ${aDescription}`); + if (targetRanges.length == aTargetRanges.length) { + for (let i = 0; i < targetRanges.length; i++) { + is(targetRanges[i].startContainer, aTargetRanges[i].startContainer, + `startContainer of getTargetRanges()[${i}] of "${aEvent.type}" event does not match: ${aDescription}`); + is(targetRanges[i].startOffset, aTargetRanges[i].startOffset, + `startOffset of getTargetRanges()[${i}] of "${aEvent.type}" event does not match: ${aDescription}`); + is(targetRanges[i].endContainer, aTargetRanges[i].endContainer, + `endContainer of getTargetRanges()[${i}] of "${aEvent.type}" event does not match: ${aDescription}`); + is(targetRanges[i].endOffset, aTargetRanges[i].endOffset, + `endOffset of getTargetRanges()[${i}] of "${aEvent.type}" event does not match: ${aDescription}`); + } + } + } + } + + input.focus(); + input.selectionStart = input.selectionEnd = 3; + beforeInputEvents = []; + inputEvents = []; + input.addEventListener("beforeinput", onBeforeInput); + input.addEventListener("input", onInput); + try { + getPlaintextEditor(input).insertLineBreak(); + } catch (e) { + ok(true, e.message); + } + input.removeEventListener("beforeinput", onBeforeInput); + input.removeEventListener("input", onInput); + is(input.value, "abcdef", "nsIEditor.insertLineBreak() should do nothing on single line editor"); + is(beforeInputEvents.length, 1, 'nsIEditor.insertLineBreak() should cause a "beforeinput" event on single line editor'); + checkInputEvent(beforeInputEvents[0], "insertLineBreak", [], "on single line editor"); + is(inputEvents.length, 0, 'nsIEditor.insertLineBreak() should not cause "input" event on single line editor'); + + textarea.focus(); + textarea.selectionStart = textarea.selectionEnd = 3; + beforeInputEvents = []; + inputEvents = []; + textarea.addEventListener("beforeinput", onBeforeInput); + textarea.addEventListener("input", onInput); + getPlaintextEditor(textarea).insertLineBreak(); + textarea.removeEventListener("beforeinput", onBeforeInput); + textarea.removeEventListener("input", onInput); + is(textarea.value, "abc\ndef", "nsIEditor.insertLineBreak() should insert \\n into multi-line editor"); + is(beforeInputEvents.length, 1, 'nsIEditor.insertLineBreak() should cause "beforeinput" event once on multi-line editor'); + checkInputEvent(beforeInputEvents[0], "insertLineBreak", [], "on multi-line editor"); + is(inputEvents.length, 1, 'nsIEditor.insertLineBreak() should cause "input" event once on multi-line editor'); + checkInputEvent(inputEvents[0], "insertLineBreak", [], "on multi-line editor"); + + // Note that despite of the name, insertLineBreak() should insert paragraph separator in HTMLEditor. + + document.execCommand("defaultParagraphSeparator", false, "br"); + + contenteditable.innerHTML = "abcdef"; + contenteditable.focus(); + contenteditable.scrollTop; + let selectionContainer = contenteditable.firstChild; + selection.collapse(selectionContainer, 3); + beforeInputEvents = []; + inputEvents = []; + contenteditable.addEventListener("beforeinput", onBeforeInput); + contenteditable.addEventListener("input", onInput); + getPlaintextEditor(contenteditable).insertLineBreak(); + contenteditable.removeEventListener("beforeinput", onBeforeInput); + contenteditable.removeEventListener("input", onInput); + is(contenteditable.innerHTML, "abc<br>def", + 'nsIEditor.insertLineBreak() should insert <br> element into text node when defaultParagraphSeparator is "br"'); + is(beforeInputEvents.length, 1, + 'nsIEditor.insertLineBreak() should cause "beforeinput" event once on contenteditable which has only text node when defaultParagraphSeparator is "br"'); + checkInputEvent(beforeInputEvents[0], "insertParagraph", + [{startContainer: selectionContainer, startOffset: 3, + endContainer: selectionContainer, endOffset: 3}], + 'on HTMLEditor (when defaultParagraphSeparator is "br")'); + is(inputEvents.length, 1, + 'nsIEditor.insertLineBreak() should cause "input" event once on contenteditable which has only text node when defaultParagraphSeparator is "br"'); + checkInputEvent(inputEvents[0], "insertParagraph", [], 'on HTMLEditor (when defaultParagraphSeparator is "br")'); + + contenteditable.innerHTML = "<p>abcdef</p>"; + contenteditable.focus(); + contenteditable.scrollTop; + selectionContainer = contenteditable.firstChild.firstChild; + selection.collapse(selectionContainer, 3); + beforeInputEvents = []; + inputEvents = []; + contenteditable.addEventListener("beforeinput", onBeforeInput); + contenteditable.addEventListener("input", onInput); + getPlaintextEditor(contenteditable).insertLineBreak(); + contenteditable.removeEventListener("beforeinput", onBeforeInput); + contenteditable.removeEventListener("input", onInput); + is(contenteditable.innerHTML, "<p>abc</p><p>def</p>", + 'nsIEditor.insertLineBreak() should add <p> element after <p> element even when defaultParagraphSeparator is "br"'); + is(beforeInputEvents.length, 1, + 'nsIEditor.insertLineBreak() should cause "beforeinput" event once on contenteditable which has <p> element when defaultParagraphSeparator is "br"'); + checkInputEvent(beforeInputEvents[0], "insertParagraph", + [{startContainer: selectionContainer, startOffset: 3, + endContainer: selectionContainer, endOffset: 3}], + 'in <p> element on HTMLEditor (when defaultParagraphSeparator is "br")'); + is(inputEvents.length, 1, + 'nsIEditor.insertLineBreak() should cause "input" event once on contenteditable which has <p> element when defaultParagraphSeparator is "br"'); + checkInputEvent(inputEvents[0], "insertParagraph", [], 'in <p> element on HTMLEditor (when defaultParagraphSeparator is "br")'); + + contenteditable.innerHTML = "<div>abcdef</div>"; + contenteditable.focus(); + contenteditable.scrollTop; + selectionContainer = contenteditable.firstChild.firstChild; + selection.collapse(selectionContainer, 3); + beforeInputEvents = []; + inputEvents = []; + contenteditable.addEventListener("beforeinput", onBeforeInput); + contenteditable.addEventListener("input", onInput); + getPlaintextEditor(contenteditable).insertLineBreak(); + contenteditable.removeEventListener("beforeinput", onBeforeInput); + contenteditable.removeEventListener("input", onInput); + is(contenteditable.innerHTML, "<div>abc<br>def</div>", + 'nsIEditor.insertLineBreak() should insert <br> element into <div> element when defaultParagraphSeparator is "br"'); + is(beforeInputEvents.length, 1, + 'nsIEditor.insertLineBreak() should cause "beforeinput" event once on contenteditable which has <div> element when defaultParagraphSeparator is "br"'); + checkInputEvent(beforeInputEvents[0], "insertParagraph", + [{startContainer: selectionContainer, startOffset: 3, + endContainer: selectionContainer, endOffset: 3}], + 'in <div> element on HTMLEditor (when defaultParagraphSeparator is "br")'); + is(inputEvents.length, 1, + 'nsIEditor.insertLineBreak() should cause "input" event once on contenteditable which has <div> element when defaultParagraphSeparator is "br"'); + checkInputEvent(inputEvents[0], "insertParagraph", [], 'in <div> element on HTMLEditor (when defaultParagraphSeparator is "br")'); + + contenteditable.innerHTML = "<pre>abcdef</pre>"; + contenteditable.focus(); + contenteditable.scrollTop; + selectionContainer = contenteditable.firstChild.firstChild; + selection.collapse(selectionContainer, 3); + beforeInputEvents = []; + inputEvents = []; + contenteditable.addEventListener("beforeinput", onBeforeInput); + contenteditable.addEventListener("input", onInput); + getPlaintextEditor(contenteditable).insertLineBreak(); + contenteditable.removeEventListener("beforeinput", onBeforeInput); + contenteditable.removeEventListener("input", onInput); + is(contenteditable.innerHTML, "<pre>abc<br>def</pre>", + 'nsIEditor.insertLineBreak() should insert <br> element into <pre> element when defaultParagraphSeparator is "br"'); + is(beforeInputEvents.length, 1, + 'nsIEditor.insertLineBreak() should cause "beforeinput" event once on contenteditable which has <pre> element when defaultParagraphSeparator is "br"'); + checkInputEvent(beforeInputEvents[0], "insertParagraph", + [{startContainer: selectionContainer, startOffset: 3, + endContainer: selectionContainer, endOffset: 3}], + 'in <pre> element on HTMLEditor (when defaultParagraphSeparator is "br")'); + is(inputEvents.length, 1, + 'nsIEditor.insertLineBreak() should cause "input" event once on contenteditable which has <pre> element when defaultParagraphSeparator is "br"'); + checkInputEvent(inputEvents[0], "insertParagraph", [], 'in <pre> element on HTMLEditor (when defaultParagraphSeparator is "br")'); + + document.execCommand("defaultParagraphSeparator", false, "p"); + + contenteditable.innerHTML = "abcdef"; + contenteditable.focus(); + contenteditable.scrollTop; + selectionContainer = contenteditable.firstChild; + selection.collapse(selectionContainer, 3); + beforeInputEvents = []; + inputEvents = []; + contenteditable.addEventListener("beforeinput", onBeforeInput); + contenteditable.addEventListener("input", onInput); + getPlaintextEditor(contenteditable).insertLineBreak(); + contenteditable.removeEventListener("beforeinput", onBeforeInput); + contenteditable.removeEventListener("input", onInput); + is(contenteditable.innerHTML, "<p>abc</p><p>def</p>", + 'nsIEditor.insertLineBreak() should create <p> elements when there is only text node and defaultParagraphSeparator is "p"'); + is(beforeInputEvents.length, 1, + 'nsIEditor.insertLineBreak() should cause "beforeinput" event once on contenteditable which has only text node when defaultParagraphSeparator is "p"'); + checkInputEvent(beforeInputEvents[0], "insertParagraph", + [{startContainer: selectionContainer, startOffset: 3, + endContainer: selectionContainer, endOffset: 3}], + 'on HTMLEditor (when defaultParagraphSeparator is "p")'); + is(inputEvents.length, 1, + 'nsIEditor.insertLineBreak() should cause "input" event once on contenteditable which has only text node when defaultParagraphSeparator is "p"'); + checkInputEvent(inputEvents[0], "insertParagraph", [], 'on HTMLEditor (when defaultParagraphSeparator is "p")'); + + contenteditable.innerHTML = "<p>abcdef</p>"; + contenteditable.focus(); + contenteditable.scrollTop; + selectionContainer = contenteditable.firstChild.firstChild; + selection.collapse(selectionContainer, 3); + beforeInputEvents = []; + inputEvents = []; + contenteditable.addEventListener("beforeinput", onBeforeInput); + contenteditable.addEventListener("input", onInput); + getPlaintextEditor(contenteditable).insertLineBreak(); + contenteditable.removeEventListener("beforeinput", onBeforeInput); + contenteditable.removeEventListener("input", onInput); + is(contenteditable.innerHTML, "<p>abc</p><p>def</p>", + 'nsIEditor.insertLineBreak() should add <p> element after <p> element when defaultParagraphSeparator is "p"'); + is(beforeInputEvents.length, 1, + 'nsIEditor.insertLineBreak() should cause "beforeinput" event once on contenteditable which has <p> element when defaultParagraphSeparator is "p"'); + checkInputEvent(beforeInputEvents[0], "insertParagraph", + [{startContainer: selectionContainer, startOffset: 3, + endContainer: selectionContainer, endOffset: 3}], + 'in <p> element on HTMLEditor (when defaultParagraphSeparator is "p")'); + is(inputEvents.length, 1, + 'nsIEditor.insertLineBreak() should cause "input" event once on contenteditable which has <p> element when defaultParagraphSeparator is "p"'); + checkInputEvent(inputEvents[0], "insertParagraph", [], 'in <p> element on HTMLEditor (when defaultParagraphSeparator is "p")'); + + contenteditable.innerHTML = "<div>abcdef</div>"; + contenteditable.focus(); + contenteditable.scrollTop; + selectionContainer = contenteditable.firstChild.firstChild; + selection.collapse(selectionContainer, 3); + beforeInputEvents = []; + inputEvents = []; + contenteditable.addEventListener("beforeinput", onBeforeInput); + contenteditable.addEventListener("input", onInput); + getPlaintextEditor(contenteditable).insertLineBreak(); + contenteditable.removeEventListener("beforeinput", onBeforeInput); + contenteditable.removeEventListener("input", onInput); + is(contenteditable.innerHTML, "<div>abc</div><div>def</div>", + 'nsIEditor.insertLineBreak() should add <div> element after <div> element even when defaultParagraphSeparator is "p"'); + is(beforeInputEvents.length, 1, + 'nsIEditor.insertLineBreak() should cause "beforeinput" event once on contenteditable which has <div> element when defaultParagraphSeparator is "p"'); + checkInputEvent(beforeInputEvents[0], "insertParagraph", + [{startContainer: selectionContainer, startOffset: 3, + endContainer: selectionContainer, endOffset: 3}], + 'in <div> element on HTMLEditor (when defaultParagraphSeparator is "p")'); + is(inputEvents.length, 1, + 'nsIEditor.insertLineBreak() should cause "input" event once on contenteditable which has <div> element when defaultParagraphSeparator is "p"'); + checkInputEvent(inputEvents[0], "insertParagraph", [], 'in <div> element on HTMLEditor (when defaultParagraphSeparator is "p")'); + + contenteditable.innerHTML = "<pre>abcdef</pre>"; + contenteditable.focus(); + contenteditable.scrollTop; + selectionContainer = contenteditable.firstChild.firstChild; + selection.collapse(selectionContainer, 3); + beforeInputEvents = []; + inputEvents = []; + contenteditable.addEventListener("beforeinput", onBeforeInput); + contenteditable.addEventListener("input", onInput); + getPlaintextEditor(contenteditable).insertLineBreak(); + contenteditable.removeEventListener("beforeinput", onBeforeInput); + contenteditable.removeEventListener("input", onInput); + is(contenteditable.innerHTML, "<pre>abc<br>def</pre>", + 'nsIEditor.insertLineBreak() should insert <br> element into <pre> element when defaultParagraphSeparator is "p"'); + is(beforeInputEvents.length, 1, + 'nsIEditor.insertLineBreak() should cause "beforeinput" event once on contenteditable which has <pre> element when defaultParagraphSeparator is "p"'); + checkInputEvent(beforeInputEvents[0], "insertParagraph", + [{startContainer: selectionContainer, startOffset: 3, + endContainer: selectionContainer, endOffset: 3}], + 'in <pre> element on HTMLEditor (when defaultParagraphSeparator is "p")'); + is(inputEvents.length, 1, + 'nsIEditor.insertLineBreak() should cause "input" event once on contenteditable which has <pre> element when defaultParagraphSeparator is "p"'); + checkInputEvent(inputEvents[0], "insertParagraph", [], 'in <pre> element on HTMLEditor (when defaultParagraphSeparator is "p")'); + + document.execCommand("defaultParagraphSeparator", false, "div"); + + contenteditable.innerHTML = "abcdef"; + contenteditable.focus(); + contenteditable.scrollTop; + selectionContainer = contenteditable.firstChild; + selection.collapse(selectionContainer, 3); + beforeInputEvents = []; + inputEvents = []; + contenteditable.addEventListener("beforeinput", onBeforeInput); + contenteditable.addEventListener("input", onInput); + getPlaintextEditor(contenteditable).insertLineBreak(); + contenteditable.removeEventListener("beforeinput", onBeforeInput); + contenteditable.removeEventListener("input", onInput); + is(contenteditable.innerHTML, "<div>abc</div><div>def</div>", + 'nsIEditor.insertLineBreak() should create <div> elements when there is only text node and defaultParagraphSeparator is "div"'); + is(beforeInputEvents.length, 1, + 'nsIEditor.insertLineBreak() should cause "beforeinput" event once on contenteditable which has only text node when defaultParagraphSeparator is "div"'); + checkInputEvent(beforeInputEvents[0], "insertParagraph", + [{startContainer: selectionContainer, startOffset: 3, + endContainer: selectionContainer, endOffset: 3}], + 'on HTMLEditor (when defaultParagraphSeparator is "div")'); + is(inputEvents.length, 1, + 'nsIEditor.insertLineBreak() should cause "input" event once on contenteditable which has only text node when defaultParagraphSeparator is "div"'); + checkInputEvent(inputEvents[0], "insertParagraph", [], 'on HTMLEditor (when defaultParagraphSeparator is "div")'); + + contenteditable.innerHTML = "<p>abcdef</p>"; + contenteditable.focus(); + contenteditable.scrollTop; + selectionContainer = contenteditable.firstChild.firstChild; + selection.collapse(selectionContainer, 3); + beforeInputEvents = []; + inputEvents = []; + contenteditable.addEventListener("beforeinput", onBeforeInput); + contenteditable.addEventListener("input", onInput); + getPlaintextEditor(contenteditable).insertLineBreak(); + contenteditable.removeEventListener("beforeinput", onBeforeInput); + contenteditable.removeEventListener("input", onInput); + is(contenteditable.innerHTML, "<p>abc</p><p>def</p>", + 'nsIEditor.insertLineBreak() should add <p> element after <p> element even when defaultParagraphSeparator is "div"'); + is(beforeInputEvents.length, 1, + 'nsIEditor.insertLineBreak() should cause "beforeinput" event once on contenteditable which has <p> element when defaultParagraphSeparator is "div"'); + checkInputEvent(beforeInputEvents[0], "insertParagraph", + [{startContainer: selectionContainer, startOffset: 3, + endContainer: selectionContainer, endOffset: 3}], + 'in <p> element on HTMLEditor (when defaultParagraphSeparator is "div")'); + is(inputEvents.length, 1, + 'nsIEditor.insertLineBreak() should cause "input" event once on contenteditable which has <p> element when defaultParagraphSeparator is "div"'); + checkInputEvent(inputEvents[0], "insertParagraph", [], 'in <p> element on HTMLEditor (when defaultParagraphSeparator is "div")'); + + contenteditable.innerHTML = "<div>abcdef</div>"; + contenteditable.focus(); + contenteditable.scrollTop; + selectionContainer = contenteditable.firstChild.firstChild; + selection.collapse(selectionContainer, 3); + beforeInputEvents = []; + inputEvents = []; + contenteditable.addEventListener("beforeinput", onBeforeInput); + contenteditable.addEventListener("input", onInput); + getPlaintextEditor(contenteditable).insertLineBreak(); + contenteditable.removeEventListener("beforeinput", onBeforeInput); + contenteditable.removeEventListener("input", onInput); + is(contenteditable.innerHTML, "<div>abc</div><div>def</div>", + 'nsIEditor.insertLineBreak() should add <div> element after <div> element when defaultParagraphSeparator is "div"'); + is(beforeInputEvents.length, 1, + 'nsIEditor.insertLineBreak() should cause "beforeinput" event once on contenteditable which has <div> element when defaultParagraphSeparator is "div"'); + checkInputEvent(beforeInputEvents[0], "insertParagraph", + [{startContainer: selectionContainer, startOffset: 3, + endContainer: selectionContainer, endOffset: 3}], + 'in <div> element on HTMLEditor (when defaultParagraphSeparator is "div")'); + is(inputEvents.length, 1, + 'nsIEditor.insertLineBreak() should cause "input" event once on contenteditable which has <div> element when defaultParagraphSeparator is "div"'); + checkInputEvent(inputEvents[0], "insertParagraph", [], 'in <div> element on HTMLEditor (when defaultParagraphSeparator is "div")'); + + contenteditable.innerHTML = "<pre>abcdef</pre>"; + contenteditable.focus(); + contenteditable.scrollTop; + selectionContainer = contenteditable.firstChild.firstChild; + selection.collapse(selectionContainer, 3); + beforeInputEvents = []; + inputEvents = []; + contenteditable.addEventListener("beforeinput", onBeforeInput); + contenteditable.addEventListener("input", onInput); + getPlaintextEditor(contenteditable).insertLineBreak(); + contenteditable.removeEventListener("beforeinput", onBeforeInput); + contenteditable.removeEventListener("input", onInput); + is(contenteditable.innerHTML, "<pre>abc<br>def</pre>", + 'nsIEditor.insertLineBreak() should insert <br> element into <pre> element when defaultParagraphSeparator is "div"'); + is(beforeInputEvents.length, 1, + 'nsIEditor.insertLineBreak() should cause "beforeinput" event once on contenteditable which has <pre> element when defaultParagraphSeparator is "div"'); + checkInputEvent(beforeInputEvents[0], "insertParagraph", + [{startContainer: selectionContainer, startOffset: 3, + endContainer: selectionContainer, endOffset: 3}], + 'in <pre> element on HTMLEditor (when defaultParagraphSeparator is "div")'); + is(inputEvents.length, 1, + 'nsIEditor.insertLineBreak() should cause "input" event once on contenteditable which has <pre> element when defaultParagraphSeparator is "div"'); + checkInputEvent(inputEvents[0], "insertParagraph", [], 'in <pre> element on HTMLEditor (when defaultParagraphSeparator is "div")'); + + SimpleTest.finish(); +}); + +function getPlaintextEditor(aEditorElement) { + let editor = aEditorElement ? SpecialPowers.wrap(aEditorElement).editor : null; + if (!editor) { + editor = SpecialPowers.wrap(window).docShell.editingSession.getEditorForWindow(window); + } + return editor; +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_nsIEditor_insertNode.html b/editor/libeditor/tests/test_nsIEditor_insertNode.html new file mode 100644 index 0000000000..47aa1e2c0a --- /dev/null +++ b/editor/libeditor/tests/test_nsIEditor_insertNode.html @@ -0,0 +1,214 @@ +<!doctype> +<html> +<head> +<meta charset="utf-8"> +<title>nsIEditor.insertNode</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +<script> +"use strict"; + +function stringifyInputEvent(aEvent) { + if (!aEvent) { + return "null"; + } + return `${aEvent.type}: { inputType=${aEvent.inputType} }`; +} + +function getRangeDescription(range) { + function getNodeDescription(node) { + if (!node) { + return "null"; + } + switch (node.nodeType) { + case Node.TEXT_NODE: + return `${node.nodeName} "${node.data}"`; + case Node.ELEMENT_NODE: + return `<${node.nodeName.toLowerCase()}>`; + default: + return `${node.nodeName}`; + } + } + if (range === null) { + return "null"; + } + if (range === undefined) { + return "undefined"; + } + return range.startContainer == range.endContainer && + range.startOffset == range.endOffset + ? `(${getNodeDescription(range.startContainer)}, ${range.startOffset})` + : `(${getNodeDescription(range.startContainer)}, ${ + range.startOffset + }) - (${getNodeDescription(range.endContainer)}, ${range.endOffset})`; +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + const editingHost = document.querySelector("div[contenteditable]"); + const editor = + SpecialPowers.wrap(window).docShell.editingSession.getEditorForWindow(window); + + editingHost.focus(); + + let events = []; + editingHost.addEventListener("input", event => events.push(event)); + + (function test_insert_text_to_start() { + editor.insertNode(document.createTextNode("abc"), editingHost, 0); + is( + editingHost.innerHTML, + "abc<br>", + "test_insert_text_to_start: insertNode() should insert new text node at start of the container" + ); + is( + events.length, + 1, + "test_insert_text_to_start: Only one input event should be fired when insertNode() inserts a text node" + ); + is( + stringifyInputEvent(events[0]), + stringifyInputEvent({ type: "input", inputType: "" }), + "test_insert_text_to_start: input event should be fired when inserting a node" + ); + is( + getRangeDescription(getSelection().getRangeAt(0)), + getRangeDescription({ + startContainer: editingHost, + startOffset: 1, + endContainer: editingHost, + endOffset: 1, + }), + "test_insert_text_to_start: insertNode() should collapse selection after the inserted text node" + ); + })(); + + (function test_insert_span_to_big_index() { + events = []; + editingHost.innerHTML = "abc"; + const span = document.createElement("span"); + span.textContent = "def"; + editor.insertNode(span, editingHost, 1000); + is( + editingHost.innerHTML, + "abc<span>def</span>", + "test_insert_span_to_big_index: insertNode() with big index should insert new node at end of the container" + ); + is( + events.length, + 1, + "test_insert_span_to_big_index: Only one input event should be fired when insertNode() inserts a node" + ); + is( + stringifyInputEvent(events[0]), + stringifyInputEvent({ type: "input", inputType: "" }), + "test_insert_span_to_big_index: input event should be fired when inserting a node" + ); + is( + getRangeDescription(getSelection().getRangeAt(0)), + getRangeDescription({ + startContainer: editingHost, + startOffset: 2, + endContainer: editingHost, + endOffset: 2, + }), + "test_insert_span_to_big_index: insertNode() should collapse selection after the inserted node" + ); + })(); + + (function test_preserve_selection() { + events = []; + editingHost.innerHTML = "abc"; + const span = document.createElement("span"); + span.textContent = "def"; + getSelection().collapse(editingHost, 0); + editor.insertNode(span, editingHost, 1, true); + is( + editingHost.innerHTML, + "abc<span>def</span>", + "test_preserve_selection: insertNode() should insert new node at end of the container" + ); + is( + events.length, + 1, + "test_preserve_selection: Only one input event should be fired when insertNode() inserts a node" + ); + is( + stringifyInputEvent(events[0]), + stringifyInputEvent({ type: "input", inputType: "" }), + "test_preserve_selection: input event should be fired when inserting a node" + ); + is( + getRangeDescription(getSelection().getRangeAt(0)), + getRangeDescription({ + startContainer: editingHost, + startOffset: 0, + endContainer: editingHost, + endOffset: 0, + }), + "test_preserve_selection: insertNode() should not collapse selection after the inserted node" + ); + })(); + + (function test_not_preserve_selection_nested_by_beforeinput() { + editingHost.innerHTML = "abc"; + const span1 = document.createElement("span"); + span1.textContent = "def"; + const span2 = document.createElement("span"); + span2.textContent = "ghi"; + getSelection().collapse(editingHost, 0); + editingHost.addEventListener("beforeinput", () => { + editor.insertNode(span1, editingHost, 1); + }, {once: true}); + editor.insertNode(span2, editingHost, 2, true); + is( + editingHost.innerHTML, + "abc<span>def</span><span>ghi</span>", + "test_not_preserve_selection_nested_by_beforeinput: both insertNode() should work" + ); + is( + getRangeDescription(getSelection().getRangeAt(0)), + getRangeDescription({ + startContainer: editingHost, + startOffset: 2, + endContainer: editingHost, + endOffset: 2, + }), + "test_not_preserve_selection_nested_by_beforeinput: only insertNode() called in beforeinput listener should update selection" + ); + })(); + + (function test_not_preserve_selection_nested_by_input() { + editingHost.innerHTML = "abc"; + const span1 = document.createElement("span"); + span1.textContent = "def"; + const span2 = document.createElement("span"); + span2.textContent = "ghi"; + getSelection().collapse(editingHost, 0); + editingHost.addEventListener("input", () => { + editor.insertNode(span2, editingHost, 2); + }, {once: true}); + editor.insertNode(span1, editingHost, 1, true); + is( + editingHost.innerHTML, + "abc<span>def</span><span>ghi</span>", + "test_not_preserve_selection_nested_by_input: both insertNode() should work" + ); + is( + getRangeDescription(getSelection().getRangeAt(0)), + getRangeDescription({ + startContainer: editingHost, + startOffset: 3, + endContainer: editingHost, + endOffset: 3, + }), + "test_not_preserve_selection_nested_by_input: only insertNode() called in input listener should update selection" + ); + })(); + + SimpleTest.finish(); +}); +</script> +</head> +<body><div contenteditable><br></div></body> +</html> diff --git a/editor/libeditor/tests/test_nsIEditor_isSelectionEditable.html b/editor/libeditor/tests/test_nsIEditor_isSelectionEditable.html new file mode 100644 index 0000000000..92c4ba1aee --- /dev/null +++ b/editor/libeditor/tests/test_nsIEditor_isSelectionEditable.html @@ -0,0 +1,16 @@ +<!doctype html> +<title>Test for nsIEditor.isSelectionEditable</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +<input> +<textarea></textarea> +<script> +for (let tag of ["input", "textarea"]) { + let node = document.querySelector(tag); + ok(SpecialPowers.wrap(node).editor.isSelectionEditable, "Empty editor selection should be editable"); + node.value = "abcd"; + ok(SpecialPowers.wrap(node).editor.isSelectionEditable, "Non-empty editor selection should be editable"); + node.value = ""; + ok(SpecialPowers.wrap(node).editor.isSelectionEditable, "Empty editor selection should be editable after setting value"); +} +</script> diff --git a/editor/libeditor/tests/test_nsIEditor_outputToString.html b/editor/libeditor/tests/test_nsIEditor_outputToString.html new file mode 100644 index 0000000000..9d2e83245b --- /dev/null +++ b/editor/libeditor/tests/test_nsIEditor_outputToString.html @@ -0,0 +1,135 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Tests of nsIEditor#outputToString()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + SimpleTest.waitForFocus(() => { + const originalBody = document.body.innerHTML; + const Ci = SpecialPowers.Ci; + + /** + * TODO: Add "text/html" cases and other `nsIDocumentEncoder.*` options. + */ + (function test_with_text_editor() { + for (const test of [ + { + tag: "input", + innerHTML: "<input>", + }, + { + tag: "textarea", + innerHTML: "<textarea></textarea>", + }, + ]) { + document.body.innerHTML = test.innerHTML; + const textControl = document.body.querySelector(test.tag); + const editor = SpecialPowers.wrap(textControl).editor; + is( + editor.outputToString("text/plain", Ci.nsIDocumentEncoder.OutputRaw), + "", + `outputToString("text/plain", OutputRaw) for <${test.tag}> should return empty string (before focused)` + ); + textControl.focus(); + is( + editor.outputToString("text/plain", Ci.nsIDocumentEncoder.OutputRaw), + "", + `outputToString("text/plain", OutputRaw) for <${test.tag}> should return empty string (after focused)` + ); + textControl.value = "abc"; + is( + editor.outputToString("text/plain", Ci.nsIDocumentEncoder.OutputRaw), + "abc", + `outputToString("text/plain", OutputRaw) for <${test.tag}> whose value is "abc" should return the value as-is` + ); + if (editor.flags & Ci.nsIEditor.eEditorSingleLineMask) { + continue; + } + textControl.value = "abc\ndef"; + is( + editor.outputToString("text/plain", Ci.nsIDocumentEncoder.OutputRaw).replace(/\r/g, ""), + "abc\ndef", + `outputToString("text/plain", OutputRaw) for <${test.tag}> whose value is "abc\ndef" should return the value as-is` + ); + textControl.value = "abc\ndef\n"; + is( + editor.outputToString("text/plain", Ci.nsIDocumentEncoder.OutputRaw).replace(/\r/g, ""), + "abc\ndef\n", + `outputToString("text/plain", OutputRaw) for <${test.tag}> whose value is "abc\ndef\n" should return the value as-is` + ); + textControl.value = "abc\ndef\n\n"; + is( + editor.outputToString("text/plain", Ci.nsIDocumentEncoder.OutputRaw).replace(/\r/g, ""), + "abc\ndef\n\n", + `outputToString("text/plain", OutputRaw) for <${test.tag}> whose value is "abc\ndef\n\n" should return the value as-is` + ); + } + })(); + + function getHTMLEditor() { + const editingSession = SpecialPowers.wrap(window).docShell.editingSession; + if (!editingSession) { + return null; + } + return editingSession.getEditorForWindow(window); + } + + (function test_with_contenteditable() { + document.body.setAttribute("contenteditable", ""); + document.body.blur(); + document.body.innerHTML = ""; + is( + getHTMLEditor().outputToString("text/plain", Ci.nsIDocumentEncoder.OutputRaw).replace(/\r/g, ""), + "", + `outputToString("text/plain", OutputRaw) for empty <body contenteditable> should return empty string (before focused)` + ); + document.body.focus(); + is( + getHTMLEditor().outputToString("text/plain", Ci.nsIDocumentEncoder.OutputRaw).replace(/\r/g, ""), + "", // Ignore the padding <br> element for empty editor. + `outputToString("text/plain", OutputRaw) for empty <body contenteditable> should return empty string (after focused)` + ); + const sourceHasParagraphsAndDivs = "<p>abc</p><p>def<br></p><div>ghi</div><div>jkl<br>mno<br></div>"; + document.body.innerHTML = sourceHasParagraphsAndDivs; + // XXX Oddly, an ASCII white-space is inserted at the head of the result. + todo_is( + getHTMLEditor().outputToString("text/plain", Ci.nsIDocumentEncoder.OutputRaw).replace(/\r/g, ""), + sourceHasParagraphsAndDivs.replace(/<br>/gi, "\n").replace(/<[^>]+>/g, ""), + `outputToString("text/plain", OutputRaw) for <body contenteditable> should return the expected string` + ); + + document.body.removeAttribute("contenteditable"); + document.body.innerHTML = "<div contenteditable></div>"; + is( + getHTMLEditor().outputToString("text/plain", Ci.nsIDocumentEncoder.OutputRaw).replace(/\r/g, ""), + "", + `outputToString("text/plain", OutputRaw) for empty <div contenteditable> should return empty string (before focused)` + ); + document.body.querySelector("div[contenteditable]").focus(); + is( + getHTMLEditor().outputToString("text/plain", Ci.nsIDocumentEncoder.OutputRaw).replace(/\r/g, ""), + "", // Ignore the padding <br> element for empty editor. + `outputToString("text/plain", OutputRaw) for empty <div contenteditable> should return empty string (after focused)` + ); + document.body.querySelector("div[contenteditable]").innerHTML = sourceHasParagraphsAndDivs; + is( + getHTMLEditor().outputToString("text/plain", Ci.nsIDocumentEncoder.OutputRaw).replace(/\r/g, ""), + sourceHasParagraphsAndDivs.replace(/<br>/gi, "\n").replace(/<[^>]+>/g, ""), + `outputToString("text/plain", OutputRaw) for <div contenteditable> should return the expected string` + ); + })(); + + document.body.innerHTML = originalBody; + SimpleTest.finish(); + }); + </script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_nsIEditor_undoAll.html b/editor/libeditor/tests/test_nsIEditor_undoAll.html new file mode 100644 index 0000000000..45e82e884b --- /dev/null +++ b/editor/libeditor/tests/test_nsIEditor_undoAll.html @@ -0,0 +1,115 @@ +<!DOCTYPE html> +<html> +<head> +<title>Test for nsIEditor.undoAll()</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content"><input><textarea></textarea><div contenteditable></div></div> +<pre id="test"> +<script> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + function isTextEditor(aElement) { + return aElement.tagName.toLowerCase() == "input" || + aElement.tagName.toLowerCase() == "textarea"; + } + function getEditor(aElement) { + if (isTextEditor(aElement)) { + return SpecialPowers.wrap(aElement).editor; + } + return SpecialPowers.wrap(window).docShell.editingSession?.getEditorForWindow(window); + } + function setValue(aElement, aValue) { + if (isTextEditor(aElement)) { + aElement.value = aValue; + return; + } + aElement.innerHTML = aValue; + } + function getValue(aElement) { + if (isTextEditor(aElement)) { + return aElement.value; + } + return aElement.innerHTML.replace(/<br>/g, ""); + } + for (const selector of ["input", "textarea", "div[contenteditable]"]) { + const editableElement = document.querySelector(selector); + editableElement.focus(); + const editor = getEditor(editableElement); + setValue(editableElement, ""); + is( + editor.canUndo, + false, + `Editor for ${selector} shouldn't have undo transaction at start` + ); + is( + editor.canRedo, + false, + `Editor for ${selector} shouldn't have redo transaction at start` + ); + + synthesizeKey("b"); + is( + getValue(editableElement), + "b", + `Editor for ${selector} should've handled inserting "b"` + ); + is( + editor.canUndo, + true, + `Editor for ${selector} should have undo transaction after inserting "b"` + ); + is( + editor.canRedo, + false, + `Editor for ${selector} shouldn't have redo transaction after inserting "b"` + ); + + synthesizeKey("KEY_ArrowLeft"); + synthesizeKey("a"); + is( + getValue(editableElement), + "ab", + `Editor for ${selector} should've handled inserting "a" before "b"` + ); + is( + editor.canUndo, + true, + `Editor for ${selector} should have undo transaction after inserting text again` + ); + is( + editor.canRedo, + false, + `Editor for ${selector} should have redo transaction after inserting text again` + ); + + editor.undoAll(); + is( + getValue(editableElement), + "", + `Editor for ${selector} should've undone everything` + ); + is( + editor.canUndo, + false, + `Editor for ${selector} shouldn't have undo transactions after undoAll() called` + ); + is( + editor.canRedo, + true, + `Editor for ${selector} should have redo transaction after undoAll() called` + ); + + } + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_nsIEditor_undoRedoEnabled.html b/editor/libeditor/tests/test_nsIEditor_undoRedoEnabled.html new file mode 100644 index 0000000000..1a0841ea1e --- /dev/null +++ b/editor/libeditor/tests/test_nsIEditor_undoRedoEnabled.html @@ -0,0 +1,91 @@ +<!DOCTYPE html> +<html> +<head> +<title>Test for nsIEditor.undoRedoEnabled</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content"><input><textarea></textarea><div contenteditable></div></div> +<pre id="test"> +<script> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + function isTextEditor(aElement) { + return aElement.tagName.toLowerCase() == "input" || + aElement.tagName.toLowerCase() == "textarea"; + } + function getEditor(aElement) { + if (isTextEditor(aElement)) { + return SpecialPowers.wrap(aElement).editor; + } + return SpecialPowers.wrap(window).docShell.editingSession?.getEditorForWindow(window); + } + function setValue(aElement, aValue) { + if (isTextEditor(aElement)) { + aElement.value = aValue; + return; + } + aElement.innerHTML = aValue; + } + function getValue(aElement) { + if (isTextEditor(aElement)) { + return aElement.value; + } + return aElement.innerHTML.replace(/<br>/g, ""); + } + for (const selector of ["input", "textarea", "div[contenteditable]"]) { + const editableElement = document.querySelector(selector); + editableElement.focus(); + const editor = getEditor(editableElement); + setValue(editableElement, ""); + is( + editor.undoRedoEnabled, + true, + `undo/redo in editor for ${selector} should be enabled by default` + ); + editor.enableUndo(false); + is( + editor.undoRedoEnabled, + false, + `undo/redo in editor for ${selector} should be disable after calling enableUndo(false)` + ); + synthesizeKey("a"); + is( + getValue(editableElement), + "a", + `inserting text should be handled by editor for ${selector} even if undo/redo is disabled` + ); + is( + editor.canUndo, + false, + `undo transaction shouldn't be created by editor for ${selector} when undo/redo is disabled` + ); + editor.enableUndo(true); + is( + editor.undoRedoEnabled, + true, + `undo/redo in editor for ${selector} should be enabled after calling enableUndo(true)` + ); + synthesizeKey("b"); + is( + getValue(editableElement), + "ab", + `inserting text should be handled by editor for ${selector} after enabling undo/redo` + ); + is( + editor.canUndo, + true, + `undo transaction should be created by editor for ${selector} when undo/redo is enabled again` + ); + } + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_nsIHTMLEditor_getElementOrParentByTagName.html b/editor/libeditor/tests/test_nsIHTMLEditor_getElementOrParentByTagName.html new file mode 100644 index 0000000000..c78ec6763d --- /dev/null +++ b/editor/libeditor/tests/test_nsIHTMLEditor_getElementOrParentByTagName.html @@ -0,0 +1,449 @@ +<!DOCTYPE> +<html> +<head> + <title>Test for nsIHTMLEditor.getElementOrParentByTagName()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> +<section> +<div id="display"> +</div> +<div id="content" contenteditable></div> +</section> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(async function() { + let section = document.querySelector("section"); + let editor = document.querySelector("[contenteditable]"); + let selection = window.getSelection(); + let element; + + // Make sure that each test can run without previous tests for making each test + // debuggable with commenting out the unrelated tests. + + try { + editor.focus(); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("", null)); + ok(false, "nsIHTMLEditor.getElementOrParentByTagName(\"\", null) should throw an exception"); + } catch { + ok(true, "nsIHTMLEditor.getElementOrParentByTagName(\"\", null) should throw an exception"); + } + + try { + editor.focus(); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName(null, null)); + ok(false, "nsIHTMLEditor.getElementOrParentByTagName(null, null) should throw an exception"); + } catch { + ok(true, "nsIHTMLEditor.getElementOrParentByTagName(null, null) should throw an exception"); + } + + try { + editor.focus(); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName(undefined, null)); + ok(false, "nsIHTMLEditor.getElementOrParentByTagName(undefined, null) should throw an exception"); + } catch { + ok(true, "nsIHTMLEditor.getElementOrParentByTagName(undefined, null) should throw an exception"); + } + + editor.focus(); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("undefinedtagname", null)); + is(element, null, + "nsIHTMLEditor.getElementOrParentByTagName(\"undefinedtagname\", null) should return null"); + + editor.blur(); + selection.collapse(document.getElementById("display"), 0); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("section", null)); + is(element, section, + "nsIHTMLEditor.getElementOrParentByTagName(\"section\", null) should return the <section> when selection is in the it (HTML editor does not have focus)"); + + editor.focus(); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("section", null)); + is(element, section, + "nsIHTMLEditor.getElementOrParentByTagName(\"section\", null) should return the <section> when selection is in the it (HTML editor has focus)"); + + editor.focus(); + selection.removeAllRanges(); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("section", null)); + is(element, null, + "nsIHTMLEditor.getElementOrParentByTagName(\"section\", null) should return null when there is no selection"); + + editor.blur(); + selection.collapse(document.getElementById("display"), 0); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("body", null)); + is(element, null, + "nsIHTMLEditor.getElementOrParentByTagName(\"body\", null) should return null when it reaches the <body>"); + + editor.blur(); + selection.collapse(document.getElementById("display"), 0); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("div", editor)); + is(element, editor, + "nsIHTMLEditor.getElementOrParentByTagName(\"div\", editor) should return editor even when selection is outside of it"); + + editor.innerHTML = "<p>first</p><p>second</p>"; + editor.focus(); + selection.setBaseAndExtent(editor.firstChild.firstChild, 0, editor.firstChild.nextSibling.firstChild, 3); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("p", null)); + is(element, editor.firstChild, + "nsIHTMLEditor.getElementOrParentByTagName(\"p\", null) should return first <p> element when selection anchor is in it"); + + editor.innerHTML = "<table><tr><td>cell</td></tr></table>"; + editor.focus(); + selection.collapse(editor.querySelector("td").firstChild, 2); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("td", null)); + is(element, editor.querySelector("td"), + "nsIHTMLEditor.getElementOrParentByTagName(\"td\", null) should return the <td> when selection is collapsed in it"); + + editor.innerHTML = "<table><tr><td>cell</td></tr></table>"; + editor.focus(); + selection.collapse(editor.querySelector("td").firstChild, 2); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("th", null)); + is(element, null, + "nsIHTMLEditor.getElementOrParentByTagName(\"th\", null) should return null when selection is collapsed in <td>"); + + editor.innerHTML = "<table><tr><td>cell</td></tr></table>"; + editor.focus(); + selection.setBaseAndExtent(editor.querySelector("tr"), 0, editor.querySelector("tr"), 1); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("td", null)); + is(element, editor.querySelector("td"), + "nsIHTMLEditor.getElementOrParentByTagName(\"td\", null) should return the <td> when it's selected"); + + editor.innerHTML = "<table><tr><th>cell</th></tr></table>"; + editor.focus(); + selection.collapse(editor.querySelector("th").firstChild, 2); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("td", null)); + is(element, editor.querySelector("th"), + "nsIHTMLEditor.getElementOrParentByTagName(\"td\", null) should return the <th> when selection is collapsed in it"); + + editor.innerHTML = "<table><tr><th>cell</th></tr></table>"; + editor.focus(); + selection.collapse(editor.querySelector("th").firstChild, 2); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("th", null)); + is(element, editor.querySelector("th"), + "nsIHTMLEditor.getElementOrParentByTagName(\"th\", null) should return the <th> when selection is collapsed in it"); + + editor.innerHTML = "<table><tr><th>cell</th></tr></table>"; + editor.focus(); + selection.setBaseAndExtent(editor.querySelector("tr"), 0, editor.querySelector("tr"), 1); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("td", null)); + is(element, editor.querySelector("th"), + "nsIHTMLEditor.getElementOrParentByTagName(\"td\", null) should return the <th> when it's selected"); + + editor.innerHTML = "<table><tr><th>cell</th></tr></table>"; + editor.focus(); + selection.setBaseAndExtent(editor.querySelector("tr"), 0, editor.querySelector("tr"), 1); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("th", null)); + is(element, editor.querySelector("th"), + "nsIHTMLEditor.getElementOrParentByTagName(\"th\", null) should return the <th> when it's selected"); + + editor.innerHTML = "<ul><li>listitem</li></ul>"; + editor.focus(); + selection.collapse(editor.querySelector("li").firstChild, 4); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("ul", null)); + is(element, editor.querySelector("ul"), + "nsIHTMLEditor.getElementOrParentByTagName(\"ul\", null) should return the <ul> when selection is collapsed in its <li>"); + + editor.innerHTML = "<ul><li>listitem</li></ul>"; + editor.focus(); + selection.collapse(editor.querySelector("li").firstChild, 4); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("list", null)); + is(element, editor.querySelector("ul"), + "nsIHTMLEditor.getElementOrParentByTagName(\"list\", null) should return the <ul> when selection is collapsed in its <li>"); + + editor.innerHTML = "<ul><li>listitem</li></ul>"; + editor.focus(); + selection.collapse(editor.querySelector("li").firstChild, 4); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("ol", null)); + is(element, null, + "nsIHTMLEditor.getElementOrParentByTagName(\"ol\", null) should return null when selection is collapsed in <ul>"); + + editor.innerHTML = "<ol><li>listitem</li></ol>"; + editor.focus(); + selection.collapse(editor.querySelector("li").firstChild, 4); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("ol", null)); + is(element, editor.querySelector("ol"), + "nsIHTMLEditor.getElementOrParentByTagName(\"ol\", null) should return the <ol> when selection is collapsed in its <li>"); + + editor.innerHTML = "<ol><li>listitem</li></ol>"; + editor.focus(); + selection.collapse(editor.querySelector("li").firstChild, 4); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("list", null)); + is(element, editor.querySelector("ol"), + "nsIHTMLEditor.getElementOrParentByTagName(\"list\", null) should return the <ol> when selection is collapsed in its <li>"); + + editor.innerHTML = "<ol><li>listitem</li></ol>"; + editor.focus(); + selection.collapse(editor.querySelector("li").firstChild, 4); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("ul", null)); + is(element, null, + "nsIHTMLEditor.getElementOrParentByTagName(\"ol\", null) should return null when selection is collapsed in <ol>"); + + editor.innerHTML = "<dl><dt>listitem</dt></dl>"; + editor.focus(); + selection.collapse(editor.querySelector("dt").firstChild, 4); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("dl", null)); + is(element, editor.querySelector("dl"), + "nsIHTMLEditor.getElementOrParentByTagName(\"dl\", null) should return the <dl> when selection is collapsed in its <dt>"); + + editor.innerHTML = "<dl><dt>listitem</dt></dl>"; + editor.focus(); + selection.collapse(editor.querySelector("dt").firstChild, 4); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("list", null)); + is(element, editor.querySelector("dl"), + "nsIHTMLEditor.getElementOrParentByTagName(\"list\", null) should return the <dl> when selection is collapsed in its <dt>"); + + editor.innerHTML = "<dl><dd>listitem</dd></dl>"; + editor.focus(); + selection.collapse(editor.querySelector("dd").firstChild, 4); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("dl", null)); + is(element, editor.querySelector("dl"), + "nsIHTMLEditor.getElementOrParentByTagName(\"dl\", null) should return the <dl> when selection is collapsed in its <dd>"); + + editor.innerHTML = "<dl><dd>listitem</dd></dl>"; + editor.focus(); + selection.collapse(editor.querySelector("dd").firstChild, 4); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("list", null)); + is(element, editor.querySelector("dl"), + "nsIHTMLEditor.getElementOrParentByTagName(\"list\", null) should return the <dl> when selection is collapsed in its <dd>"); + + editor.innerHTML = "<ul><ol><li>listitem</li></ol></ul>"; + editor.focus(); + selection.collapse(editor.querySelector("li").firstChild, 4); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("list", null)); + is(element, editor.querySelector("ol"), + "nsIHTMLEditor.getElementOrParentByTagName(\"list\", null) should return the <ol> (sublist) when selection is collapsed in its <li>"); + + editor.innerHTML = "<ul><ol><li>listitem</li></ol></ul>"; + editor.focus(); + selection.collapse(editor.querySelector("li").firstChild, 4); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("ol", null)); + is(element, editor.querySelector("ol"), + "nsIHTMLEditor.getElementOrParentByTagName(\"ol\", null) should return the <ol> (sublist) when selection is collapsed in its <li>"); + + editor.innerHTML = "<ul><ol><li>listitem</li></ol></ul>"; + editor.focus(); + selection.collapse(editor.querySelector("li").firstChild, 4); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("ul", null)); + is(element, editor.querySelector("ul"), + "nsIHTMLEditor.getElementOrParentByTagName(\"ul\", null) should return the <ul> when selection is collapsed in its sublist's <li>"); + + editor.innerHTML = "<ol><ul><li>listitem</li></ul></ol>"; + editor.focus(); + selection.collapse(editor.querySelector("li").firstChild, 4); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("list", null)); + is(element, editor.querySelector("ul"), + "nsIHTMLEditor.getElementOrParentByTagName(\"list\", null) should return the <ul> (sublist) when selection is collapsed in its <li>"); + + editor.innerHTML = "<ol><ul><li>listitem</li></ul></ol>"; + editor.focus(); + selection.collapse(editor.querySelector("li").firstChild, 4); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("ul", null)); + is(element, editor.querySelector("ul"), + "nsIHTMLEditor.getElementOrParentByTagName(\"ul\", null) should return the <ul> (sublist) when selection is collapsed in its <li>"); + + editor.innerHTML = "<ol><ul><li>listitem</li></ul></ol>"; + editor.focus(); + selection.collapse(editor.querySelector("li").firstChild, 4); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("ol", null)); + is(element, editor.querySelector("ol"), + "nsIHTMLEditor.getElementOrParentByTagName(\"ol\", null) should return the <ol> when selection is collapsed in its sublist's <li>"); + + editor.innerHTML = "<p><a href=\"about:config\">anchor</a></p>"; + editor.focus(); + selection.collapse(editor.querySelector("a").firstChild, 3); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("a", null)); + is(element, editor.querySelector("a"), + "nsIHTMLEditor.getElementOrParentByTagName(\"a\", null) should return the <a href=\"about:config\"> when selection is collapsed in it"); + + editor.innerHTML = "<p><a href=\"about:config\">anchor</a></p>"; + editor.focus(); + selection.setBaseAndExtent(editor.firstChild, 0, editor.firstChild, 1); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("a", null)); + is(element, editor.querySelector("a"), + "nsIHTMLEditor.getElementOrParentByTagName(\"a\", null) should return the <a href=\"about:config\"> when it's selected"); + + editor.innerHTML = "<p><a href=\"about:config\">anchor</a></p>"; + editor.focus(); + selection.collapse(editor.querySelector("a").firstChild, 3); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("href", null)); + is(element, editor.querySelector("a"), + "nsIHTMLEditor.getElementOrParentByTagName(\"href\", null) should return the <a href=\"about:config\"> when selection is collapsed in it"); + + editor.innerHTML = "<p><a href=\"about:config\">anchor</a></p>"; + editor.focus(); + selection.setBaseAndExtent(editor.firstChild, 0, editor.firstChild, 1); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("href", null)); + is(element, editor.querySelector("a"), + "nsIHTMLEditor.getElementOrParentByTagName(\"href\", null) should return the <a href=\"about:config\"> when it's selected"); + + editor.innerHTML = "<p><a href=\"about:config\">anchor</a></p>"; + editor.focus(); + selection.collapse(editor.querySelector("a").firstChild, 3); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("anchor", null)); + is(element, null, + "nsIHTMLEditor.getElementOrParentByTagName(\"anchor\", null) should return null when selection is collapsed in the <a href=\"about:config\">"); + + editor.innerHTML = "<p><a href=\"about:config\">anchor</a></p>"; + editor.focus(); + selection.setBaseAndExtent(editor.firstChild, 0, editor.firstChild, 1); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("anchor", null)); + is(element, null, + "nsIHTMLEditor.getElementOrParentByTagName(\"anchor\", null) should return null when the <a href=\"about:config\"> is selected"); + + editor.innerHTML = "<p><a href=\"\">anchor</a></p>"; + editor.focus(); + selection.collapse(editor.querySelector("a").firstChild, 3); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("a", null)); + is(element, editor.querySelector("a"), + "nsIHTMLEditor.getElementOrParentByTagName(\"a\", null) should return the <a href=\"\"> when selection is collapsed in it"); + + editor.innerHTML = "<p><a href=\"\">anchor</a></p>"; + editor.focus(); + selection.setBaseAndExtent(editor.firstChild, 0, editor.firstChild, 1); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("a", null)); + is(element, editor.querySelector("a"), + "nsIHTMLEditor.getElementOrParentByTagName(\"a\", null) should return the <a href=\"\"> when it's selected"); + + editor.innerHTML = "<p><a href=\"\">anchor</a></p>"; + editor.focus(); + selection.collapse(editor.querySelector("a").firstChild, 3); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("href", null)); + is(element, editor.querySelector("a"), + "nsIHTMLEditor.getElementOrParentByTagName(\"href\", null) should return the <a href=\"\"> when selection is collapsed in it"); + + editor.innerHTML = "<p><a href=\"\">anchor</a></p>"; + editor.focus(); + selection.setBaseAndExtent(editor.firstChild, 0, editor.firstChild, 1); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("href", null)); + is(element, editor.querySelector("a"), + "nsIHTMLEditor.getElementOrParentByTagName(\"href\", null) should return the <a href=\"\"> when it's selected"); + + editor.innerHTML = "<p><a name=\"foo\">anchor</a></p>"; + editor.focus(); + selection.collapse(editor.querySelector("a").firstChild, 3); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("a", null)); + is(element, editor.querySelector("a"), + "nsIHTMLEditor.getElementOrParentByTagName(\"a\", null) should return the <a name=\"foo\"> when selection is collapsed in it"); + + editor.innerHTML = "<p><a name=\"foo\">anchor</a></p>"; + editor.focus(); + selection.setBaseAndExtent(editor.firstChild, 0, editor.firstChild, 1); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("a", null)); + is(element, editor.querySelector("a"), + "nsIHTMLEditor.getElementOrParentByTagName(\"a\", null) should return the <a name=\"foo\"> when it's selected"); + + editor.innerHTML = "<p><a name=\"foo\">anchor</a></p>"; + editor.focus(); + selection.collapse(editor.querySelector("a").firstChild, 3); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("href", null)); + is(element, null, + "nsIHTMLEditor.getElementOrParentByTagName(\"href\", null) should return null when selection is collapsed in the <a name=\"foo\">"); + + editor.innerHTML = "<p><a name=\"foo\">anchor</a></p>"; + editor.focus(); + selection.setBaseAndExtent(editor.firstChild, 0, editor.firstChild, 1); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("href", null)); + is(element, null, + "nsIHTMLEditor.getElementOrParentByTagName(\"href\", null) should return null when the <a name=\"foo\"> is selected"); + + editor.innerHTML = "<p><a name=\"foo\">anchor</a></p>"; + editor.focus(); + selection.collapse(editor.querySelector("a").firstChild, 3); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("anchor", null)); + is(element, editor.querySelector("a"), + "nsIHTMLEditor.getElementOrParentByTagName(\"anchor\", null) should return the <a name=\"foo\"> when selection is collapsed in it"); + + editor.innerHTML = "<p><a name=\"foo\">anchor</a></p>"; + editor.focus(); + selection.setBaseAndExtent(editor.firstChild, 0, editor.firstChild, 1); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("anchor", null)); + is(element, editor.querySelector("a"), + "nsIHTMLEditor.getElementOrParentByTagName(\"anchor\", null) should return the <a name=\"foo\"> when it's selected"); + + editor.innerHTML = "<p><a name=\"\">anchor</a></p>"; + editor.focus(); + selection.collapse(editor.querySelector("a").firstChild, 3); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("a", null)); + is(element, editor.querySelector("a"), + "nsIHTMLEditor.getElementOrParentByTagName(\"a\", null) should return the <a name=\"\"> when selection is collapsed in it"); + + editor.innerHTML = "<p><a name=\"\">anchor</a></p>"; + editor.focus(); + selection.setBaseAndExtent(editor.firstChild, 0, editor.firstChild, 1); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("a", null)); + is(element, editor.querySelector("a"), + "nsIHTMLEditor.getElementOrParentByTagName(\"a\", null) should return the <a name=\"\"> when it's selected"); + + editor.innerHTML = "<p><a name=\"\">anchor</a></p>"; + editor.focus(); + selection.collapse(editor.querySelector("a").firstChild, 3); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("anchor", null)); + is(element, null, + "nsIHTMLEditor.getElementOrParentByTagName(\"anchor\", null) should return null when selection is collapsed in the <a name=\"\">"); + + editor.innerHTML = "<p><a name=\"\">anchor</a></p>"; + editor.focus(); + selection.setBaseAndExtent(editor.firstChild, 0, editor.firstChild, 1); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("anchor", null)); + is(element, null, + "nsIHTMLEditor.getElementOrParentByTagName(\"anchor\", null) should return null when the <a name=\"\"> is selected"); + + editor.innerHTML = "<p><a>anchor</a></p>"; + editor.focus(); + selection.collapse(editor.querySelector("a").firstChild, 3); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("a", null)); + is(element, editor.querySelector("a"), + "nsIHTMLEditor.getElementOrParentByTagName(\"a\", null) should return the <a> when selection is collapsed in it"); + + editor.innerHTML = "<p><a>anchor</a></p>"; + editor.focus(); + selection.collapse(editor.querySelector("a").firstChild, 3); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("href", null)); + is(element, null, + "nsIHTMLEditor.getElementOrParentByTagName(\"href\", null) should return null when selection is collapsed in the <a>"); + + editor.innerHTML = "<p><a>anchor</a></p>"; + editor.focus(); + selection.collapse(editor.querySelector("a").firstChild, 3); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("anchor", null)); + is(element, null, + "nsIHTMLEditor.getElementOrParentByTagName(\"anchor\", null) should return null when selection is collapsed in the <a>"); + + editor.innerHTML = "<p><a>anchor</a></p>"; + editor.focus(); + selection.setBaseAndExtent(editor.firstChild, 0, editor.firstChild, 1); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("a", null)); + is(element, editor.querySelector("a"), + "nsIHTMLEditor.getElementOrParentByTagName(\"a\", null) should return the <a> when it's selected"); + + editor.innerHTML = "<p><a>anchor</a></p>"; + editor.focus(); + selection.setBaseAndExtent(editor.firstChild, 0, editor.firstChild, 1); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("href", null)); + is(element, null, + "nsIHTMLEditor.getElementOrParentByTagName(\"href\", null) should return null when the <a> is selected"); + + editor.innerHTML = "<p><a>anchor</a></p>"; + editor.focus(); + selection.setBaseAndExtent(editor.firstChild, 0, editor.firstChild, 1); + element = SpecialPowers.unwrap(getHTMLEditor().getElementOrParentByTagName("anchor", null)); + is(element, null, + "nsIHTMLEditor.getElementOrParentByTagName(\"anchor\", null) should return the <a> is selected"); + + SimpleTest.finish(); +}); + +function getHTMLEditor() { + var Ci = SpecialPowers.Ci; + var editingSession = SpecialPowers.wrap(window).docShell.editingSession; + return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsIHTMLEditor); +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_nsIHTMLEditor_getParagraphState.html b/editor/libeditor/tests/test_nsIHTMLEditor_getParagraphState.html new file mode 100644 index 0000000000..0297464c3f --- /dev/null +++ b/editor/libeditor/tests/test_nsIHTMLEditor_getParagraphState.html @@ -0,0 +1,156 @@ +<!DOCTYPE> +<html> +<head> + <title>Test for nsIHTMLEditor.getParagraphState()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> +<div id="display"> +</div> +<div id="content" contenteditable></div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + let editor = document.getElementById("content"); + let selection = window.getSelection(); + let tag, range, mixed = {}; + + editor.focus(); + editor.blur(); + selection.removeAllRanges(); + + try { + tag = getHTMLEditor().getParagraphState(mixed); + ok(false, "nsIHTMLEditor.getParagraphState() should throw exception when there is no selection range"); + } catch (e) { + ok(true, "nsIHTMLEditor.getParagraphState() should throw exception when there is no selection range"); + } + + range = document.createRange(); + range.setStart(document, 0); + selection.addRange(range); + tag = getHTMLEditor().getParagraphState(mixed); + is(tag, "x", "nsIHTMLEditor.getParagraphState() should return \"x\" when selection range starts from document node"); + is(mixed.value, false, "nsIHTMLEditor.getParagraphState() should return false for mixed state when selection range starts from document node"); + + editor.focus(); + selection.collapse(editor, 0); + tag = getHTMLEditor().getParagraphState(mixed); + is(tag, "x", "nsIHTMLEditor.getParagraphState() should return \"x\" when the editing host is empty"); + is(mixed.value, false, "nsIHTMLEditor.getParagraphState() should return false for mixed state when the editing host is empty"); + + editor.innerHTML = "foo"; + selection.collapse(editor.firstChild, 0); + tag = getHTMLEditor().getParagraphState(mixed); + is(tag, "", "nsIHTMLEditor.getParagraphState() should return \"\" when the editing host has only text node"); + is(mixed.value, false, "nsIHTMLEditor.getParagraphState() should return false for mixed state when the editing host has only text node"); + + for (let test of [ + {tag: "p", + expected: {tag: "p", tagIfEmpty: "p"}}, + {tag: "pre", + expected: {tag: "pre", tagIfEmpty: "pre"}}, + {tag: "h1", + expected: {tag: "h1", tagIfEmpty: "h1"}}, + {tag: "h2", + expected: {tag: "h2", tagIfEmpty: "h2"}}, + {tag: "h3", + expected: {tag: "h3", tagIfEmpty: "h3"}}, + {tag: "h4", + expected: {tag: "h4", tagIfEmpty: "h4"}}, + {tag: "h5", + expected: {tag: "h5", tagIfEmpty: "h5"}}, + {tag: "h6", + expected: {tag: "h6", tagIfEmpty: "h6"}}, + {tag: "address", + expected: {tag: "address", tagIfEmpty: "address"}}, + {tag: "span", + expected: {tag: "", tagIfEmpty: ""}}, + {tag: "b", + expected: {tag: "", tagIfEmpty: ""}}, + {tag: "i", + expected: {tag: "", tagIfEmpty: ""}}, + {tag: "em", + expected: {tag: "", tagIfEmpty: ""}}, + {tag: "div", + expected: {tag: "", tagIfEmpty: "x"}}, + {tag: "section", + expected: {tag: "", tagIfEmpty: "x"}}, + {tag: "article", + expected: {tag: "", tagIfEmpty: "x"}}, + {tag: "header", + expected: {tag: "", tagIfEmpty: "x"}}, + {tag: "main", + expected: {tag: "", tagIfEmpty: "x"}}, + {tag: "footer", + expected: {tag: "", tagIfEmpty: "x"}}, + {tag: "aside", + expected: {tag: "", tagIfEmpty: "x"}}, + {tag: "blockquote", + expected: {tag: "", tagIfEmpty: "x"}}, + {tag: "form", + expected: {tag: "", tagIfEmpty: "x"}}, + ]) { + editor.innerHTML = `<${test.tag}></${test.tag}>`; + selection.collapse(editor.firstChild, 0); + tag = getHTMLEditor().getParagraphState(mixed); + is(tag, test.expected.tagIfEmpty, `nsIHTMLEditor.getParagraphState() should return "${test.expected.tagIfEmpty}" when the editing host has an empty <${test.tag}>`); + is(mixed.value, false, `nsIHTMLEditor.getParagraphState() should return false for mixed state when the editing host has an empty <${test.tag}>`); + + editor.innerHTML = `<${test.tag}>foo</${test.tag}>`; + selection.collapse(editor.firstChild.firstChild, 0); + tag = getHTMLEditor().getParagraphState(mixed); + is(tag, test.expected.tag, `nsIHTMLEditor.getParagraphState() should return "${test.expected.tag}" when the editing host has a <${test.tag}> which has a text node`); + is(mixed.value, false, `nsIHTMLEditor.getParagraphState() should return false for mixed state when the editing host has a <${test.tag}> which has a text node`); + + editor.innerHTML = `<${test.tag}><span>foo</span></${test.tag}>`; + selection.collapse(editor.firstChild.firstChild.firstChild, 0); + tag = getHTMLEditor().getParagraphState(mixed); + is(tag, test.expected.tag, `nsIHTMLEditor.getParagraphState() should return "${test.expected.tag}" when the editing host has a <${test.tag}> which has a <span>`); + is(mixed.value, false, `nsIHTMLEditor.getParagraphState() should return false for mixed state when the editing host has a <${test.tag}> which has a <span>`); + + editor.innerHTML = `<${test.tag}>foo</${test.tag}>`; + selection.collapse(editor.firstChild, 1); + tag = getHTMLEditor().getParagraphState(mixed); + is(tag, test.expected.tag, `nsIHTMLEditor.getParagraphState() should return "${test.expected.tag}" when the editing host has a <${test.tag}> which has a text node (selection collapsed at end of the element)`); + is(mixed.value, false, `nsIHTMLEditor.getParagraphState() should return false for mixed state when the editing host has a <${test.tag}> which has a text node (selection collapsed at end of the element)`); + } + + editor.innerHTML = "<main><h1>header1</h1><section><h2>header2</h2><article><h3>header3</h3><p>paragraph</p><pre>preformat</pre></article></section></main>"; + + selection.setBaseAndExtent(document.querySelector("[contenteditable] h1").firstChild, 0, + document.querySelector("[contenteditable] h2").firstChild, 0); + tag = getHTMLEditor().getParagraphState(mixed); + is(tag, "h2", "nsIHTMLEditor.getParagraphState() should return \"h1\" when between <h1> and <h2> is selected"); + is(mixed.value, true, "nsIHTMLEditor.getParagraphState() should return true for mixed state when between <h1> and <h2> is selected"); + + selection.setBaseAndExtent(document.querySelector("[contenteditable] h1").firstChild, 0, + document.querySelector("[contenteditable] h3").firstChild, 0); + tag = getHTMLEditor().getParagraphState(mixed); + is(tag, "h3", "nsIHTMLEditor.getParagraphState() should return \"h3\" when between <h1> and <h3> is selected (whole of <h2> is also selected)"); + is(mixed.value, true, "nsIHTMLEditor.getParagraphState() should return true for mixed state when between <h1> and <h3> is selected (whole of <h2> is also selected)"); + + selection.setBaseAndExtent(document.querySelector("[contenteditable] p").firstChild, 0, + document.querySelector("[contenteditable] pre").firstChild, 0); + tag = getHTMLEditor().getParagraphState(mixed); + is(tag, "pre", "nsIHTMLEditor.getParagraphState() should return \"pre\" when between <p> and <pre> is selected"); + is(mixed.value, true, "nsIHTMLEditor.getParagraphState() should return true for mixed state when between <p> and <pre> is selected"); + + SimpleTest.finish(); +}); + +function getHTMLEditor() { + var Ci = SpecialPowers.Ci; + var editingSession = SpecialPowers.wrap(window).docShell.editingSession; + return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsIHTMLEditor); +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_nsIHTMLEditor_getSelectedElement.html b/editor/libeditor/tests/test_nsIHTMLEditor_getSelectedElement.html new file mode 100644 index 0000000000..009591c252 --- /dev/null +++ b/editor/libeditor/tests/test_nsIHTMLEditor_getSelectedElement.html @@ -0,0 +1,816 @@ +<!DOCTYPE> +<html> +<head> + <title>Test for nsIHTMLEditor.getSelectedElement()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> +<div id="display"> +</div> +<div id="content" contenteditable></div> +<img src="green.png"><!-- necessary to load this image before start testing --> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(async function() { + let editor = document.getElementById("content"); + let selection = window.getSelection(); + + // nsIHTMLEditor.getSelectedElement() is probably designed to retrieve + // a[href]:not([href=""]), a[name]:not([name=""]), or void element like + // <img> element. When user selects usual inline elements with dragging, + // double-clicking, Gecko sets start and/or end to point in text nodes + // as far as possible. Therefore, this API users don't expect that this + // returns usual inline elements like <b> element. + + // So, we need to check user's operation works fine. + for (let eatSpaceToNextWord of [true, false]) { + await SpecialPowers.pushPrefEnv({"set": [["layout.word_select.eat_space_to_next_word", eatSpaceToNextWord]]}); + + editor.innerHTML = "<p>This <b>is</b> an <i>example </i>text.<br></p>" + + "<p>and an image <img src=\"green.png\"> is here.</p>" + + "<p>An anchor with href attr <a href=\"about:blank\">is</a> here.</p>"; + editor.focus(); + editor.scrollTop; // flush layout. + + let b = editor.firstChild.firstChild.nextSibling; + let i = b.nextSibling.nextSibling; + let img = editor.firstChild.nextSibling.firstChild.nextSibling; + let href = editor.firstChild.nextSibling.nextSibling.firstChild.nextSibling; + + // double clicking usual inline element shouldn't cause "selecting" the element. + synthesizeMouseAtCenter(b, {clickCount: 1}); + synthesizeMouseAtCenter(b, {clickCount: 2}); + is(selection.getRangeAt(0).startContainer, b.previousSibling, + `#0-1 Double-clicking in <b> element should set start of selection to end of previous text node (eat_space_to_next_word: ${eatSpaceToNextWord})`); + is(selection.getRangeAt(0).startOffset, b.previousSibling.length, + `#0-1 Double-clicking in <b> element should set start of selection to end of previous text node (eat_space_to_next_word: ${eatSpaceToNextWord})`); + is(selection.getRangeAt(0).endContainer, b.nextSibling, + `#0-1 Double-clicking in <b> element should set end of selection to start of next text node (eat_space_to_next_word: ${eatSpaceToNextWord})`); + is(selection.getRangeAt(0).endOffset, eatSpaceToNextWord ? 1 : 0, + `#0-1 Double-clicking in <b> element should set end of selection to start of next text node (eat_space_to_next_word: ${eatSpaceToNextWord})`); + + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")), + null, + `#0-1 nsIHTMLEditor::getSelectedElement(\"\") should return null after double-clicking in <b> element (eat_space_to_next_word: ${eatSpaceToNextWord})`); + + synthesizeMouseAtCenter(i, {clickCount: 1}); + synthesizeMouseAtCenter(i, {clickCount: 2}); + is(selection.getRangeAt(0).startContainer, i.previousSibling, + `#0-2 Double-clicking in <i> element should set start of selection to end of previous text node (eat_space_to_next_word: ${eatSpaceToNextWord})`); + is(selection.getRangeAt(0).startOffset, i.previousSibling.length, + `#0-2 Double-clicking in <i> element should set start of selection to end of previous text node (eat_space_to_next_word: ${eatSpaceToNextWord})`); + if (eatSpaceToNextWord) { + is(selection.getRangeAt(0).endContainer, i.nextSibling, + "#0-2 Double-clicking in <i> element should set end of selection to start of next text node (eat_space_to_next_word: true)"); + is(selection.getRangeAt(0).endOffset, 0, + "#0-2 Double-clicking in <i> element should set end of selection to start of next text node (eat_space_to_next_word: true)"); + } else { + is(selection.getRangeAt(0).endContainer, i.firstChild, + "#0-2 Double-clicking in <i> element should set end of selection to end of the word in <i> (eat_space_to_next_word: false)"); + is(selection.getRangeAt(0).endOffset, "example".length, + "#0-2 Double-clicking in <i> element should set end of selection to end of the word in <i> (eat_space_to_next_word: false)"); + } + + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")), + null, + `#0-2 nsIHTMLEditor::getSelectedElement(\"\") should return null after double-clicking in <b> element (eat_space_to_next_word: ${eatSpaceToNextWord})`); + + // Both clicking and double-clicking on <img> element should "select" it. + synthesizeMouseAtCenter(img, {clickCount: 1}); + is(selection.getRangeAt(0).startContainer, img.parentElement, + `#0-3 Clicking in <img> element should set start of selection to the <img> element (eat_space_to_next_word: ${eatSpaceToNextWord})`); + is(selection.getRangeAt(0).startOffset, 1, + `#0-3 Clicking in <img> element should set start of selection to the <img> element (eat_space_to_next_word: ${eatSpaceToNextWord})`); + is(selection.getRangeAt(0).endContainer, img.parentElement, + `#0-3 Clicking in <img> element should set end of selection to start of next text node (eat_space_to_next_word: ${eatSpaceToNextWord})`); + is(selection.getRangeAt(0).endOffset, 2, + `#0-3 Clicking in <img> element should set end of selection to start of next text node (eat_space_to_next_word: ${eatSpaceToNextWord})`); + + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")), + img, + `#0-3 nsIHTMLEditor::getSelectedElement(\"\") should return the <img> element after clicking in it (eat_space_to_next_word: ${eatSpaceToNextWord})`); + + synthesizeMouseAtCenter(img, {clickCount: 1}); + synthesizeMouseAtCenter(img, {clickCount: 2}); + is(selection.getRangeAt(0).startContainer, img.parentElement, + `#0-4 Double-clicking in <img> element should set start of selection to the <img> element (eat_space_to_next_word: ${eatSpaceToNextWord})`); + is(selection.getRangeAt(0).startOffset, 1, + `#0-4 Double-clicking in <img> element should set start of selection to the <img> element (eat_space_to_next_word: ${eatSpaceToNextWord})`); + is(selection.getRangeAt(0).endContainer, img.parentElement, + `#0-4 Double-clicking in <img> element should set end of selection to start of next text node (eat_space_to_next_word: ${eatSpaceToNextWord})`); + is(selection.getRangeAt(0).endOffset, 2, + `#0-4 Double-clicking in <img> element should set end of selection to start of next text node (eat_space_to_next_word: ${eatSpaceToNextWord})`); + + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")), + img, + `#0-4 nsIHTMLEditor::getSelectedElement(\"\") should return the <img> element after double-clicking in it (eat_space_to_next_word: ${eatSpaceToNextWord})`); + + // Puts caret into the <a href> element. + synthesizeMouseAtCenter(href, {clickCount: 1}); + is(selection.getRangeAt(0).startContainer, href.firstChild, + `#0-5 Clicking in <a href> element should set start of selection to the text node in it (eat_space_to_next_word: ${eatSpaceToNextWord})`); + is(selection.isCollapsed, true, + `#0-5 Clicking in <a href> element should cause collapsing Selection (eat_space_to_next_word: ${eatSpaceToNextWord})`); + + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")), + null, + `#0-5 nsIHTMLEditor::getSelectedElement(\"\") should return null after clicking in the <a href> element (eat_space_to_next_word: ${eatSpaceToNextWord})`); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")), + href, + `#0-5 nsIHTMLEditor::getSelectedElement(\"href\") should return the <a href> element after clicking in it (eat_space_to_next_word: ${eatSpaceToNextWord})`); + + // Selects the <a href> element with a triple-click. + synthesizeMouseAtCenter(href, {clickCount: 1}); + synthesizeMouseAtCenter(href, {clickCount: 2}); + synthesizeMouseAtCenter(href, {clickCount: 3}); + is(selection.getRangeAt(0).startContainer, href.parentElement, + `#0-6 Triple-clicking in <a href> element should set start of selection to the element (eat_space_to_next_word: ${eatSpaceToNextWord})`); + is(selection.getRangeAt(0).startOffset, 1, + `#0-6 Triple-clicking in <a href> element should set start of selection to the element (eat_space_to_next_word: ${eatSpaceToNextWord})`); + is(selection.getRangeAt(0).endContainer, href.parentElement, + `#0-6 Triple-clicking in <a href> element should set end of selection to start of next <br> element (eat_space_to_next_word: ${eatSpaceToNextWord})`); + is(selection.getRangeAt(0).endOffset, 2, + `#0-6 Triple-clicking in <a href> element should set end of selection to start of next <br> element (eat_space_to_next_word: ${eatSpaceToNextWord})`); + + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")), + href, + `#0-6 nsIHTMLEditor::getSelectedElement(\"\") should return the <a href> element after double-clicking in it (eat_space_to_next_word: ${eatSpaceToNextWord})`); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")), + href, + `#0-6 nsIHTMLEditor::getSelectedElement(\"href\") should return the <a href> element after double-clicking in it (eat_space_to_next_word: ${eatSpaceToNextWord})`); + } + + editor.innerHTML = "<p>p1<b>b1</b><i>i1</i></p>"; + editor.focus(); + + // <p>[]p1... + let range = document.createRange(); + range.setStart(editor.firstChild.firstChild, 0); + range.setEnd(editor.firstChild.firstChild, 0); + selection.removeAllRanges(); + selection.addRange(range); + + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")), + null, + "#1-1 nsIHTMLEditor::getSelectedElement(\"\") should return null when selection is collapsed in a text node"); + + // <p>[p1]<b>... + range = document.createRange(); + range.setStart(editor.firstChild.firstChild, 0); + range.setEnd(editor.firstChild.firstChild, 2); + selection.removeAllRanges(); + selection.addRange(range); + + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")), + null, + "#1-2 nsIHTMLEditor::getSelectedElement(\"\") should return null when selection ends in a text node"); + + // <p>[p1<b>]b1</b>... + range = document.createRange(); + range.setStart(editor.firstChild.firstChild, 0); + range.setEnd(editor.firstChild.firstChild.nextSibling.firstChild, 0); + selection.removeAllRanges(); + selection.addRange(range); + + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")), + null, + "#1-3 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection ends at start of text node in <b> element"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("b")), + null, + "#1-3 nsIHTMLEditor::getSelectedElement(\"b\") should return null when Selection ends at start of text node in <b> element"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("i")), + editor.firstChild.nextSibling, + "#1-3 nsIHTMLEditor::getSelectedElement(\"i\") should return the <b> element when Selection ends at start of text node in <b> element"); + + // <p>[p1<b>b1]</b>... + range = document.createRange(); + range.setStart(editor.firstChild.firstChild, 0); + range.setEnd(editor.firstChild.firstChild.nextSibling.firstChild, 2); + selection.removeAllRanges(); + selection.addRange(range); + + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")), + null, + "#1-4 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection ends at end of text node in a text node"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("b")), + null, + "#1-4 nsIHTMLEditor::getSelectedElement(\"b\") should return null when Selection ends at end of text node in a text node"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("i")), + editor.firstChild.nextSibling, + "#1-4 nsIHTMLEditor::getSelectedElement(\"i\") should return the <b> element when Selection ends at end of text node in <b> element"); + + // <p>[p1}<b>b1... + range = document.createRange(); + range.setStart(editor.firstChild.firstChild, 0); + range.setEnd(editor.firstChild.firstChild.nextSibling, 0); + selection.removeAllRanges(); + selection.addRange(range); + + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")), + null, + "#1-5 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection ends at text node and there are no elements"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("b")), + null, + "#1-5 nsIHTMLEditor::getSelectedElement(\"b\") should return null when Selection ends at text node and there are no elements"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("i")), + null, + "#1-5 nsIHTMLEditor::getSelectedElement(\"i\") should return null when Selection ends at text node and there are no elements"); + + // <p>p1<b>{b1}</b>... + range = document.createRange(); + range.setStart(editor.firstChild.firstChild.nextSibling, 0); + range.setEnd(editor.firstChild.firstChild.nextSibling, 1); + selection.removeAllRanges(); + selection.addRange(range); + + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")), + null, + "#1-6 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection only selects a text node"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("b")), + null, + "#1-6 nsIHTMLEditor::getSelectedElement(\"b\") should return null when Selection only selects a text node"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("i")), + null, + "#1-6 nsIHTMLEditor::getSelectedElement(\"i\") should return null when Selection only selects a text node"); + + // <p>[p1<b>b1</b>}<i>... + range = document.createRange(); + range.setStart(editor.firstChild.firstChild, 0); + range.setEnd(editor.firstChild, 2); + selection.removeAllRanges(); + selection.addRange(range); + + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")), + null, + "#1-7 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection ends the <b> element but starts from the previous text node"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("b")), + null, + "#1-7 nsIHTMLEditor::getSelectedElement(\"b\") should return null when Selection ends the <b> element but starts from the previous text node"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("i")), + null, + "#1-7 nsIHTMLEditor::getSelectedElement(\"i\") should return null when Selection ends the <b> element but starts from the previous text node"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")), + null, + "#1-7 nsIHTMLEditor::getSelectedElement(\"href\") should return null when Selection ends the <b> element but starts from the previous text node"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")), + null, + "#1-7 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null when Selection ends the <b> element but starts from the previous text node"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("namedanchor")), + null, + "#1-7 nsIHTMLEditor::getSelectedElement(\"namedanchor\") should return null when Selection ends the <b> element but starts from the previous text node"); + + // <p>[p1<b>b1</b><i>i1</i>}... + range = document.createRange(); + range.setStart(editor.firstChild.firstChild, 0); + range.setEnd(editor.firstChild, 3); + selection.removeAllRanges(); + selection.addRange(range); + + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")), + null, + "#1-8 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection includes 2 elements and starts from previous text node"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("b")), + null, + "#1-8 nsIHTMLEditor::getSelectedElement(\"b\") should return null when Selection includes 2 elements and starts from previous text node"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("i")), + null, + "#1-8 nsIHTMLEditor::getSelectedElement(\"i\") should return null when Selection includes 2 elements and starts from previous text node"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")), + null, + "#1-8 nsIHTMLEditor::getSelectedElement(\"href\") should null when Selection includes 2 elements and starts from previous text node"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")), + null, + "#1-8 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null when Selection includes 2 elements and starts from previous text node"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("namedanchor")), + null, + "#1-8 nsIHTMLEditor::getSelectedElement(\"namedanchor\") should return null when Selection includes 2 elements and starts from previous text node"); + + // <p>p1{<b>b1</b>}<i>i1</i>... + // Note that this won't happen with user operation since Gecko sets + // start and end of Selection to points in text nodes as far as possible. + range = document.createRange(); + range.setStart(editor.firstChild, 1); + range.setEnd(editor.firstChild, 2); + selection.removeAllRanges(); + selection.addRange(range); + + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")), + editor.firstChild.firstChild.nextSibling, + "#1-9 nsIHTMLEditor::getSelectedElement(\"\") should return <b> element when only it is selected and matched"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("b")), + editor.firstChild.firstChild.nextSibling, + "#1-9 nsIHTMLEditor::getSelectedElement(\"b\") should return <b> element when only it is selected and matched"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("i")), + null, + "#1-9 nsIHTMLEditor::getSelectedElement(\"i\") should return null when only a <b> element is selected but it is unmatched"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")), + null, + "#1-9 nsIHTMLEditor::getSelectedElement(\"href\") should return null when only a <b> element is selected but it is unmatched"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")), + null, + "#1-9 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null when only a <b> element is selected but it is unmatched"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("namedanchor")), + null, + "#1-9 nsIHTMLEditor::getSelectedElement(\"namedanchor\") should return when only a <b> element is selected but it is unmatched"); + + // <p>p1<b>b1</b>{<i>i1</i>}</p>... + range = document.createRange(); + range.setStart(editor.firstChild, 2); + range.setEnd(editor.firstChild, 3); + selection.removeAllRanges(); + selection.addRange(range); + + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")), + editor.firstChild.firstChild.nextSibling.nextSibling, + "#1-10 nsIHTMLEditor::getSelectedElement(\"\") should return <i> element when only it is selected and matched"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("b")), + null, + "#1-10 nsIHTMLEditor::getSelectedElement(\"b\") should return null when only an <i> element is selected but it is unmatched"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("i")), + editor.firstChild.firstChild.nextSibling.nextSibling, + "#1-10 nsIHTMLEditor::getSelectedElement(\"i\") should return <i> element when only it is selected and matched"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")), + null, + "#1-10 nsIHTMLEditor::getSelectedElement(\"href\") should return null when only an <i> element is selected but it is unmatched"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")), + null, + "#1-10 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null when only an <i> element is selected but it is unmatched"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("namedanchor")), + null, + "#1-10 nsIHTMLEditor::getSelectedElement(\"namedanchor\") should return null when only an <i> element is selected but it is unmatched"); + + // <p>{p1}<b>b1</b><i>... + range = document.createRange(); + range.setStart(editor.firstChild, 0); + range.setEnd(editor.firstChild, 1); + selection.removeAllRanges(); + selection.addRange(range); + + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")), + null, + "#1-11 nsIHTMLEditor::getSelectedElement(\"\") should return null when only a text node is selected"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("b")), + null, + "#1-11 nsIHTMLEditor::getSelectedElement(\"b\") should return null when only a text node is selected"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("i")), + null, + "#1-11 nsIHTMLEditor::getSelectedElement(\"i\") should return null when only a text node is selected"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")), + null, + "#1-11 nsIHTMLEditor::getSelectedElement(\"href\") should return null when only a text node is selected"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")), + null, + "#1-11 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null when only a text node is selected"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("namedanchor")), + null, + "#1-11 nsIHTMLEditor::getSelectedElement(\"namedanchor\") should return null when only a text node is selected"); + + editor.innerHTML = "<p>p1<b>b1</b><b>b2</b><b>b3</b></p>"; + editor.focus(); + + // <p>p1<b>b[1</b><b>b2</b><b>b]3</b>... + range = document.createRange(); + range.setStart(editor.firstChild.firstChild.nextSibling.firstChild, 1); + range.setEnd(editor.firstChild.firstChild.nextSibling.nextSibling.nextSibling.firstChild, 1); + selection.removeAllRanges(); + selection.addRange(range); + + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")), + null, + "#2-1 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection is across 3 <b> elements"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("b")), + null, + "#2-1 nsIHTMLEditor::getSelectedElement(\"b\") should return null when Selection is across 3 <b> elements"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("i")), + null, + "#2-1 nsIHTMLEditor::getSelectedElement(\"i\") should return null when Selection is across 3 <b> elements"); + + // <p>p[1<b>b1</b><b>b2</b><b>b]3</b>... + range = document.createRange(); + range.setStart(editor.firstChild.firstChild, 1); + range.setEnd(editor.firstChild.firstChild.nextSibling.nextSibling.nextSibling.firstChild, 1); + selection.removeAllRanges(); + selection.addRange(range); + + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")), + null, + "#2-2 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection is across 3 <b> elements and previous text node"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("b")), + null, + "#2-2 nsIHTMLEditor::getSelectedElement(\"b\") should return null when Selection is across 3 <b> elements and previous text node"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("i")), + null, + "#2-2 nsIHTMLEditor::getSelectedElement(\"i\") should return null when Selection is across 3 <b> elements and previous text node"); + + editor.innerHTML = "<p>p1<b>b1<b>b2<b>b3</b></b></b>p2</p>"; + editor.focus(); + + // <p>p1<b>b[1<b>b1-2<b>b]1-3</b>... + range = document.createRange(); + range.setStart(editor.firstChild.firstChild.nextSibling.firstChild, 1); + range.setEnd(editor.firstChild.firstChild.nextSibling.firstChild.nextSibling.firstChild.nextSibling.firstChild, 1); + selection.removeAllRanges(); + selection.addRange(range); + + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")), + null, + "#3-1 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection is across 3 <b> elements which are nested"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("b")), + null, + "#3-1 nsIHTMLEditor::getSelectedElement(\"b\") should return null when Selection is across 3 <b> elements which are nested"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("i")), + null, + "#3-1 nsIHTMLEditor::getSelectedElement(\"i\") should return null when Selection is across 3 <b> elements which are nested"); + + // <p>p[1<b>b1<b>b1-2<b>b]1-3</b>... + range = document.createRange(); + range.setStart(editor.firstChild.firstChild, 1); + range.setEnd(editor.firstChild.firstChild.nextSibling.firstChild.nextSibling.firstChild.nextSibling.firstChild, 1); + selection.removeAllRanges(); + selection.addRange(range); + + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")), + null, + "#3-2 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection is across a text node and 3 <b> elements which are nested"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("b")), + null, + "#3-2 nsIHTMLEditor::getSelectedElement(\"b\") should return null when Selection is across a text node and 3 <b> elements which are nested"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("i")), + null, + "#3-2 nsIHTMLEditor::getSelectedElement(\"i\") should return null when Selection is across a text node and 3 <b> elements which are nested"); + + // <p>p1<b>b1<b>b1-2<b>b[1-3</b></b></b>p]2... + range = document.createRange(); + range.setStart(editor.firstChild.firstChild.nextSibling.firstChild.nextSibling.firstChild.nextSibling.firstChild, 1); + range.setEnd(editor.firstChild.firstChild.nextSibling.nextSibling, 1); + selection.removeAllRanges(); + selection.addRange(range); + + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")), + null, + "#3-3 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection is across 3 <b> elements which are nested and following text node"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("b")), + null, + "#3-3 nsIHTMLEditor::getSelectedElement(\"b\") should return null when Selection is across 3 <b> elements which are nested and following text node"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("i")), + null, + "#3-3 nsIHTMLEditor::getSelectedElement(\"i\") should return null when Selection is across 3 <b> elements which are nested and following text node"); + + editor.innerHTML = "<p><b>b1</b><a href=\"about:blank\">a1</a><a href=\"about:blank\">a2</a><a name=\"foo\">a3</a><b>b2</b></p>"; + editor.focus(); + + // <p><b>b1</b>{<a href="...">a1</a>}<a href="...">a2... + // Note that this won't happen with user operation since Gecko sets + // start and end of Selection to points in text nodes as far as possible. + range = document.createRange(); + range.setStart(editor.firstChild, 1); + range.setEnd(editor.firstChild, 2); + selection.removeAllRanges(); + selection.addRange(range); + + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")), + editor.firstChild.firstChild.nextSibling, + "#4-1 nsIHTMLEditor::getSelectedElement(\"\") should return the first <a> element when only it is selected and matched"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")), + editor.firstChild.firstChild.nextSibling, + "#4-1 nsIHTMLEditor::getSelectedElement(\"href\") should return the first <a> element when only it is selected and matched"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")), + null, + "#4-1 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null when the first <a> element is selected but it is unmatched"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("namedanchor")), + null, + "#4-1 nsIHTMLEditor::getSelectedElement(\"namedanchor\") should return null when the first <a> element is selected but it is unmatched"); + + // <p><b>b1</b><a href="...">a[]1</a><a href="...">a2... + range = document.createRange(); + range.setStart(editor.firstChild.firstChild.nextSibling.firstChild, 1); + range.setEnd(editor.firstChild.firstChild.nextSibling.firstChild, 1); + selection.removeAllRanges(); + selection.addRange(range); + + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")), + null, + "#4-2 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection is collapsed"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")), + editor.firstChild.firstChild.nextSibling, + "#4-2 nsIHTMLEditor::getSelectedElement(\"href\") should return the first <a> element when Selection is collapsed in the element"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")), + null, + "#4-2 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null when Selection is collapsed"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("namedanchor")), + null, + "#4-2 nsIHTMLEditor::getSelectedElement(\"namedanchor\") should return null when Selection is collapsed"); + + // <p><b>b1</b><a href="...">a[1]</a><a href="...">a2... + range = document.createRange(); + range.setStart(editor.firstChild.firstChild.nextSibling.firstChild, 1); + range.setEnd(editor.firstChild.firstChild.nextSibling.firstChild, 2); + selection.removeAllRanges(); + selection.addRange(range); + + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")), + null, + "#4-3 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection is in a text node"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")), + editor.firstChild.firstChild.nextSibling, + "#4-3 nsIHTMLEditor::getSelectedElement(\"href\") should return the first <a> element when Selection is in the element"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")), + null, + "#4-3 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null when Selection is in a text node"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("namedanchor")), + null, + "#4-3 nsIHTMLEditor::getSelectedElement(\"namedanchor\") should return null when Selection is in a text node"); + + // <p><b>b1</b><a href="...">a[1</a><a href="...">a]2... + range = document.createRange(); + range.setStart(editor.firstChild.firstChild.nextSibling.firstChild, 1); + range.setEnd(editor.firstChild.firstChild.nextSibling.nextSibling.firstChild, 1); + selection.removeAllRanges(); + selection.addRange(range); + + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")), + null, + "#4-4 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection crosses 2 <a> elements"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")), + null, + "#4-4 nsIHTMLEditor::getSelectedElement(\"href\") should return null when Selection crosses 2 <a> elements"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")), + null, + "#4-4 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null when Selection crosses 2 <a> elements"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("namedanchor")), + null, + "#4-4 nsIHTMLEditor::getSelectedElement(\"namedanchor\") should return null when Selection crosses 2 <a> elements"); + + // <p><b>b1</b><a href="...">a1</a><a href="...">a2</a>{<a name="...">a3</a>}<b>b2</b></p> + // Note that this won't happen with user operation since Gecko sets + // start and end of Selection to points in text nodes as far as possible. + range = document.createRange(); + range.setStart(editor.firstChild, 3); + range.setEnd(editor.firstChild, 4); + selection.removeAllRanges(); + selection.addRange(range); + + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")), + editor.firstChild.firstChild.nextSibling.nextSibling.nextSibling, + "#4-5 nsIHTMLEditor::getSelectedElement(\"\") should return the third <a> element when only it is selected and matched"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")), + null, + "#4-5 nsIHTMLEditor::getSelectedElement(\"href\") should return null when the third <a> element is selected but it is unmatched"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")), + editor.firstChild.firstChild.nextSibling.nextSibling.nextSibling, + "#4-5 nsIHTMLEditor::getSelectedElement(\"anchor\") should return the third <a> element when only it is selected and matched"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("namedanchor")), + null, + "#4-5 nsIHTMLEditor::getSelectedElement(\"namedanchor\") should return null when the third <a> element is selected but it is unmatched"); + + // <p><b>b1</b><a href="...">a1</a><a href="...">a2</a><a name="...">a[]3</a><b>b2</b></p> + range = document.createRange(); + range.setStart(editor.firstChild.firstChild.nextSibling.nextSibling.nextSibling.firstChild, 1); + range.setEnd(editor.firstChild.firstChild.nextSibling.nextSibling.nextSibling.firstChild, 1); + selection.removeAllRanges(); + selection.addRange(range); + + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")), + null, + "#4-6 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection is collapsed in a text node even in named anchor element"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")), + null, + "#4-6 nsIHTMLEditor::getSelectedElement(\"href\") should return null when Selection is collapsed in a text node even in named anchor element"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")), + null, + "#4-6 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null when Selection is collapsed in a text node even in named anchor element"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("namedanchor")), + null, + "#4-6 nsIHTMLEditor::getSelectedElement(\"namedanchor\") should return null when Selection is collapsed in a text node even in named anchor element"); + + editor.innerHTML = "<p>p1<b>b1</b>p1</p>"; + editor.focus(); + + // <p>p1[<b>b1</b>]p1</p> + // This is usual case that user selects <b> element with dragging or double-clicking. + range = document.createRange(); + range.setStart(editor.firstChild.firstChild, 2); + range.setEnd(editor.firstChild.firstChild.nextSibling.nextSibling, 0); + selection.removeAllRanges(); + selection.addRange(range); + + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")), + null, + "#5-1 nsIHTMLEditor::getSelectedElement(\"\") should return null even when Selection starts from end of preceding text node and ends at start or following text node of <b> element"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")), + null, + "#5-1 nsIHTMLEditor::getSelectedElement(\"href\") should return null even when Selection starts from end of preceding text node and ends at start or following text node of <b> element"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")), + null, + "#5-1 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null even when Selection starts from end of preceding text node and ends at start or following text node of <b> element"); + + // <p>p[1<b>b1</b>]p1</p> + range = document.createRange(); + range.setStart(editor.firstChild.firstChild, 1); + range.setEnd(editor.firstChild.firstChild.nextSibling.nextSibling, 0); + selection.removeAllRanges(); + selection.addRange(range); + + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")), + null, + "#5-2 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection ends at start of following text node of <b> element"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")), + null, + "#5-2 nsIHTMLEditor::getSelectedElement(\"href\") should return null when Selection ends at start of following text node of <b> element"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")), + null, + "#5-2 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null when Selection ends at start of following text node of <b> element"); + + // <p>p1[<b>b1</b>p]1</p> + range = document.createRange(); + range.setStart(editor.firstChild.firstChild, 2); + range.setEnd(editor.firstChild.firstChild.nextSibling.nextSibling, 1); + selection.removeAllRanges(); + selection.addRange(range); + + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")), + null, + "#5-3 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection starts from end of following text node of <b> element"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")), + null, + "#5-3 nsIHTMLEditor::getSelectedElement(\"href\") should return null when Selection starts from at end of following text node of <b> element"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")), + null, + "#5-3 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null when Selection starts from end of following text node of <b> element"); + + // <p>p1<b>[b1</b>]p1</p> + range = document.createRange(); + range.setStart(editor.firstChild.firstChild.nextSibling.firstChild, 0); + range.setEnd(editor.firstChild.firstChild.nextSibling.nextSibling, 0); + selection.removeAllRanges(); + selection.addRange(range); + + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")), + null, + "#5-4 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection starts from start of text node in <b> element and ends at start of following text node of <b> element"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")), + null, + "#5-4 nsIHTMLEditor::getSelectedElement(\"href\") should return null when Selection starts from start of text node in <b> element and ends at start of following text node of <b> element"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")), + null, + "#5-4 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null when Selection starts from start of text node in <b> element and ends at start of following text node of <b> element"); + + // <p>p1[<b>b1]</b>p1</p> + range = document.createRange(); + range.setStart(editor.firstChild.firstChild, 2); + range.setEnd(editor.firstChild.firstChild.nextSibling.firstChild, 2); + selection.removeAllRanges(); + selection.addRange(range); + + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")), + null, + "#5-5 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection starts from end of preceding text node of <b> element and ends at end of text node in <b> element"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")), + null, + "#5-5 nsIHTMLEditor::getSelectedElement(\"href\") should return null when Selection starts from end of preceding text node of <b> element and ends at end of text node in <b> element"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")), + null, + "#5-5 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null when Selection starts from end of preceding text node of <b> element and ends at end of text node in <b> element"); + + // <p>p1<b>b[1</b>}p1</p> + range = document.createRange(); + range.setStart(editor.firstChild.firstChild.nextSibling.firstChild, 1); + range.setEnd(editor.firstChild, 2); + selection.removeAllRanges(); + selection.addRange(range); + + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")), + editor.firstChild.firstChild.nextSibling, + "#5-6 nsIHTMLEditor::getSelectedElement(\"\") should the <b> element when Selection starts from in the <b> element's text node and end of preceding text node of <b> element and ends at end of the <b> element"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")), + null, + "#5-6 nsIHTMLEditor::getSelectedElement(\"href\") should return null when Selection starts from in the <b> element's text node and end of preceding text node of <b> element and ends at end of the <b> element"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")), + null, + "#5-6 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null when Selection starts from in the <b> element's text node and end of preceding text node of <b> element and ends at end of the <b> element"); + + editor.innerHTML = "<p>p1<b>b1</b></p>"; + editor.focus(); + + // <p>p1[<b>b1</b>}</p> + // This is usual case of double-clicking in the <b> element. + range = document.createRange(); + range.setStart(editor.firstChild.firstChild, 2); + range.setEnd(editor.firstChild, 2); + selection.removeAllRanges(); + selection.addRange(range); + + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")), + null, + "#6-1 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection starts from previous text node of <b> element and end at end of <b> element"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")), + null, + "#6-1 nsIHTMLEditor::getSelectedElement(\"href\") should return null when Selection starts from previous text node of <b> element and end at end of <b> element"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")), + null, + "#6-1 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null when Selection starts from previous text node of <b> element and end at end of <b> element"); + + editor.innerHTML = "<p><b>b1</b>p1</p>"; + editor.focus(); + + // <p><b>[b1</b>]p1</p> + // This is usual case of double-clicking in the <b> element. + range = document.createRange(); + range.setStart(editor.firstChild.firstChild.firstChild, 0); + range.setEnd(editor.firstChild.firstChild.nextSibling, 0); + selection.removeAllRanges(); + selection.addRange(range); + + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")), + null, + "#7-1 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection starts from a text node in <b> element and ends at start of following text node"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")), + null, + "#7-1 nsIHTMLEditor::getSelectedElement(\"href\") should return null when Selection starts from a text node in <b> element and ends at start of following text node"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")), + null, + "#7-1 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null when Selection starts from a text node in <b> element and ends at start of following text node"); + + editor.innerHTML = "<p>p1<b>b1</b><br></p>"; + editor.focus(); + + // <p>p1[<b>b1</b>}<br></p> + // This is usual case of double-clicking in the <b> element. + range = document.createRange(); + range.setStart(editor.firstChild.firstChild, 2); + range.setEnd(editor.firstChild, 2); + selection.removeAllRanges(); + selection.addRange(range); + + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")), + null, + "#8-1 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection starts from previous text node of <b> element and ends before the following <br> element"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")), + null, + "#8-1 nsIHTMLEditor::getSelectedElement(\"href\") should return null when Selection starts from previous text node of <b> element and ends before the following <br> element"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")), + null, + "#8-1 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null when Selection starts from previous text node of <b> element and ends before the following <br> element"); + + + editor.innerHTML = "<p><b>b1</b><br></p>"; + editor.focus(); + + // <p><b>[b1</b>}<br></p> + // This is usual case of double-clicking in the <b> element. + range = document.createRange(); + range.setStart(editor.firstChild.firstChild.firstChild, 0); + range.setEnd(editor.firstChild, 1); + selection.removeAllRanges(); + selection.addRange(range); + + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")), + null, + "#9-1 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection starts from a text node in <b> element and ends before the following <br> element"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")), + null, + "#9-1 nsIHTMLEditor::getSelectedElement(\"href\") should return null when Selection starts from a text node in <b> element and ends before the following <br> element"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")), + null, + "#9-1 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null when Selection starts from a text node in <b> element and ends before the following <br> element"); + + editor.innerHTML = "<p><b>b1</b><b><br></b><b>b2</b></p>"; + editor.focus(); + + // <p><b>[b1</b><b>}<br></b><b>b2</b></p> + // This is usual case of double-clicking in the first <b> element. + range = document.createRange(); + range.setStart(editor.firstChild.firstChild.firstChild, 0); + range.setEnd(editor.firstChild.firstChild.nextSibling, 0); + selection.removeAllRanges(); + selection.addRange(range); + + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("")), + null, + "#10-1 nsIHTMLEditor::getSelectedElement(\"\") should return null when Selection starts from a text node in <b> element and ends before the following <br> element in another <b> element"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("href")), + null, + "#10-1 nsIHTMLEditor::getSelectedElement(\"href\") should return null when Selection starts from a text node in <b> element and ends before the following <br> element in another <b> element"); + is(SpecialPowers.unwrap(getHTMLEditor().getSelectedElement("anchor")), + null, + "#10-1 nsIHTMLEditor::getSelectedElement(\"anchor\") should return null when Selection starts from a text node in <b> element and ends before the following <br> element in another <b> element"); + + SimpleTest.finish(); +}); + +function getHTMLEditor() { + var Ci = SpecialPowers.Ci; + var editingSession = SpecialPowers.wrap(window).docShell.editingSession; + return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsIHTMLEditor); +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_nsIHTMLEditor_insertElementAtSelection.html b/editor/libeditor/tests/test_nsIHTMLEditor_insertElementAtSelection.html new file mode 100644 index 0000000000..9a0b7450eb --- /dev/null +++ b/editor/libeditor/tests/test_nsIHTMLEditor_insertElementAtSelection.html @@ -0,0 +1,185 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<title>Testing `nsIHTMLEditor.insertElementAtSelection()`</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> +<script> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + +function getRangeDescription(range) { + function getNodeDescription(node) { + if (!node) { + return "null"; + } + switch (node.nodeType) { + case Node.TEXT_NODE: + case Node.COMMENT_NODE: + case Node.CDATA_SECTION_NODE: + return `${node.nodeName} "${node.data}"`; + case Node.ELEMENT_NODE: + return `<${node.nodeName.toLowerCase()}${ + node.hasAttribute("id") + ? ` id="${node.getAttribute("id")}"` + : "" + }${ + node.hasAttribute("class") + ? ` class="${node.getAttribute("class")}"` + : "" + }${ + node.hasAttribute("contenteditable") + ? ` contenteditable="${node.getAttribute("contenteditable")}"` + : "" + }>`; + default: + return `${node.nodeName}`; + } + } + if (range === null) { + return "null"; + } + if (range === undefined) { + return "undefined"; + } + return range.startContainer == range.endContainer && + range.startOffset == range.endOffset + ? `(${getNodeDescription(range.startContainer)}, ${range.startOffset})` + : `(${getNodeDescription(range.startContainer)}, ${ + range.startOffset + }) - (${getNodeDescription(range.endContainer)}, ${range.endOffset})`; +} + +function doTest(aEditingHost, aHTMLEditor, aDeleteSelection) { + const description = `aDeleteSelection=${aDeleteSelection}:`; + (() => { + aEditingHost.innerHTML = "abc"; + aEditingHost.focus(); + getSelection().collapse(aEditingHost.firstChild, 0); + const p = document.createElement("p"); + p.appendChild(document.createElement("br")); + aHTMLEditor.insertElementAtSelection(p, aDeleteSelection); + is( + aEditingHost.innerHTML, + "<p><br></p>abc", + `${description} The <p> element should be inserted before the text node when selection is collapsed at start of the text node` + ); + is( + getRangeDescription(getSelection().getRangeAt(0)), + '(<div contenteditable="">, 1)', + `${description} The selection should be collapsed after the inserted <p> element` + ); + })(); + (() => { + aEditingHost.innerHTML = "abc"; + aEditingHost.focus(); + getSelection().collapse(aEditingHost.firstChild, 1); + const p = document.createElement("p"); + p.appendChild(document.createElement("br")); + aHTMLEditor.insertElementAtSelection(p, aDeleteSelection); + is( + aEditingHost.innerHTML, + "a<p><br></p>bc", + `${description} The <p> element should be inserted middle of the text node when selection is collapsed at middle of the text node` + ); + is( + getRangeDescription(getSelection().getRangeAt(0)), + '(<div contenteditable="">, 2)', + `${description} The selection should be collapsed after the inserted <p> element (i.e., at right text node)` + ); + })(); + (() => { + aEditingHost.innerHTML = "abc"; + aEditingHost.focus(); + getSelection().collapse(aEditingHost.firstChild, 3); + const p = document.createElement("p"); + p.appendChild(document.createElement("br")); + aHTMLEditor.insertElementAtSelection(p, aDeleteSelection); + is( + aEditingHost.innerHTML, + "abc<p><br></p>", + `${description} The <p> element should be inserted after the text node when selection is collapsed at end of the text node` + ); + is( + getRangeDescription(getSelection().getRangeAt(0)), + '(<div contenteditable="">, 2)', + `${description} The selection should be collapsed at end of the editing host` + ); + })(); + (() => { + aEditingHost.innerHTML = "abc"; + aEditingHost.focus(); + getSelection().setBaseAndExtent(aEditingHost.firstChild, 0, aEditingHost.firstChild, 1); + const p = document.createElement("p"); + p.appendChild(document.createElement("br")); + aHTMLEditor.insertElementAtSelection(p, aDeleteSelection); + is( + aEditingHost.innerHTML, + aDeleteSelection ? "<p><br></p>bc" : "a<p><br></p>bc", + `${description} The <p> element should be inserted after selected character when selection selects the first character of the text node` + ); + is( + getRangeDescription(getSelection().getRangeAt(0)), + `(<div contenteditable="">, ${aDeleteSelection ? "1" : "2"})`, + `${description} The selection should be collapsed after the inserted <p> element (when selection selected the first character)` + ); + })(); + (() => { + aEditingHost.innerHTML = "abc"; + aEditingHost.focus(); + getSelection().setBaseAndExtent(aEditingHost.firstChild, 1, aEditingHost.firstChild, 2); + const p = document.createElement("p"); + p.appendChild(document.createElement("br")); + aHTMLEditor.insertElementAtSelection(p, aDeleteSelection); + is( + aEditingHost.innerHTML, + aDeleteSelection ? "a<p><br></p>c" : "ab<p><br></p>c", + `${description} The <p> element should be inserted after selected character when selection selects a middle character of the text node` + ); + is( + getRangeDescription(getSelection().getRangeAt(0)), + '(<div contenteditable="">, 2)', + `${description} The selection should be collapsed after the inserted <p> element (i.e., at right text node, when selection selected the middle character)` + ); + })(); + (() => { + aEditingHost.innerHTML = "abc"; + aEditingHost.focus(); + getSelection().setBaseAndExtent(aEditingHost.firstChild, 2, aEditingHost.firstChild, 3); + const p = document.createElement("p"); + p.appendChild(document.createElement("br")); + aHTMLEditor.insertElementAtSelection(p, aDeleteSelection); + is( + aEditingHost.innerHTML, + aDeleteSelection ? "ab<p><br></p>" : "abc<p><br></p>", + `${description} The <p> element should be inserted after selected character when selection selects the last character of the text node` + ); + is( + getRangeDescription(getSelection().getRangeAt(0)), + '(<div contenteditable="">, 2)', + `${description} The selection should be collapsed at end of the editing host (when selection selected the last character)` + ); + })(); +} + +async function runTests() { + const editingHost = document.createElement("div"); + editingHost.setAttribute("contenteditable", ""); + document.body.appendChild(editingHost); + const editor = + SpecialPowers. + wrap(window). + docShell. + editingSession. + getEditorForWindow(window). + QueryInterface(SpecialPowers.Ci.nsIHTMLEditor); + doTest(editingHost, editor, true); + doTest(editingHost, editor, false); + SimpleTest.finish(); +} +</script> +</body> +</html> diff --git a/editor/libeditor/tests/test_nsIHTMLEditor_removeInlineProperty.html b/editor/libeditor/tests/test_nsIHTMLEditor_removeInlineProperty.html new file mode 100644 index 0000000000..7f7e297efe --- /dev/null +++ b/editor/libeditor/tests/test_nsIHTMLEditor_removeInlineProperty.html @@ -0,0 +1,420 @@ +<!DOCTYPE> +<html> +<head> + <title>Test for nsIHTMLEditor.removeInlineProperty()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> +<div id="display"> +</div> +<div id="content" contenteditable></div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> +"use strict"; + +SimpleTest.waitForExplicitFinish(); + +SimpleTest.waitForFocus(() => { + let editor = document.getElementById("content"); + let selection = window.getSelection(); + let description, condition; + let beforeInputEvents = []; + let inputEvents = []; + let selectionRanges = []; + function onBeforeInput(aEvent) { + beforeInputEvents.push(aEvent); + selectionRanges = []; + for (let i = 0; i < selection.rangeCount; i++) { + let range = selection.getRangeAt(i); + selectionRanges.push({startContainer: range.startContainer, startOffset: range.startOffset, + endContainer: range.endContainer, endOffset: range.endOffset}); + } + } + function onInput(aEvent) { + inputEvents.push(aEvent); + } + editor.addEventListener("beforeinput", onBeforeInput); + editor.addEventListener("input", onInput); + + function checkInputEvent(aEvent, aInputType, aDescription) { + if (aEvent.type !== "input" && aEvent.type !== "beforeinput") { + return; + } + ok(aEvent instanceof InputEvent, `${aDescription}"${aEvent.type}" event should be dispatched with InputEvent interface`); + // If it were cancelable whose inputType is empty string, web apps would + // block any Firefox specific modification whose inputType are not declared + // by the spec. + let expectedCancelable = aEvent.type === "beforeinput" && aInputType !== ""; + is(aEvent.cancelable, expectedCancelable, + `${aDescription}"${aEvent.type}" event should ${expectedCancelable ? "be" : "be never"} cancelable`); + is(aEvent.bubbles, true, `${aDescription}"${aEvent.type}" event should always bubble`); + is(aEvent.inputType, aInputType, `${aDescription}inputType of "${aEvent.type}" event should be ${aInputType}`); + is(aEvent.data, null, `${aDescription}data of "${aEvent.type}" event should be null`); + is(aEvent.dataTransfer, null, `${aDescription}dataTransfer of "${aEvent.type}" event should be null`); + let targetRanges = aEvent.getTargetRanges(); + if (aEvent.type === "beforeinput") { + is(targetRanges.length, selectionRanges.length, + `${aDescription}getTargetRanges() of "${aEvent.type}" event should return selection ranges`); + if (targetRanges.length === selectionRanges.length) { + for (let i = 0; i < selectionRanges.length; i++) { + is(targetRanges[i].startContainer, selectionRanges[i].startContainer, + `${aDescription}startContainer of getTargetRanges()[${i}] of "beforeinput" event does not match`); + is(targetRanges[i].startOffset, selectionRanges[i].startOffset, + `${aDescription}startOffset of getTargetRanges()[${i}] of "beforeinput" event does not match`); + is(targetRanges[i].endContainer, selectionRanges[i].endContainer, + `${aDescription}endContainer of getTargetRanges()[${i}] of "beforeinput" event does not match`); + is(targetRanges[i].endOffset, selectionRanges[i].endOffset, + `${aDescription}endOffset of getTargetRanges()[${i}] of "beforeinput" event does not match`); + } + } + } else { + is(targetRanges.length, 0, `${aDescription}getTargetRanges() of "${aEvent.type}" event should return empty array`); + } + } + + function selectFromTextSiblings(aNode) { + condition = "selecting the node from end of previous text to start of next text node"; + selection.setBaseAndExtent(aNode.previousSibling, aNode.previousSibling.length, + aNode.nextSibling, 0); + } + function selectNode(aNode) { + condition = "selecting the node"; + let range = document.createRange(); + range.selectNode(aNode); + selection.removeAllRanges(); + selection.addRange(range); + } + function selectAllChildren(aNode) { + condition = "selecting all children of the node"; + selection.selectAllChildren(aNode); + } + function selectChildContents(aNode) { + condition = "selecting all contents of its child"; + let range = document.createRange(); + range.selectNodeContents(aNode.firstChild); + selection.removeAllRanges(); + selection.addRange(range); + } + + description = "When there is a <b> element and "; + for (let prepare of [selectFromTextSiblings, selectNode, selectAllChildren, selectChildContents]) { + editor.innerHTML = "<p>test: <b>here</b> is bolden text</p>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + prepare(editor.firstChild.firstChild.nextSibling); + getHTMLEditor().removeInlineProperty("b", ""); + is(editor.innerHTML, "<p>test: here is bolden text</p>", + description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should remove the <b> element'); + is(beforeInputEvents.length, 1, + description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should cause a "beforeinput" event'); + checkInputEvent(beforeInputEvents[0], "formatBold", description + condition + ': nsIHTMLEditor.removeInlineProperty("b", ""): '); + is(inputEvents.length, 1, + description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should cause an "input" event'); + checkInputEvent(inputEvents[0], "formatBold", description + condition + ': nsIHTMLEditor.removeInlineProperty("b", ""): '); + } + + description = "When there is a <b> element which has style attribute and "; + for (let prepare of [selectFromTextSiblings, selectNode, selectAllChildren, selectChildContents]) { + editor.innerHTML = '<p>test: <b style="font-style: italic">here</b> is bolden text</p>'; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + prepare(editor.firstChild.firstChild.nextSibling); + getHTMLEditor().removeInlineProperty("b", ""); + is(editor.innerHTML, '<p>test: <span style="font-style: italic">here</span> is bolden text</p>', + description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should replace the <b> element with <span> element to keep the style'); + is(beforeInputEvents.length, 1, + description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should cause a "beforeinput" event'); + checkInputEvent(beforeInputEvents[0], "formatBold", description + condition + ': nsIHTMLEditor.removeInlineProperty("b", ""): '); + is(inputEvents.length, 1, + description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should cause an "input" event'); + checkInputEvent(inputEvents[0], "formatBold", description + condition + ': nsIHTMLEditor.removeInlineProperty("b", ""): '); + } + + description = "When there is a <b> element which has class attribute and "; + for (let prepare of [selectFromTextSiblings, selectNode, selectAllChildren, selectChildContents]) { + editor.innerHTML = '<p>test: <b class="foo">here</b> is bolden text</p>'; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + prepare(editor.firstChild.firstChild.nextSibling); + getHTMLEditor().removeInlineProperty("b", ""); + is(editor.innerHTML, '<p>test: <span class="foo">here</span> is bolden text</p>', + description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should replace the <b> element with <span> element to keep the class'); + is(beforeInputEvents.length, 1, + description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should cause a "beforeinput" event'); + checkInputEvent(beforeInputEvents[0], "formatBold", description + condition + ': nsIHTMLEditor.removeInlineProperty("b", ""): '); + is(inputEvents.length, 1, + description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should cause an "input" event'); + checkInputEvent(inputEvents[0], "formatBold", description + condition + ': nsIHTMLEditor.removeInlineProperty("b", ""): '); + } + + description = "When there is a <b> element which has an <i> element and "; + for (let prepare of [selectFromTextSiblings, selectNode, selectAllChildren, selectChildContents]) { + editor.innerHTML = "<p>test: <b><i>here</i></b> is bolden and italic text</p>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + prepare(editor.firstChild.firstChild.nextSibling); + getHTMLEditor().removeInlineProperty("b", ""); + is(editor.innerHTML, "<p>test: <i>here</i> is bolden and italic text</p>", + description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should remove only the <b> element'); + is(beforeInputEvents.length, 1, + description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should cause a "beforeinput" event'); + checkInputEvent(beforeInputEvents[0], "formatBold", description + condition + ': nsIHTMLEditor.removeInlineProperty("b", ""): '); + is(inputEvents.length, 1, + description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should cause an "input" event'); + checkInputEvent(inputEvents[0], "formatBold", description + condition + ': nsIHTMLEditor.removeInlineProperty("b", ""): '); + } + + description = "When there is a <b> element which has an <i> element and "; + for (let prepare of [selectFromTextSiblings, selectNode, selectAllChildren, selectChildContents]) { + editor.innerHTML = "<p>test: <b><i>here</i></b> is bolden and italic text</p>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + prepare(editor.firstChild.firstChild.nextSibling); + getHTMLEditor().removeInlineProperty("i", ""); + is(editor.innerHTML, "<p>test: <b>here</b> is bolden and italic text</p>", + description + condition + ': nsIHTMLEditor.removeInlineProperty("i", "") should remove only the <i> element'); + is(beforeInputEvents.length, 1, + description + condition + ': nsIHTMLEditor.removeInlineProperty("i", "") should cause a "beforeinput" event'); + checkInputEvent(beforeInputEvents[0], "formatItalic", description + condition + ': nsIHTMLEditor.removeInlineProperty("i", ""): '); + is(inputEvents.length, 1, + description + condition + ': nsIHTMLEditor.removeInlineProperty("i", "") should cause an "input" event'); + checkInputEvent(inputEvents[0], "formatItalic", description + condition + ': nsIHTMLEditor.removeInlineProperty("i", ""): '); + } + + description = "When there is an <i> element in a <b> element and "; + for (let prepare of [selectNode, selectAllChildren, selectChildContents]) { + editor.innerHTML = "<p>test: <b><i>here</i></b> is bolden and italic text</p>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + prepare(editor.firstChild.firstChild.nextSibling.firstChild); + getHTMLEditor().removeInlineProperty("b", ""); + is(editor.innerHTML, "<p>test: <i>here</i> is bolden and italic text</p>", + description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should remove only the <b> element'); + is(beforeInputEvents.length, 1, + description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should cause a "beforeinput" event'); + checkInputEvent(beforeInputEvents[0], "formatBold", description + condition + ': nsIHTMLEditor.removeInlineProperty("b", ""): '); + is(inputEvents.length, 1, + description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should cause an "input" event'); + checkInputEvent(inputEvents[0], "formatBold", description + condition + ': nsIHTMLEditor.removeInlineProperty("b", ""): '); + } + + description = "When there is an <i> element in a <b> element and "; + for (let prepare of [selectNode, selectAllChildren, selectChildContents]) { + editor.innerHTML = "<p>test: <b><i>here</i></b> is bolden and italic text</p>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + prepare(editor.firstChild.firstChild.nextSibling.firstChild); + getHTMLEditor().removeInlineProperty("i", ""); + is(editor.innerHTML, "<p>test: <b>here</b> is bolden and italic text</p>", + description + condition + ': nsIHTMLEditor.removeInlineProperty("i", "") should remove only the <i> element'); + is(beforeInputEvents.length, 1, + description + condition + ': nsIHTMLEditor.removeInlineProperty("i", "") should cause a "beforeinput" event'); + checkInputEvent(beforeInputEvents[0], "formatItalic", description + condition + ': nsIHTMLEditor.removeInlineProperty("i", ""): '); + is(inputEvents.length, 1, + description + condition + ': nsIHTMLEditor.removeInlineProperty("i", "") should cause an "input" event'); + checkInputEvent(inputEvents[0], "formatItalic", description + condition + ': nsIHTMLEditor.removeInlineProperty("i", ""): '); + } + + description = "When there is an <i> element between text nodes in a <b> element and "; + for (let prepare of [selectNode, selectAllChildren, selectChildContents]) { + editor.innerHTML = "<p>test: <b>h<i>e</i>re</b> is bolden and italic text</p>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + prepare(editor.firstChild.firstChild.nextSibling.firstChild.nextSibling); + getHTMLEditor().removeInlineProperty("i", ""); + is(editor.innerHTML, "<p>test: <b>here</b> is bolden and italic text</p>", + description + condition + ': nsIHTMLEditor.removeInlineProperty("i", "") should remove only the <i> element'); + is(beforeInputEvents.length, 1, + description + condition + ': nsIHTMLEditor.removeInlineProperty("i", "") should cause a "beforeinput" event'); + checkInputEvent(beforeInputEvents[0], "formatItalic", description + condition + ': nsIHTMLEditor.removeInlineProperty("i", ""): '); + is(inputEvents.length, 1, + description + condition + ': nsIHTMLEditor.removeInlineProperty("i", "") should cause an "input" event'); + checkInputEvent(inputEvents[0], "formatItalic", description + condition + ': nsIHTMLEditor.removeInlineProperty("i", ""): '); + } + + description = "When there is an <i> element between text nodes in a <b> element and "; + for (let prepare of [selectNode, selectAllChildren, selectChildContents]) { + editor.innerHTML = "<p>test: <b>h<i>e</i>re</b> is bolden and italic text</p>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + prepare(editor.firstChild.firstChild.nextSibling.firstChild.nextSibling); + getHTMLEditor().removeInlineProperty("b", ""); + is(editor.innerHTML, "<p>test: <b>h</b><i>e</i><b>re</b> is bolden and italic text</p>", + description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should split the <b> element'); + is(beforeInputEvents.length, 1, + description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should cause a "beforeinput" event'); + checkInputEvent(beforeInputEvents[0], "formatBold", description + condition + ': nsIHTMLEditor.removeInlineProperty("b", ""): '); + is(inputEvents.length, 1, + description + condition + ': nsIHTMLEditor.removeInlineProperty("b", "") should cause an "input" event'); + checkInputEvent(inputEvents[0], "formatBold", description + condition + ': nsIHTMLEditor.removeInlineProperty("b", ""): '); + } + + description = "When there is an <a> element whose href attribute is not empty and "; + for (let prepare of [selectFromTextSiblings, selectNode, selectAllChildren, selectChildContents]) { + editor.innerHTML = '<p>test: <a href="about:blank">here</a> is a link</p>'; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + prepare(editor.firstChild.firstChild.nextSibling); + getHTMLEditor().removeInlineProperty("href", ""); + is(editor.innerHTML, "<p>test: here is a link</p>", + description + condition + ': nsIHTMLEditor.removeInlineProperty("href", "") should remove the <a> element'); + is(beforeInputEvents.length, 1, + description + condition + ': nsIHTMLEditor.removeInlineProperty("href", "") should cause a "beforeinput" event'); + checkInputEvent(beforeInputEvents[0], "", description + condition + ': nsIHTMLEditor.removeInlineProperty("href", ""): '); + is(inputEvents.length, 1, + description + condition + ': nsIHTMLEditor.removeInlineProperty("href", "") should cause an "input" event'); + checkInputEvent(inputEvents[0], "", description + condition + ': nsIHTMLEditor.removeInlineProperty("href", ""): '); + } + + // XXX In the case of "name", removeInlineProperty() does not the <a> element when name attribute is empty. + description = "When there is an <a> element whose href attribute is empty and "; + for (let prepare of [selectFromTextSiblings, selectNode, selectAllChildren, selectChildContents]) { + editor.innerHTML = '<p>test: <a href="">here</a> is a link</p>'; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + prepare(editor.firstChild.firstChild.nextSibling); + getHTMLEditor().removeInlineProperty("href", ""); + is(editor.innerHTML, "<p>test: here is a link</p>", + description + condition + ': nsIHTMLEditor.removeInlineProperty("href", "") should remove the <a> element'); + is(beforeInputEvents.length, 1, + description + condition + ': nsIHTMLEditor.removeInlineProperty("href", "") should cause a "beforeinput" event'); + checkInputEvent(beforeInputEvents[0], "", description + condition + ': nsIHTMLEditor.removeInlineProperty("href", ""): '); + is(inputEvents.length, 1, + description + condition + ': nsIHTMLEditor.removeInlineProperty("href", "") should cause an "input" event'); + checkInputEvent(inputEvents[0], "", description + condition + ': nsIHTMLEditor.removeInlineProperty("href", ""): '); + } + + description = "When there is an <a> element which does not have href attribute and "; + for (let prepare of [selectFromTextSiblings, selectNode, selectAllChildren, selectChildContents]) { + editor.innerHTML = "<p>test: <a>here</a> is an anchor</p>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + prepare(editor.firstChild.firstChild.nextSibling); + getHTMLEditor().removeInlineProperty("href", ""); + is(editor.innerHTML, "<p>test: <a>here</a> is an anchor</p>", + description + condition + ': nsIHTMLEditor.removeInlineProperty("href", "") should NOT remove the <a> element'); + is(beforeInputEvents.length, 1, + description + condition + ': nsIHTMLEditor.removeInlineProperty("href", "") should cause a "beforeinput" event even though HTMLEditor will do nothing'); + checkInputEvent(beforeInputEvents[0], "", description + condition + ': nsIHTMLEditor.removeInlineProperty("href", ""): '); + is(inputEvents.length, 0, + description + condition + ': nsIHTMLEditor.removeInlineProperty("href", "") should NOT cause an "input" event'); + } + + description = "When there is an <a> element whose name attribute is not empty and "; + for (let prepare of [selectFromTextSiblings, selectNode, selectAllChildren, selectChildContents]) { + editor.innerHTML = '<p>test: <a name="foo">here</a> is a named anchor</p>'; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + prepare(editor.firstChild.firstChild.nextSibling); + getHTMLEditor().removeInlineProperty("href", ""); + is(editor.innerHTML, '<p>test: <a name="foo">here</a> is a named anchor</p>', + description + condition + ': nsIHTMLEditor.removeInlineProperty("href", "") should NOT remove the <a> element'); + is(beforeInputEvents.length, 1, + description + condition + ': nsIHTMLEditor.removeInlineProperty("href", "") should cause a "beforeinput" event even though HTMLEditor will do nothing'); + checkInputEvent(beforeInputEvents[0], "", description + condition + ': nsIHTMLEditor.removeInlineProperty("href", ""): '); + is(inputEvents.length, 0, + description + condition + ': nsIHTMLEditor.removeInlineProperty("href", "") should NOT cause an "input" event'); + } + + description = "When there is an <a> element whose name attribute is not empty and "; + for (let prepare of [selectFromTextSiblings, selectNode, selectAllChildren, selectChildContents]) { + editor.innerHTML = '<p>test: <a name="foo">here</a> is a named anchor</p>'; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + prepare(editor.firstChild.firstChild.nextSibling); + getHTMLEditor().removeInlineProperty("name", ""); + is(editor.innerHTML, "<p>test: here is a named anchor</p>", + description + condition + ': nsIHTMLEditor.removeInlineProperty("name", "") should remove the <a> element'); + is(beforeInputEvents.length, 1, + description + condition + ': nsIHTMLEditor.removeInlineProperty("name", "") should cause a "beforeinput" event'); + checkInputEvent(beforeInputEvents[0], "", description + condition + ': nsIHTMLEditor.removeInlineProperty("name", ""): '); + is(inputEvents.length, 1, + description + condition + ': nsIHTMLEditor.removeInlineProperty("name", "") should cause an "input" event'); + checkInputEvent(inputEvents[0], "", description + condition + ': nsIHTMLEditor.removeInlineProperty("name", ""): '); + } + + // XXX In the case of "href", removeInlineProperty() removes the <a> element when href attribute is empty. + description = "When there is an <a> element whose name attribute is empty and "; + for (let prepare of [selectFromTextSiblings, selectNode, selectAllChildren, selectChildContents]) { + editor.innerHTML = '<p>test: <a name="">here</a> is a named anchor</p>'; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + prepare(editor.firstChild.firstChild.nextSibling); + getHTMLEditor().removeInlineProperty("name", ""); + is(editor.innerHTML, '<p>test: <a name="">here</a> is a named anchor</p>', + description + condition + ': nsIHTMLEditor.removeInlineProperty("name", "") should NOT remove the <a> element'); + is(beforeInputEvents.length, 1, + description + condition + ': nsIHTMLEditor.removeInlineProperty("name", "") should cause a "beforeinput" event even though HTMLEditor will do nothing'); + checkInputEvent(beforeInputEvents[0], "", description + condition + ': nsIHTMLEditor.removeInlineProperty("name", ""): '); + is(inputEvents.length, 0, + description + condition + ': nsIHTMLEditor.removeInlineProperty("name", "") should NOT cause an "input" event'); + } + + description = "When there is an <a> element which does not have name attribute and "; + for (let prepare of [selectFromTextSiblings, selectNode, selectAllChildren, selectChildContents]) { + editor.innerHTML = "<p>test: <a>here</a> is an anchor</p>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + prepare(editor.firstChild.firstChild.nextSibling); + getHTMLEditor().removeInlineProperty("name", ""); + is(editor.innerHTML, "<p>test: <a>here</a> is an anchor</p>", + description + condition + ': nsIHTMLEditor.removeInlineProperty("name", "") should NOT remove the <a> element'); + is(beforeInputEvents.length, 1, + description + condition + ': nsIHTMLEditor.removeInlineProperty("name", "") should cause a "beforeinput" event even though HTMLEditor will do nothing'); + checkInputEvent(beforeInputEvents[0], "", description + condition + ': nsIHTMLEditor.removeInlineProperty("name", "")'); + is(inputEvents.length, 0, + description + condition + ': nsIHTMLEditor.removeInlineProperty("name", "") should NOT cause an "input" event'); + } + + description = "When there is an <a> element whose href attribute is not empty and "; + for (let prepare of [selectFromTextSiblings, selectNode, selectAllChildren, selectChildContents]) { + editor.innerHTML = '<p>test: <a href="about:blank">here</a> is a link</p>'; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + prepare(editor.firstChild.firstChild.nextSibling); + getHTMLEditor().removeInlineProperty("name", ""); + is(editor.innerHTML, '<p>test: <a href="about:blank">here</a> is a link</p>', + description + condition + ': nsIHTMLEditor.removeInlineProperty("name", "") should NOT remove the <a> element'); + is(beforeInputEvents.length, 1, + description + condition + ': nsIHTMLEditor.removeInlineProperty("name", "") should cause a "beforeinput" event even though HTMLEditor will do nothing'); + checkInputEvent(beforeInputEvents[0], "", description + condition + ': nsIHTMLEditor.removeInlineProperty("name", "")'); + is(inputEvents.length, 0, + description + condition + ': nsIHTMLEditor.removeInlineProperty("name", "") should NOT cause an "input" event'); + } + + editor.removeEventListener("beforeinput", onBeforeInput); + editor.removeEventListener("input", onInput); + + SimpleTest.finish(); +}); + +function getHTMLEditor() { + var Ci = SpecialPowers.Ci; + var editingSession = SpecialPowers.wrap(window).docShell.editingSession; + return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsIHTMLEditor); +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_nsIHTMLEditor_selectElement.html b/editor/libeditor/tests/test_nsIHTMLEditor_selectElement.html new file mode 100644 index 0000000000..1ac21635ea --- /dev/null +++ b/editor/libeditor/tests/test_nsIHTMLEditor_selectElement.html @@ -0,0 +1,131 @@ +<!DOCTYPE> +<html> +<head> + <title>Test for nsIHTMLEditor.selectElement()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> +<div id="display"> +</div> +<div id="content" contenteditable></div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + let editor = document.getElementById("content"); + let selection = window.getSelection(); + + editor.innerHTML = "<p>p1<b>b1</b><i>i1</i></p><p><b>b2</b><i>i2</i>p2</p>"; + + editor.focus(); + try { + getHTMLEditor().selectElement(editor.firstChild.firstChild); + ok(false, "nsIHTMLEditor.selectElement() should throw an exception if given node is not an element"); + } catch (e) { + ok(true, `nsIHTMLEditor.selectElement() should throw an exception if given node is not an element: ${e}`); + } + + editor.focus(); + try { + getHTMLEditor().selectElement(editor.firstChild.firstChild.nextSibling); + is(selection.anchorNode, editor.firstChild, + "nsIHTMLEditor.selectElement() should set anchor node to parent of <b> element in the first paragraph"); + is(selection.anchorOffset, 1, + "nsIHTMLEditor.selectElement() should set anchor offset to the index of <b> element in the first paragraph"); + is(selection.focusNode, editor.firstChild, + "nsIHTMLEditor.selectElement() should set focus node to parent of <b> element in the first paragraph"); + is(selection.focusOffset, 2, + "nsIHTMLEditor.selectElement() should set anchor offset to the index of <b> element + 1 in the first paragraph"); + } catch (e) { + ok(false, `nsIHTMLEditor.selectElement() shouldn't throw exception when selecting an element in focused editor #1: ${e}`); + } + + editor.focus(); + try { + getHTMLEditor().selectElement(editor.firstChild.nextSibling.firstChild); + is(selection.anchorNode, editor.firstChild.nextSibling, + "nsIHTMLEditor.selectElement() should set anchor node to parent of <b> element in the second paragraph"); + is(selection.anchorOffset, 0, + "nsIHTMLEditor.selectElement() should set anchor offset to the index of <b> element in the second paragraph"); + is(selection.focusNode, editor.firstChild.nextSibling, + "nsIHTMLEditor.selectElement() should set focus node to parent of <b> element in the second paragraph"); + is(selection.focusOffset, 1, + "nsIHTMLEditor.selectElement() should set anchor offset to the index of <b> element + 1 in the second paragraph"); + } catch (e) { + ok(false, `nsIHTMLEditor.selectElement() shouldn't throw exception when selecting an element in focused editor #2: ${e}`); + } + + editor.focus(); + try { + getHTMLEditor().selectElement(editor); + ok(false, "nsIHTMLEditor.selectElement() should throw an exception if given node is the editing host"); + } catch (e) { + ok(true, `nsIHTMLEditor.selectElement() should throw an exception if given node is the editing host: ${e}`); + } + + editor.focus(); + try { + getHTMLEditor().selectElement(editor.parentElement); + ok(false, "nsIHTMLEditor.selectElement() should throw an exception if given node is outside of the editing host"); + } catch (e) { + ok(true, `nsIHTMLEditor.selectElement() should throw an exception if given node is outside of the editing host: ${e}`); + } + + selection.removeAllRanges(); + editor.blur(); + try { + getHTMLEditor().selectElement(editor.firstChild.nextSibling.firstChild); + ok(false, "nsIHTMLEditor.selectElement() should throw an exception if there is no active editing host"); + } catch (e) { + ok(true, `nsIHTMLEditor.selectElement() should throw an exception if there is no active editing host: ${e}`); + } + + editor.focus(); + editor.firstChild.firstChild.nextSibling.nextSibling.setAttribute("contenteditable", "false"); + try { + getHTMLEditor().selectElement(editor.firstChild.firstChild.nextSibling.nextSibling); + is(selection.anchorNode, editor.firstChild, + "nsIHTMLEditor.selectElement() should set anchor node to parent of <i contenteditable=\"false\"> element in the first paragraph"); + is(selection.anchorOffset, 2, + "nsIHTMLEditor.selectElement() should set anchor offset to the index of <i contenteditable=\"false\"> element in the first paragraph"); + is(selection.focusNode, editor.firstChild, + "nsIHTMLEditor.selectElement() should set focus node to parent of <i contenteditable=\"false\"> element in the first paragraph"); + is(selection.focusOffset, 3, + "nsIHTMLEditor.selectElement() should set anchor offset to the index of <i contenteditable=\"false\"> element + 1 in the first paragraph"); + } catch (e) { + ok(false, `nsIHTMLEditor.selectElement() shouldn't throw exception when selecting an element in focused editor #3: ${e}`); + } + + editor.focus(); + editor.firstChild.nextSibling.setAttribute("contenteditable", "false"); + try { + getHTMLEditor().selectElement(editor.firstChild.nextSibling.firstChild.nextSibling); + is(selection.anchorNode, editor.firstChild.nextSibling, + "nsIHTMLEditor.selectElement() should set anchor node to parent of <i> element in the second paragraph which is not editable"); + is(selection.anchorOffset, 1, + "nsIHTMLEditor.selectElement() should set anchor offset to the index of <i> element in the second paragraph which is not editable"); + is(selection.focusNode, editor.firstChild.nextSibling, + "nsIHTMLEditor.selectElement() should set focus node to parent of <i> element in the second paragraph which is not editable"); + is(selection.focusOffset, 2, + "nsIHTMLEditor.selectElement() should set anchor offset to the index of <i> element + 1 in the second paragraph which is not editable"); + } catch (e) { + ok(false, `nsIHTMLEditor.selectElement() shouldn't throw exception when selecting an element in focused editor #4: ${e}`); + } + + SimpleTest.finish(); +}); + +function getHTMLEditor() { + var Ci = SpecialPowers.Ci; + var editingSession = SpecialPowers.wrap(window).docShell.editingSession; + return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsIHTMLEditor); +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_nsIHTMLEditor_setBackgroundColor.html b/editor/libeditor/tests/test_nsIHTMLEditor_setBackgroundColor.html new file mode 100644 index 0000000000..1d67e7b688 --- /dev/null +++ b/editor/libeditor/tests/test_nsIHTMLEditor_setBackgroundColor.html @@ -0,0 +1,187 @@ +<!DOCTYPE> +<html> +<head> + <title>Test for nsIHTMLEditor.setBackgroundColor()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> +<div id="display"> +</div> +<div id="content" contenteditable></div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> +"use strict"; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + let editor = document.getElementById("content"); + let selection = document.getSelection(); + + (function test_with_selecting_table_cells() { + editor.innerHTML = + '<table id="table">' + + '<tr id="r1"><td id="c1-1">cell1-1</td><td id="c1-2">cell1-2</td><td id="c1-3">cell1-3</td><td id="c1-4" colspan="2" rowspan="2">cell1-4</td></tr>' + + '<tr id="r2"><th id="c2-1" rowspan="2">cell2-1</th><td id="c2-2">cell2-2</td><td id="c2-3">cell2-3</td></tr>' + + '<tr id="r3"><td id="c3-2">cell3-2</td><td id="c3-3">cell3-3</td><td id="c3-4" colspan="2">cell3-4</td></tr>' + + '<tr id="r4"><td id="c4-1" rowspan="4">cell4-1</td><td id="c4-2">cell4-2</td><td id="c4-3">cell4-3</td><th id="c4-4">cell4-4</th><td id="c4-5">cell4-5</td></tr>' + + '<tr id="r5"><th id="c5-2">cell5-2</th><th id="c5-3" colspan="2">' + + '<table><tr id="r2-1"><td id="c2-1-1">cell2-1-1</td></tr></table>' + + '</th><td id="c5-5">cell5-5</td></tr>' + + '<tr id="r6"><td id="c6-2">cell6-2</td><td id="c6-3">cell6-3</td><td id="c6-4"><p>cell6-4</p></td><td id="c6-5">cell6-5</td></tr>' + + '<tr id="r7"><td id="c7-2" colspan="4">cell7-2</td></tr>' + + "</table>"; + + (function test_without_CSS() { + (function test_with_selecting_1_1() { + let cell = document.getElementById("c1-1"); + let range = document.createRange(); + range.selectNode(cell); + selection.removeAllRanges(); + selection.addRange(range); + getHTMLEditor().setBackgroundColor("#ff0000"); + is(cell.getAttribute("bgcolor"), "#ff0000", + '#1-1 nsIHTMLEditor.setBackgroundColor("#ff0000") should set "bgcolor" attribute of the first cell element in the first row'); + ok(!cell.nextSibling.hasAttribute("bgcolor"), + '#1-1 nsIHTMLEditor.setBackgroundColor("#ff0000") should not set "bgcolor" attribute of the other cells'); + ok(!cell.parentNode.hasAttribute("bgcolor"), + '#1-1 nsIHTMLEditor.setBackgroundColor("#ff0000") should not set "bgcolor" attribute of the parent row'); + ok(!cell.parentNode.parentNode.hasAttribute("bgcolor"), + '#1-1 nsIHTMLEditor.setBackgroundColor("#ff0000") should not set "bgcolor" attribute of the parent tbody'); + ok(!cell.parentNode.parentNode.parentNode.hasAttribute("bgcolor"), + '#1-1 nsIHTMLEditor.setBackgroundColor("#ff0000") should not set "bgcolor" attribute of the parent table'); + ok(!editor.hasAttribute("bgcolor"), + '#1-1 nsIHTMLEditor.setBackgroundColor("#ff0000") should not set "bgcolor" attribute of the editing host'); + ok(!document.body.hasAttribute("bgcolor"), + '#1-1 nsIHTMLEditor.setBackgroundColor("#ff0000") should not set "bgcolor" attribute of the body'); + ok(!document.documentElement.hasAttribute("bgcolor"), + '#1-1 nsIHTMLEditor.setBackgroundColor("#ff0000") should not set "bgcolor" attribute of the html'); + + getHTMLEditor().setBackgroundColor(""); + ok(!cell.hasAttribute("bgcolor"), + '#1-1 nsIHTMLEditor.setBackgroundColor("") should remove "bgcolor" attribute of the first cell element in the first row'); + })(); + + (function test_with_selecting_2_2_and_2_3() { + let cell2_2 = editor.querySelector("#c2-2"); + let cell2_3 = editor.querySelector("#c2-3"); + selection.removeAllRanges(); + let range = document.createRange(); + range.selectNode(cell2_2); + selection.addRange(range); + range = document.createRange(); + range.selectNode(cell2_3); + selection.addRange(range); + getHTMLEditor().setBackgroundColor("#ff0000"); + is(cell2_2.getAttribute("bgcolor"), "#ff0000", + '#2-2, #2-3 nsIHTMLEditor.setBackgroundColor("#ff0000") should set "bgcolor" attribute of the second cell element in the second row'); + is(cell2_3.getAttribute("bgcolor"), "#ff0000", + '#2-2, #2-3 nsIHTMLEditor.setBackgroundColor("#ff0000") should set "bgcolor" attribute of the third cell element in the second row'); + ok(!cell2_2.parentNode.hasAttribute("bgcolor"), + '#2-2, #2-3 nsIHTMLEditor.setBackgroundColor("#ff0000") should not set "bgcolor" attribute of the parent row'); + ok(!cell2_2.parentNode.parentNode.hasAttribute("bgcolor"), + '#2-2, #2-3 nsIHTMLEditor.setBackgroundColor("#ff0000") should not set "bgcolor" attribute of the parent tbody'); + ok(!cell2_2.parentNode.parentNode.parentNode.hasAttribute("bgcolor"), + '#2-2, #2-3 nsIHTMLEditor.setBackgroundColor("#ff0000") should not set "bgcolor" attribute of the parent table'); + ok(!editor.hasAttribute("bgcolor"), + '#2-2, #2-3 nsIHTMLEditor.setBackgroundColor("#ff0000") should not set "bgcolor" attribute of the editing host'); + ok(!document.body.hasAttribute("bgcolor"), + '#2-2, #2-3 nsIHTMLEditor.setBackgroundColor("#ff0000") should not set "bgcolor" attribute of the body'); + ok(!document.documentElement.hasAttribute("bgcolor"), + '#2-2, #2-3 nsIHTMLEditor.setBackgroundColor("#ff0000") should not set "bgcolor" attribute of the html'); + + getHTMLEditor().setBackgroundColor(""); + ok(!cell2_2.hasAttribute("bgcolor"), + '#2-2, #2-3 nsIHTMLEditor.setBackgroundColor("") should remove "bgcolor" attribute of the second cell element in the second row'); + ok(!cell2_3.hasAttribute("bgcolor"), + '#2-2, #2-3 nsIHTMLEditor.setBackgroundColor("") should remove "bgcolor" attribute of the third cell element in the second row'); + })(); + + (function test_with_selecting_6_3_and_paragraph_in_6_4_and_6_5() { + selection.removeAllRanges(); + let cell6_3 = editor.querySelector("#c6-3"); + let cell6_4 = editor.querySelector("#c6-4"); + let cell6_5 = editor.querySelector("#c6-5"); + let range = document.createRange(); + range.selectNode(cell6_3); + selection.addRange(range); + range = document.createRange(); + range.selectNode(cell6_4.firstChild); + selection.addRange(range); + range = document.createRange(); + range.selectNode(cell6_5); + selection.addRange(range); + getHTMLEditor().setBackgroundColor("#ff0000"); + is(cell6_3.getAttribute("bgcolor"), "#ff0000", + '#6-3, #6-5 nsIHTMLEditor.setBackgroundColor("#ff0000") should set "bgcolor" attribute of the second cell element in the sixth row'); + ok(!cell6_4.hasAttribute("bgcolor"), + '#6-3, #6-5 nsIHTMLEditor.setBackgroundColor("#ff0000") should not set "bgcolor" attribute of the third cell element in the sixth row'); + ok(!cell6_4.firstChild.hasAttribute("bgcolor"), + '#6-3, #6-5 nsIHTMLEditor.setBackgroundColor("#ff0000") should not set "bgcolor" attribute of the paragraph in the third cell element in the sixth row'); + is(cell6_5.getAttribute("bgcolor"), "#ff0000", + '#6-3, #6-5 nsIHTMLEditor.setBackgroundColor("#ff0000") should set "bgcolor" attribute of the forth cell element in the sixth row'); + + getHTMLEditor().setBackgroundColor(""); + ok(!cell6_3.hasAttribute("bgcolor"), + '#6-3, #6-5 nsIHTMLEditor.setBackgroundColor("") should remove "bgcolor" attribute of the second cell element in the sixth row'); + ok(!cell6_5.hasAttribute("bgcolor"), + '#6-3, #6-5 nsIHTMLEditor.setBackgroundColor("") should remove "bgcolor" attribute of the third cell element in the sixth row'); + })(); + + (function test_with_selecting_3_2_and_2_1_1_and_7_2() { + let cell3_2 = editor.querySelector("#c3-2"); + let cell2_1_1 = editor.querySelector("#c2-1-1"); + let cell6_5 = editor.querySelector("#c6-5"); + selection.removeAllRanges(); + let range = document.createRange(); + range.selectNode(cell3_2); + selection.addRange(range); + range = document.createRange(); + range.selectNode(cell2_1_1); + selection.addRange(range); + range = document.createRange(); + range.selectNode(cell6_5); + selection.addRange(range); + + getHTMLEditor().setBackgroundColor("#ff0000"); + is(cell3_2.getAttribute("bgcolor"), "#ff0000", + '#3-2, #2-1-1, #6-5 nsIHTMLEditor.setBackgroundColor("#ff0000") should set "bgcolor" attribute of the first cell element in the third row'); + is(cell2_1_1.getAttribute("bgcolor"), "#ff0000", + '#3-2, #2-1-1, #6-5 nsIHTMLEditor.setBackgroundColor("#ff0000") should set "bgcolor" attribute of the cell element in the child table'); + ok(!cell2_1_1.parentNode.hasAttribute("bgcolor"), + '#3-2, #2-1-1, #6-5 nsIHTMLEditor.setBackgroundColor("#ff0000") should not set "bgcolor" attribute of the row in the child table'); + ok(!cell2_1_1.parentNode.parentNode.hasAttribute("bgcolor"), + '#3-2, #2-1-1, #6-5 nsIHTMLEditor.setBackgroundColor("#ff0000") should not set "bgcolor" attribute of the tbody in the child table'); + ok(!cell2_1_1.parentNode.parentNode.parentNode.hasAttribute("bgcolor"), + '#3-2, #2-1-1, #6-5 nsIHTMLEditor.setBackgroundColor("#ff0000") should not set "bgcolor" attribute of the table in the child table'); + ok(!cell2_1_1.parentNode.parentNode.parentNode.parentNode.hasAttribute("bgcolor"), + '#3-2, #2-1-1, #6-5 nsIHTMLEditor.setBackgroundColor("#ff0000") should not set "bgcolor" attribute of the cell having the child table'); + is(cell6_5.getAttribute("bgcolor"), "#ff0000", + '#3-2, #2-1-1, #6-5 nsIHTMLEditor.setBackgroundColor("#ff0000") should set "bgcolor" attribute of the forth cell element in the sixth row'); + + getHTMLEditor().setBackgroundColor(""); + ok(!cell3_2.hasAttribute("bgcolor"), + '#3-2, #2-1-1, #6-5 nsIHTMLEditor.setBackgroundColor("") should remove "bgcolor" attribute of the first cell element in the third row'); + ok(!cell2_1_1.hasAttribute("bgcolor"), + '#3-2, #2-1-1, #6-5 nsIHTMLEditor.setBackgroundColor("") should remove "bgcolor" attribute of the cell element in the child table'); + ok(!cell6_5.hasAttribute("bgcolor"), + '#3-2, #2-1-1, #6-5 nsIHTMLEditor.setBackgroundColor("") should remove "bgcolor" attribute of the third cell element in the sixth row'); + })(); + })(); + })(); + + SimpleTest.finish(); +}); + +function getHTMLEditor() { + var Ci = SpecialPowers.Ci; + var editingSession = SpecialPowers.wrap(window).docShell.editingSession; + return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsIHTMLEditor); +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_nsIHTMLObjectResizer_hideResizers.html b/editor/libeditor/tests/test_nsIHTMLObjectResizer_hideResizers.html new file mode 100644 index 0000000000..39679a546d --- /dev/null +++ b/editor/libeditor/tests/test_nsIHTMLObjectResizer_hideResizers.html @@ -0,0 +1,54 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test for nsIHTMLObjectResizer.hideResizers()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none;"> + +</div> + +<div id="editor" contenteditable></div> +<img src="green.png"><!-- for ensuring to load the image at first test of <img> case --> +<pre id="test"> + +<script class="testbody" type="application/javascript"> +"use strict"; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(async function() { + async function waitForSelectionChange() { + return new Promise(resolve => { + document.addEventListener("selectionchange", () => { + resolve(); + }, {once: true}); + }); + } + + document.execCommand("enableObjectResizing", false, true); + let editor = document.getElementById("editor"); + editor.innerHTML = "<img id=\"target\" src=\"green.png\" width=\"100\" height=\"100\">"; + let img = document.getElementById("target"); + let promiseSelectionChangeEvent = waitForSelectionChange(); + synthesizeMouseAtCenter(img, {}); + await promiseSelectionChangeEvent; + ok(img.hasAttribute("_moz_resizing"), "resizers of the <img> should be visible"); + getHTMLObjectResizer().hideResizers(); + ok(!img.hasAttribute("_moz_resizing"), "resizers of the <img> should be hidden after a call of hideResizers()"); + + SimpleTest.finish(); +}); + +function getHTMLObjectResizer() { + var Ci = SpecialPowers.Ci; + var editingSession = SpecialPowers.wrap(window).docShell.editingSession; + return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsIHTMLObjectResizer); +} +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_nsITableEditor_deleteTableCell.html b/editor/libeditor/tests/test_nsITableEditor_deleteTableCell.html new file mode 100644 index 0000000000..b17fcd6930 --- /dev/null +++ b/editor/libeditor/tests/test_nsITableEditor_deleteTableCell.html @@ -0,0 +1,716 @@ +<!DOCTYPE> +<html> +<head> + <title>Test for nsITableEditor.deleteTableCell()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> +<div id="display"> +</div> +<div id="content" contenteditable>out of table<table><tr><td>default content</td></tr></table></div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> +"use strict"; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + let editor = document.getElementById("content"); + let selection = document.getSelection(); + let selectionRanges = []; + + function checkInputEvent(aEvent, aDescription) { + ok(aEvent instanceof InputEvent, + `"${aEvent.type}" event should be dispatched with InputEvent interface ${aDescription}`); + is(aEvent.cancelable, aEvent.type === "beforeinput", + `"${aEvent.type}" event should be ${aEvent.type === "beforeinput" ? "be" : "never cancelable"} ${aDescription}`); + is(aEvent.bubbles, true, + `"${aEvent.type}" event should always bubble ${aDescription}`); + is(aEvent.inputType, "deleteContent", + `inputType of "${aEvent.type}" event should be "deleteContent" ${aDescription}`); + is(aEvent.data, null, + `data of "${aEvent.type}" event should be null ${aDescription}`); + is(aEvent.dataTransfer, null, + `dataTransfer of "${aEvent.type}" event should be null ${aDescription}`); + let targetRanges = aEvent.getTargetRanges(); + if (aEvent.type === "beforeinput") { + is(targetRanges.length, selectionRanges.length, + `getTargetRanges() of "beforeinput" event should return selection ranges ${aDescription}`); + if (targetRanges.length === selectionRanges.length) { + for (let i = 0; i < selectionRanges.length; i++) { + is(targetRanges[i].startContainer, selectionRanges[i].startContainer, + `startContainer of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`); + is(targetRanges[i].startOffset, selectionRanges[i].startOffset, + `startOffset of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`); + is(targetRanges[i].endContainer, selectionRanges[i].endContainer, + `endContainer of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`); + is(targetRanges[i].endOffset, selectionRanges[i].endOffset, + `endOffset of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`); + } + } + } else { + is(targetRanges.length, 0, + `getTargetRanges() of "${aEvent.type}" event should return empty array ${aDescription}`); + } + } + + let beforeInputEvents = []; + let inputEvents = []; + function onBeforeInput(aEvent) { + beforeInputEvents.push(aEvent); + selectionRanges = []; + for (let i = 0; i < selection.rangeCount; i++) { + let range = selection.getRangeAt(i); + selectionRanges.push({startContainer: range.startContainer, startOffset: range.startOffset, + endContainer: range.endContainer, endOffset: range.endOffset}); + } + } + function onInput(aEvent) { + inputEvents.push(aEvent); + } + editor.addEventListener("beforeinput", onBeforeInput); + editor.addEventListener("input", onInput); + + beforeInputEvents = []; + inputEvents = []; + selection.collapse(editor.firstChild, 0); + getTableEditor().deleteTableCell(1); + is(editor.innerHTML, "out of table<table><tbody><tr><td>default content</td></tr></tbody></table>", + "nsITableEditor.deleteTableCell(1) should do nothing if selection is not in <table>"); + // If there were specific inputType value for this API, we should dispatch cancelable "beforeinput", though. + is(beforeInputEvents.length, 1, + '"beforeinput" event should be fired when a call of nsITableEditor.deleteTableCell(1) even though it will do nothing'); + checkInputEvent(beforeInputEvents[0], "when selection is collapsed outside table element"); + is(inputEvents.length, 0, + 'No "input" event should be fired when a call of nsITableEditor.deleteTableCell(1) does nothing'); + + selection.removeAllRanges(); + try { + beforeInputEvents = []; + inputEvents = []; + getTableEditor().deleteTableCell(1); + ok(false, "getTableEditor().deleteTableCell(1) without selection ranges should throw exception"); + } catch (e) { + ok(true, "getTableEditor().deleteTableCell(1) without selection ranges should throw exception"); + is(beforeInputEvents.length, 0, + 'No "beforeinput" event should be fired when nsITableEditor.deleteTableCell(1) causes exception due to no selection range'); + is(inputEvents.length, 0, + 'No "input" event should be fired when nsITableEditor.deleteTableCell(1) causes exception due to no selection range'); + } + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td id="select">cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>' + + "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" + + "</table>"; + beforeInputEvents = []; + inputEvents = []; + let range = document.createRange(); + range.selectNode(document.getElementById("select")); + selection.addRange(range); + getTableEditor().deleteTableCell(1); + is(editor.innerHTML, "<table><tbody>" + + "<tr><td>cell1-2</td><td>cell1-3</td></tr>" + + "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" + + "</tbody></table>", + "nsITableEditor.deleteTableCellContents(1) should remove only selected cell when only one cell is selected #1-1"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when only one cell is selected #1-1'); + checkInputEvent(beforeInputEvents[0], "when only one cell is selected #1-1"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when only one cell is selected #1-1'); + checkInputEvent(inputEvents[0], "when only one cell is selected #1-1"); + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td>cell1-1</td><td id="select">cell1-2</td><td>cell1-3</td></tr>' + + "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" + + "</table>"; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select")); + selection.addRange(range); + getTableEditor().deleteTableCell(1); + is(editor.innerHTML, "<table><tbody>" + + "<tr><td>cell1-1</td><td>cell1-3</td></tr>" + + "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" + + "</tbody></table>", + "nsITableEditor.deleteTableCellContents(1) should remove only selected cell when only one cell is selected #1-2"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when only one cell is selected #1-2'); + checkInputEvent(beforeInputEvents[0], "when only one cell is selected #1-2"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when only one cell is selected #1-2'); + checkInputEvent(inputEvents[0], "when only one cell is selected #1-2"); + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td>cell1-1</td><td>cell1-2</td><td id="select">cell1-3</td></tr>' + + "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" + + "</table>"; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select")); + selection.addRange(range); + getTableEditor().deleteTableCell(1); + is(editor.innerHTML, "<table><tbody>" + + "<tr><td>cell1-1</td><td>cell1-2</td></tr>" + + "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" + + "</tbody></table>", + "nsITableEditor.deleteTableCellContents(1) should remove only selected cell when only one cell is selected #1-3"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when only one cell is selected #1-3'); + checkInputEvent(beforeInputEvents[0], "when only one cell is selected #1-3"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when only one cell is selected #1-3'); + checkInputEvent(inputEvents[0], "when only one cell is selected #1-3"); + + // When only one cell element is selected, the argument should be used. + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td id="select">cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>' + + "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" + + "</table>"; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select")); + selection.addRange(range); + getTableEditor().deleteTableCell(2); + is(editor.innerHTML, "<table><tbody>" + + "<tr><td>cell1-3</td></tr>" + + "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" + + "</tbody></table>", + "nsITableEditor.deleteTableCellContents(2) should remove selected cell element and next cell element in same row #1-4"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when only one cell is selected #1-4'); + checkInputEvent(beforeInputEvents[0], "when only one cell is selected #1-4"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when only one cell is selected #1-4'); + checkInputEvent(inputEvents[0], "when only one cell is selected #1-4"); + + // When the argument is larger than remaining cell elements from selected + // cell element, the behavior is really buggy. + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td>cell1-1</td><td>cell1-2</td><td id="select">cell1-3</td></tr>' + + "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" + + "</table>"; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select")); + selection.addRange(range); + getTableEditor().deleteTableCell(2); + is(editor.innerHTML, "<table><tbody>" + + "<tr><td>cell1-1</td></tr>" + + "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" + + "</tbody></table>", + "nsITableEditor.deleteTableCellContents(2) should remove selected cell element and its previous cell element when it reaches the last cell element in the row"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when the argument is larger than remaining cell elements from selected cell element'); + checkInputEvent(beforeInputEvents[0], "when the argument is larger than remaining cell elements from selected cell element"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when the argument is larger than remaining cell elements from selected cell element'); + checkInputEvent(inputEvents[0], "when the argument is larger than remaining cell elements from selected cell element"); + + // XXX If the former case is expected, first row should be removed in this + // case, but it removes only selected cell and its previous cell. + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td>cell1-1</td><td>cell1-2</td><td id="select">cell1-3</td></tr>' + + "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" + + "</table>"; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select")); + selection.addRange(range); + getTableEditor().deleteTableCell(4); + todo_is(editor.innerHTML, "<table><tbody>" + + "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" + + "</tbody></table>", + "nsITableEditor.deleteTableCellContents(4) should remove the first row when a cell in it is selected and the argument is larger than number of cells in the row"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when the argument is larger than number of cells in the row'); + checkInputEvent(beforeInputEvents[0], "when the argument is larger than number of cells in the row"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when the argument is larger than number of cells in the row'); + checkInputEvent(inputEvents[0], "when the argument is larger than number of cells in the row"); + + // If 2 or more cells are selected, the argument should be ignored. + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td id="select1">cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>' + + '<tr><td>cell2-1</td><td id="select2">cell2-2</td><td>cell2-3</td></tr>' + + "</table>"; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select1")); + selection.addRange(range); + range = document.createRange(); + range.selectNode(document.getElementById("select2")); + selection.addRange(range); + getTableEditor().deleteTableCell(1); + is(editor.innerHTML, "<table><tbody>" + + "<tr><td>cell1-2</td><td>cell1-3</td></tr>" + + "<tr><td>cell2-1</td><td>cell2-3</td></tr>" + + "</tbody></table>", + "nsITableEditor.deleteTableCellContents(1) should remove selected cell elements even if the argument is smaller than number of selected cells"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired even if the argument is smaller than number of selected cells'); + checkInputEvent(beforeInputEvents[0], "even if the argument is smaller than number of selected cells"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired even if the argument is smaller than number of selected cells'); + checkInputEvent(inputEvents[0], "even if the argument is smaller than number of selected cells"); + + // If all cells in a row are selected, the <tr> element should also be removed. + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td id="select1">cell1-1</td><td id="select2">cell1-2</td><td id="select3">cell1-3</td></tr>' + + "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" + + "</table>"; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select1")); + selection.addRange(range); + range = document.createRange(); + range.selectNode(document.getElementById("select2")); + selection.addRange(range); + range = document.createRange(); + range.selectNode(document.getElementById("select3")); + selection.addRange(range); + getTableEditor().deleteTableCell(1); + is(editor.innerHTML, "<table><tbody>" + + "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" + + "</tbody></table>", + "nsITableEditor.deleteTableCellContents(1) should remove also <tr> element when all cell elements in a row is selected"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when all cell elements in a row is selected'); + checkInputEvent(beforeInputEvents[0], "when all cell elements in a row is selected"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when all cell elements in a row is selected'); + checkInputEvent(inputEvents[0], "when all cell elements in a row is selected"); + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td id="select">cell1-1</td></tr>' + + "<tr><td>cell2-1</td></tr>" + + "</table>"; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select")); + selection.addRange(range); + getTableEditor().deleteTableCell(1); + is(editor.innerHTML, "<table><tbody>" + + "<tr><td>cell2-1</td></tr>" + + "</tbody></table>", + "nsITableEditor.deleteTableCellContents(1) should remove also <tr> element when a cell element which is only child of a row is selected"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when a cell element which is only child of a row is selected'); + checkInputEvent(beforeInputEvents[0], "when a cell element which is only child of a row is selected"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when a cell element which is only child of a row is selected'); + checkInputEvent(inputEvents[0], "when a cell element which is only child of a row is selected"); + + // If all cells are removed, the <table> element should be removed. + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td id="select1">cell1-1</td><td id="select2">cell1-2</td></tr>' + + '<tr><td id="select3">cell2-1</td><td id="select4">cell2-2</td></tr>' + + "</table>"; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select1")); + selection.addRange(range); + range = document.createRange(); + range.selectNode(document.getElementById("select2")); + selection.addRange(range); + range = document.createRange(); + range.selectNode(document.getElementById("select3")); + selection.addRange(range); + range = document.createRange(); + range.selectNode(document.getElementById("select4")); + selection.addRange(range); + getTableEditor().deleteTableCell(1); + is(editor.innerHTML, "", + "nsITableEditor.deleteTableCellContents(1) should remove also <table> element when all cell elements are selected"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when all cell elements are selected'); + checkInputEvent(beforeInputEvents[0], "when all cell elements are selected"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when all cell elements are selected'); + checkInputEvent(inputEvents[0], "when all cell elements are selected"); + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td id="select">cell1-1</td></tr>' + + "</table>"; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select")); + selection.addRange(range); + getTableEditor().deleteTableCell(1); + is(editor.innerHTML, "", + "nsITableEditor.deleteTableCellContents(1) should remove also <table> element when a cell element which is only child of <table> is selected"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when a cell element which is only child of <table> is selected'); + checkInputEvent(beforeInputEvents[0], "when a cell element which is only child of <table> is selected"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when a cell element which is only child of <table> is selected'); + checkInputEvent(inputEvents[0], "when a cell element which is only child of <table> is selected"); + + // rowspan + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td id="select" rowspan="2">cell1-1</td><td>cell1-2</td></tr>' + + "<tr><td>cell2-2</td></tr>" + + "<tr><td>cell3-1</td><td>cell3-2</td></tr>" + + "</table>"; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select")); + selection.addRange(range); + getTableEditor().deleteTableCell(1); + is(editor.innerHTML, "<table><tbody>" + + "<tr><td>cell1-2</td></tr>" + + "<tr><td>cell2-2</td></tr>" + + "<tr><td>cell3-1</td><td>cell3-2</td></tr>" + + "</tbody></table>", + "nsITableEditor.deleteTableCellContents(1) should just remove the cell spanning rows"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when removing the cell spanning rows'); + checkInputEvent(beforeInputEvents[0], "when removing the cell spanning rows"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when removing the cell spanning rows'); + checkInputEvent(inputEvents[0], "when removing the cell spanning rows"); + + // XXX cell3-1 is also removed even though it's not selected. + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td id="select1" rowspan="2">cell1-1</td><td>cell1-2</td></tr>' + + "<tr><td>cell2-2</td></tr>" + + '<tr><td>cell3-1</td><td id="select2">cell3-2</td></tr>' + + "</table>"; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select1")); + selection.addRange(range); + range = document.createRange(); + range.selectNode(document.getElementById("select2")); + selection.addRange(range); + getTableEditor().deleteTableCell(1); + todo_is(editor.innerHTML, "<table><tbody>" + + "<tr><td>cell1-2</td></tr>" + + "<tr><td>cell2-2</td></tr>" + + "<tr><td>cell3-1</td></tr>" + + "</tbody></table>", + "nsITableEditor.deleteTableCellContents(1) should just remove the cell spanning rows (when 2 cell elements are selected)"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when removing the cell spanning rows (when 2 cell elements are selected)'); + checkInputEvent(beforeInputEvents[0], "when removing the cell spanning rows (when 2 cell elements are selected)"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when removing the cell spanning rows (when 2 cell elements are selected)'); + checkInputEvent(inputEvents[0], "when removing the cell spanning rows (when 2 cell elements are selected)"); + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td rowspan="2">cell1-1</td><td>cell1-2</td></tr>' + + '<tr><td id="select">cell2-2</td></tr>' + + "<tr><td>cell3-1</td><td>cell3-2</td></tr>" + + "</table>"; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select")); + selection.addRange(range); + getTableEditor().deleteTableCell(1); + is(editor.innerHTML, "<table><tbody>" + + '<tr><td rowspan="1">cell1-1</td><td>cell1-2</td></tr>' + + "<tr><td>cell3-1</td><td>cell3-2</td></tr>" + + "</tbody></table>", + "nsITableEditor.deleteTableCellContents(1) should adjust rowspan when spanned <tr>'s last child cell element is removed"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when spanned <tr>\'s last child cell element is removed'); + checkInputEvent(beforeInputEvents[0], "when spanned <tr>'s last child cell element is removed"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when spanned <tr>\'s last child cell element is removed'); + checkInputEvent(inputEvents[0], "when spanned <tr>'s last child cell element is removed"); + + // XXX broken case, the second row isn't removed. + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td rowspan="2">cell1-1</td><td>cell1-2</td></tr>' + + '<tr><td id="select1">cell2-2</td></tr>' + + '<tr><td>cell3-1</td><td id="select2">cell3-2</td></tr>' + + "</table>"; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select1")); + selection.addRange(range); + range = document.createRange(); + range.selectNode(document.getElementById("select2")); + selection.addRange(range); + getTableEditor().deleteTableCell(1); + todo_is(editor.innerHTML, "<table><tbody>" + + '<tr><td rowspan="1">cell1-1</td><td>cell1-2</td></tr>' + + "<tr><td>cell3-1</td></tr>" + + "</tbody></table>", + "nsITableEditor.deleteTableCellContents(1) should adjust rowspan when spanned <tr>'s last child cell element is removed (when 2 cell elements are selected)"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when spanned <tr>\'s last child cell element is removed (when 2 cell elements are selected)'); + checkInputEvent(beforeInputEvents[0], "when spanned <tr>'s last child cell element is removed (when 2 cell elements are selected)"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when spanned <tr>\'s last child cell element is removed (when 2 cell elements are selected)'); + checkInputEvent(inputEvents[0], "when spanned <tr>'s last child cell element is removed (when 2 cell elements are selected)"); + + // XXX broken case, neither the selected cell nor the second row is removed. + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td rowspan="3">cell1-1</td><td>cell1-2</td></tr>' + + '<tr><td id="select">cell2-2</td></tr>' + + "<tr><td>cell3-1</td><td>cell3-2</td></tr>" + + "</table>"; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select")); + selection.addRange(range); + getTableEditor().deleteTableCell(1); + todo_is(editor.innerHTML, "<table><tbody>" + + '<tr><td rowspan="2">cell1-1</td><td>cell1-2</td></tr>' + + "<tr><td>cell3-2</td></tr>" + + "</tbody></table>", + "nsITableEditor.deleteTableCellContents(1) should adjust rowspan when spanned <tr>'s last child cell element is removed (removing middle row)"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when spanned <tr>\'s last child cell element is removed (removing middle row)'); + checkInputEvent(beforeInputEvents[0], "when spanned <tr>'s last child cell element is removed (removing middle row)"); + todo_is(inputEvents.length, 1, + 'Only one "input" event should be fired when spanned <tr>\'s last child cell element is removed (removing middle row)'); + if (inputEvents.length) { + checkInputEvent(inputEvents[0], "when spanned <tr>'s last child cell element is removed (removing middle row)"); + } + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td rowspan="3">cell1-1</td><td>cell1-2</td></tr>' + + '<tr><td id="select1">cell2-2</td></tr>' + + '<tr><td>cell3-1</td><td id="select2">cell3-2</td></tr>' + + "</table>"; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select1")); + selection.addRange(range); + range = document.createRange(); + range.selectNode(document.getElementById("select2")); + selection.addRange(range); + getTableEditor().deleteTableCell(1); + todo_is(editor.innerHTML, "<table><tbody>" + + '<tr><td rowspan="2">cell1-1</td><td>cell1-2</td></tr>' + + "</tbody></table>", + "nsITableEditor.deleteTableCellContents(1) should adjust rowspan when spanned <tr>'s last child cell element and remove the last row"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when spanned <tr>\'s last child cell element and remove the last row'); + checkInputEvent(beforeInputEvents[0], "when spanned <tr>'s last child cell element and remove the last row"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when spanned <tr>\'s last child cell element and remove the last row'); + checkInputEvent(inputEvents[0], "when spanned <tr>'s last child cell element and remove the last row"); + + // colspan + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td id="select" colspan="2">cell1-1</td><td>cell1-3</td></tr>' + + "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" + + "</table>"; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select")); + selection.addRange(range); + getTableEditor().deleteTableCell(1); + is(editor.innerHTML, "<table><tbody>" + + "<tr><td>cell1-3</td></tr>" + + "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" + + "</tbody></table>", + "nsITableEditor.deleteTableCellContents(1) should just remove the cell spanning columns"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when removing the cell spanning columns'); + checkInputEvent(beforeInputEvents[0], "when removing the cell spanning columns"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when removing the cell spanning columns'); + checkInputEvent(inputEvents[0], "when removing the cell spanning columns"); + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td id="select1" colspan="2">cell1-1</td><td>cell1-3</td></tr>' + + '<tr><td>cell2-1</td><td id="select2">cell2-2</td><td>cell2-3</td></tr>' + + "</table>"; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select1")); + selection.addRange(range); + range = document.createRange(); + range.selectNode(document.getElementById("select2")); + selection.addRange(range); + getTableEditor().deleteTableCell(1); + is(editor.innerHTML, "<table><tbody>" + + "<tr><td>cell1-3</td></tr>" + + "<tr><td>cell2-1</td><td>cell2-3</td></tr>" + + "</tbody></table>", + "nsITableEditor.deleteTableCellContents(1) should just remove the cell spanning columns and the other selected cell element"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when removing the cell spanning columns and the other selected cell element'); + checkInputEvent(beforeInputEvents[0], "when removing the cell spanning columns and the other selected cell element"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when removing the cell spanning columns and the other selected cell element'); + checkInputEvent(inputEvents[0], "when removing the cell spanning columns and the other selected cell element"); + + // XXX broken case, colspan is not adjusted. + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td colspan="2">cell1-1</td><td>cell1-3</td></tr>' + + '<tr><td>cell2-1</td><td id="select">cell2-2</td><td>cell2-3</td></tr>' + + "</table>"; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select")); + selection.addRange(range); + getTableEditor().deleteTableCell(1); + todo_is(editor.innerHTML, "<table><tbody>" + + '<tr><td colspan="1">cell1-1</td><td>cell1-3</td></tr>' + + "<tr><td>cell2-1</td><td>cell2-3</td></tr>" + + "</tbody></table>", + "nsITableEditor.deleteTableCellContents(1) should adjust different row's colspan when corresponding cell element is removed"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when corresponding cell element is removed'); + checkInputEvent(beforeInputEvents[0], "when corresponding cell element is removed"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when corresponding cell element is removed'); + checkInputEvent(inputEvents[0], "when corresponding cell element is removed"); + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td colspan="2">cell1-1</td><td>cell1-3</td></tr>' + + '<tr><td>cell2-1</td><td id="select1">cell2-2</td><td id="select2">cell2-3</td></tr>' + + "</table>"; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select1")); + selection.addRange(range); + range = document.createRange(); + range.selectNode(document.getElementById("select2")); + selection.addRange(range); + getTableEditor().deleteTableCell(1); + todo_is(editor.innerHTML, "<table><tbody>" + + '<tr><td colspan="1">cell1-1</td><td>cell1-3</td></tr>' + + "<tr><td>cell2-1</td></tr>" + + "</tbody></table>", + "nsITableEditor.deleteTableCellContents(1) should adjust different row's colspan when corresponding cell element is removed (when 2 cell elements are selected)"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when corresponding cell element is removed (when 2 cell elements are selected)'); + checkInputEvent(beforeInputEvents[0], "when corresponding cell element is removed (when 2 cell elements are selected)"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when corresponding cell element is removed (when 2 cell elements are selected)'); + checkInputEvent(inputEvents[0], "when corresponding cell element is removed (when 2 cell elements are selected)"); + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td>cell1-1</td><td id="select1">cell1-2</td><td>cell1-4</td></tr>' + + '<tr><td id="select2" colspan="2">cell2-1</td><td>cell2-3</td><td>cell2-4</td></tr>' + + "</table>"; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select1")); + selection.addRange(range); + range = document.createRange(); + range.selectNode(document.getElementById("select2")); + selection.addRange(range); + getTableEditor().deleteTableCell(1); + is(editor.innerHTML, "<table><tbody>" + + "<tr><td>cell1-1</td><td>cell1-4</td></tr>" + + "<tr><td>cell2-3</td><td>cell2-4</td></tr>" + + "</tbody></table>", + "nsITableEditor.deleteTableCellContents(1) should adjust different row's colspan when corresponding cell element is removed (when 2 cell elements are selected)"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when corresponding cell element is removed (when 2 cell elements are selected)'); + checkInputEvent(beforeInputEvents[0], "when corresponding cell element is removed (when 2 cell elements are selected)"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when corresponding cell element is removed (when 2 cell elements are selected)'); + checkInputEvent(inputEvents[0], "when corresponding cell element is removed (when 2 cell elements are selected)"); + + // When a cell contains first selection range, it should be removed. + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td id="select">cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>' + + "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" + + "</table>"; + beforeInputEvents = []; + inputEvents = []; + selection.setBaseAndExtent(document.getElementById("select").firstChild, 0, + document.getElementById("select").firstChild, 1); + getTableEditor().deleteTableCell(1); + is(editor.innerHTML, "<table><tbody>" + + "<tr><td>cell1-2</td><td>cell1-3</td></tr>" + + "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" + + "</tbody></table>", + "nsITableEditor.deleteTableCellContents(1) should remove only a cell containing first selection range when there is no selected cell element"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when there is no selected cell element'); + checkInputEvent(beforeInputEvents[0], "when there is no selected cell element"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when there is no selected cell element'); + checkInputEvent(inputEvents[0], "when there is no selected cell element"); + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td id="select">cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>' + + "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" + + "</table>"; + beforeInputEvents = []; + inputEvents = []; + selection.setBaseAndExtent(document.getElementById("select").firstChild, 0, + document.getElementById("select").firstChild, 1); + getTableEditor().deleteTableCell(2); + is(editor.innerHTML, "<table><tbody>" + + "<tr><td>cell1-3</td></tr>" + + "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" + + "</tbody></table>", + "nsITableEditor.deleteTableCellContents(2) should remove only 2 cell elements starting from a cell containing first selection range when there is no selected cell element"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when there is no selected cell element'); + checkInputEvent(beforeInputEvents[0], "when there is no selected cell element"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when there is no selected cell element'); + checkInputEvent(inputEvents[0], "when there is no selected cell element"); + + editor.removeEventListener("input", onInput); + + SimpleTest.finish(); +}); + +function getTableEditor() { + var editingSession = SpecialPowers.wrap(window).docShell.editingSession; + return editingSession.getEditorForWindow(window).QueryInterface(SpecialPowers.Ci.nsITableEditor); +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_nsITableEditor_deleteTableCellContents.html b/editor/libeditor/tests/test_nsITableEditor_deleteTableCellContents.html new file mode 100644 index 0000000000..754c22766e --- /dev/null +++ b/editor/libeditor/tests/test_nsITableEditor_deleteTableCellContents.html @@ -0,0 +1,296 @@ +<!DOCTYPE> +<html> +<head> + <title>Test for nsITableEditor.deleteTableCellContents()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> +<div id="display"> +</div> +<div id="content" contenteditable>out of table<table><tr><td>default content</td></tr></table></div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> +"use strict"; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + let editor = document.getElementById("content"); + let selection = document.getSelection(); + let selectionRanges = []; + + function checkInputEvent(aEvent, aDescription) { + ok(aEvent instanceof InputEvent, + `"${aEvent.type}" event should be dispatched with InputEvent interface ${aDescription}`); + is(aEvent.cancelable, aEvent.type === "beforeinput", + `"${aEvent.type}" event should ${aEvent.type === "beforeinput" ? "be" : "be never"} cancelable ${aDescription}`); + is(aEvent.bubbles, true, + `"${aEvent.type}" event should always bubble ${aDescription}`); + is(aEvent.inputType, "deleteContent", + `inputType of "${aEvent.type}" event should be "deleteContent" ${aDescription}`); + is(aEvent.data, null, + `data of "${aEvent.type}" event should be null ${aDescription}`); + is(aEvent.dataTransfer, null, + `dataTransfer of "${aEvent.type}" event should be null ${aDescription}`); + let targetRanges = aEvent.getTargetRanges(); + if (aEvent.type === "beforeinput") { + is(targetRanges.length, selectionRanges.length, + `getTargetRanges() of "beforeinput" event should return selection ranges ${aDescription}`); + if (targetRanges.length === selectionRanges.length) { + for (let i = 0; i < selectionRanges.length; i++) { + is(targetRanges[i].startContainer, selectionRanges[i].startContainer, + `startContainer of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`); + is(targetRanges[i].startOffset, selectionRanges[i].startOffset, + `startOffset of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`); + is(targetRanges[i].endContainer, selectionRanges[i].endContainer, + `endContainer of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`); + is(targetRanges[i].endOffset, selectionRanges[i].endOffset, + `endOffset of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`); + } + } + } else { + is(targetRanges.length, 0, + `getTargetRanges() of "${aEvent.type}" event should return empty array ${aDescription}`); + } + } + + let beforeInputEvents = []; + let inputEvents = []; + function onBeforeInput(aEvent) { + beforeInputEvents.push(aEvent); + selectionRanges = []; + for (let i = 0; i < selection.rangeCount; i++) { + let range = selection.getRangeAt(i); + selectionRanges.push({startContainer: range.startContainer, startOffset: range.startOffset, + endContainer: range.endContainer, endOffset: range.endOffset}); + } + } + function onInput(aEvent) { + inputEvents.push(aEvent); + } + editor.addEventListener("beforeinput", onBeforeInput); + editor.addEventListener("input", onInput); + + selection.collapse(editor.firstChild, 0); + beforeInputEvents = []; + inputEvents = []; + getTableEditor().deleteTableCellContents(); + is(editor.innerHTML, "out of table<table><tbody><tr><td>default content</td></tr></tbody></table>", + "nsITableEditor.deleteTableCellContents() should do nothing if selection is not in <table>"); + // If there were specific inputType value for this API, we should dispatch cancelable "beforeinput", though. + is(beforeInputEvents.length, 1, + '"beforeinput" event should be fired when a call of nsITableEditor.deleteTableCellContents() even though it will do nothing'); + checkInputEvent(beforeInputEvents[0], "when selection is collapsed outside table element"); + is(inputEvents.length, 0, + 'No "input" event should be fired when a call of nsITableEditor.deleteTableCellContents() does nothing'); + + selection.removeAllRanges(); + try { + beforeInputEvents = []; + inputEvents = []; + getTableEditor().deleteTableCellContents(); + ok(false, "getTableEditor().deleteTableCellContents() without selection ranges should throw exception"); + } catch (e) { + ok(true, "getTableEditor().deleteTableCellContents() without selection ranges should throw exception"); + is(beforeInputEvents.length, 0, + 'No "beforeinput" event should be fired when nsITableEditor.deleteTableCellContents() causes exception due to no selection range'); + is(inputEvents.length, 0, + 'No "input" event should be fired when nsITableEditor.deleteTableCellContents() causes exception due to no selection range'); + } + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td id="select">cell1-1</td><td>cell1-2</td></tr>' + + "<tr><td>cell2-1</td><td>cell2-2</td></tr>" + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + let range = document.createRange(); + range.selectNode(document.getElementById("select")); + selection.addRange(range); + getTableEditor().deleteTableCellContents(); + is(editor.innerHTML, "<table><tbody>" + + '<tr><td id="select"><br></td><td>cell1-2</td></tr>' + + "<tr><td>cell2-1</td><td>cell2-2</td></tr>" + + "</tbody></table>", + "nsITableEditor.deleteTableCellContents() should replace the selected cell's text with <br> element"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when all text in a cell is selected'); + checkInputEvent(beforeInputEvents[0], "when all text in a cell is selected"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when all text in a cell is selected'); + checkInputEvent(inputEvents[0], "when all text in a cell is selected"); + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td id="select"><ul><li>list1</li></ul></td><td>cell1-2</td></tr>' + + "<tr><td>cell2-1</td><td>cell2-2</td></tr>" + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select")); + selection.addRange(range); + getTableEditor().deleteTableCellContents(); + is(editor.innerHTML, "<table><tbody>" + + '<tr><td id="select"><br></td><td>cell1-2</td></tr>' + + "<tr><td>cell2-1</td><td>cell2-2</td></tr>" + + "</tbody></table>", + "nsITableEditor.deleteTableCellContents() should replace the selected cell's <ul> element with <br> element"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when <ul> element in a cell is selected'); + checkInputEvent(beforeInputEvents[0], "when <ul> element in a cell is selected"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when <ul> element in a cell is selected'); + checkInputEvent(inputEvents[0], "when <ul> element in a cell is selected"); + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td id="select1">cell1-1</td><td>cell1-2</td></tr>' + + '<tr><td id="select2">cell2-1</td><td>cell2-2</td></tr>' + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select1")); + selection.addRange(range); + range = document.createRange(); + range.selectNode(document.getElementById("select2")); + selection.addRange(range); + getTableEditor().deleteTableCellContents(); + is(editor.innerHTML, "<table><tbody>" + + '<tr><td id="select1"><br></td><td>cell1-2</td></tr>' + + '<tr><td id="select2"><br></td><td>cell2-2</td></tr>' + + "</tbody></table>", + "nsITableEditor.deleteTableCellContents() should replace the selected 2 cells' text with <br> element"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when 2 cell elements are selected'); + checkInputEvent(beforeInputEvents[0], "when 2 cell elements are selected"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when 2 cell elements are selected'); + checkInputEvent(inputEvents[0], "when 2 cell elements are selected"); + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td id="select1">cell1-1</td><td id="select2">cell1-2</td></tr>' + + '<tr><td id="select3">cell2-1</td><td id="select4">cell2-2</td></tr>' + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select1")); + selection.addRange(range); + range = document.createRange(); + range.selectNode(document.getElementById("select2")); + selection.addRange(range); + range = document.createRange(); + range.selectNode(document.getElementById("select3")); + selection.addRange(range); + range = document.createRange(); + range.selectNode(document.getElementById("select4")); + selection.addRange(range); + getTableEditor().deleteTableCellContents(); + is(editor.innerHTML, "<table><tbody>" + + '<tr><td id="select1"><br></td><td id="select2"><br></td></tr>' + + '<tr><td id="select3"><br></td><td id="select4"><br></td></tr>' + + "</tbody></table>", + "nsITableEditor.deleteTableCellContents() should replace the selected 4 cells' text with <br> element"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when 4 cell elements are selected'); + checkInputEvent(beforeInputEvents[0], "when 4 cell elements are selected"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when 4 cell elements are selected'); + checkInputEvent(inputEvents[0], "when 4 cell elements are selected"); + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td id="select" rowspan="2">cell1-1</td><td>cell1-2</td></tr>' + + "<tr><td>cell2-2</td></tr>" + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select")); + selection.addRange(range); + getTableEditor().deleteTableCellContents(); + is(editor.innerHTML, "<table><tbody>" + + '<tr><td id="select" rowspan="2"><br></td><td>cell1-2</td></tr>' + + "<tr><td>cell2-2</td></tr>" + + "</tbody></table>", + "nsITableEditor.deleteTableCellContents() should replace the selected cell's text with <br> element (even if the cell is row-spanning)"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when a cell element are selected (even if the cell is row-spanning)'); + checkInputEvent(beforeInputEvents[0], "when a cell element are selected (even if the cell is row-spanning)"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when a cell element are selected (even if the cell is row-spanning)'); + checkInputEvent(inputEvents[0], "when a cell element are selected (even if the cell is row-spanning)"); + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td id="select" colspan="2">cell1-1</td></tr>' + + "<tr><td>cell2-1</td><td>cell2-2</td></tr>" + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select")); + selection.addRange(range); + getTableEditor().deleteTableCellContents(); + is(editor.innerHTML, "<table><tbody>" + + '<tr><td id="select" colspan="2"><br></td></tr>' + + "<tr><td>cell2-1</td><td>cell2-2</td></tr>" + + "</tbody></table>", + "nsITableEditor.deleteTableCellContents() should replace the selected cell's text with <br> element (even if the cell is column-spanning)"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when a cell element are selected (even if the cell is column-spanning)'); + checkInputEvent(beforeInputEvents[0], "when a cell element are selected (even if the cell is column-spanning)"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when a cell element are selected (even if the cell is column-spanning)'); + checkInputEvent(inputEvents[0], "when a cell element are selected (even if the cell is column-spanning)"); + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td id="select">cell1-1</td><td>cell1-2</td></tr>' + + "<tr><td>cell2-1</td><td>cell2-2</td></tr>" + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + selection.setBaseAndExtent(document.getElementById("select").firstChild, 0, + document.getElementById("select").firstChild, 0); + getTableEditor().deleteTableCellContents(); + is(editor.innerHTML, "<table><tbody>" + + '<tr><td id="select"><br></td><td>cell1-2</td></tr>' + + "<tr><td>cell2-1</td><td>cell2-2</td></tr>" + + "</tbody></table>", + "nsITableEditor.deleteTableCellContents() should replace a cell's text with <br> element when the cell contains selection range"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when the cell contains selection range'); + checkInputEvent(beforeInputEvents[0], "when the cell contains selection range"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when the cell contains selection range'); + checkInputEvent(inputEvents[0], "when the cell contains selection range"); + + editor.removeEventListener("beforeinput", onBeforeInput); + editor.removeEventListener("input", onInput); + + SimpleTest.finish(); +}); + +function getTableEditor() { + var editingSession = SpecialPowers.wrap(window).docShell.editingSession; + return editingSession.getEditorForWindow(window).QueryInterface(SpecialPowers.Ci.nsITableEditor); +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_nsITableEditor_deleteTableColumn.html b/editor/libeditor/tests/test_nsITableEditor_deleteTableColumn.html new file mode 100644 index 0000000000..3b34a989f4 --- /dev/null +++ b/editor/libeditor/tests/test_nsITableEditor_deleteTableColumn.html @@ -0,0 +1,515 @@ +<!DOCTYPE> +<html> +<head> + <title>Test for nsITableEditor.deleteTableColumn()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> +<div id="display"> +</div> +<div id="content" contenteditable>out of table<table><tr><td>default content</td></tr></table></div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> +"use strict"; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + let editor = document.getElementById("content"); + let selection = document.getSelection(); + let selectionRanges = []; + + function checkInputEvent(aEvent, aDescription) { + ok(aEvent instanceof InputEvent, + `"${aEvent.type}" event should be dispatched with InputEvent interface ${aDescription}`); + is(aEvent.cancelable, aEvent.type === "beforeinput", + `"${aEvent.type}" event should ${aEvent.type === "beforeinput" ? "be" : "be never"} cancelable ${aDescription}`); + is(aEvent.bubbles, true, + `"${aEvent.type}" event should always bubble ${aDescription}`); + is(aEvent.inputType, "deleteContent", + `inputType of "${aEvent.type}" event should be "deleteContent" ${aDescription}`); + is(aEvent.data, null, + `data of "${aEvent.type}" event should be null ${aDescription}`); + is(aEvent.dataTransfer, null, + `dataTransfer of "${aEvent.type}" event should be null ${aDescription}`); + let targetRanges = aEvent.getTargetRanges(); + if (aEvent.type === "beforeinput") { + is(targetRanges.length, selectionRanges.length, + `getTargetRanges() of "beforeinput" event should return selection ranges ${aDescription}`); + if (targetRanges.length === selectionRanges.length) { + for (let i = 0; i < selectionRanges.length; i++) { + is(targetRanges[i].startContainer, selectionRanges[i].startContainer, + `startContainer of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`); + is(targetRanges[i].startOffset, selectionRanges[i].startOffset, + `startOffset of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`); + is(targetRanges[i].endContainer, selectionRanges[i].endContainer, + `endContainer of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`); + is(targetRanges[i].endOffset, selectionRanges[i].endOffset, + `endOffset of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`); + } + } + } else { + is(targetRanges.length, 0, + `getTargetRanges() of "${aEvent.type}" event should return empty array ${aDescription}`); + } + } + + let beforeInputEvents = []; + let inputEvents = []; + function onBeforeInput(aEvent) { + beforeInputEvents.push(aEvent); + selectionRanges = []; + for (let i = 0; i < selection.rangeCount; i++) { + let range = selection.getRangeAt(i); + selectionRanges.push({startContainer: range.startContainer, startOffset: range.startOffset, + endContainer: range.endContainer, endOffset: range.endOffset}); + } + } + function onInput(aEvent) { + inputEvents.push(aEvent); + } + editor.addEventListener("beforeinput", onBeforeInput); + editor.addEventListener("input", onInput); + + beforeInputEvents = []; + inputEvents = []; + selection.collapse(editor.firstChild, 0); + getTableEditor().deleteTableColumn(1); + is(editor.innerHTML, "out of table<table><tbody><tr><td>default content</td></tr></tbody></table>", + "nsITableEditor.deleteTableColumn(1) should do nothing if selection is not in <table>"); + // If there were specific inputType value for this API, we should dispatch cancelable "beforeinput", though. + is(beforeInputEvents.length, 1, + '"beforeinput" event should be fired when a call of nsITableEditor.deleteTableColumn(1) will do nothing'); + checkInputEvent(beforeInputEvents[0], "when selection is collapsed outside table element (nsITableEditor.deleteTableColumn(1))"); + is(inputEvents.length, 0, + 'No "input" event should be fired when a call of nsITableEditor.deleteTableColumn(1) does nothing'); + + selection.removeAllRanges(); + try { + beforeInputEvents = []; + inputEvents = []; + getTableEditor().deleteTableColumn(1); + ok(false, "getTableEditor().deleteTableColumn(1) without selection ranges should throw exception"); + } catch (e) { + ok(true, "getTableEditor().deleteTableColumn(1) without selection ranges should throw exception"); + is(beforeInputEvents.length, 0, + 'No "beforeinput" event should be fired when nsITableEditor.deleteTableColumn(1) causes exception due to no selection range'); + is(inputEvents.length, 0, + 'No "input" event should be fired when nsITableEditor.deleteTableColumn(1) causes exception due to no selection range'); + } + + // If a cell is selected and the argument is less than number of rows, + // specified number of rows should be removed starting from the row + // containing the selected cell. But if the argument is same or + // larger than actual number of rows, the <table> should be removed. + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + let range = document.createRange(); + range.selectNode(document.getElementById("select")); + selection.addRange(range); + getTableEditor().deleteTableColumn(1); + is(editor.innerHTML, "<table><tbody><tr><td>cell1-2</td></tr><tr><td>cell2-2</td></tr></tbody></table>", + "nsITableEditor.deleteTableColumn(1) should delete the first column when a cell in the first column is selected"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when a cell in the first column is selected'); + checkInputEvent(beforeInputEvents[0], "when a cell in the first column is selected"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when a cell in the first column is selected'); + checkInputEvent(inputEvents[0], "when a cell in the first column is selected"); + + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td id="select">cell2-2</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select")); + selection.addRange(range); + getTableEditor().deleteTableColumn(1); + is(editor.innerHTML, "<table><tbody><tr><td>cell1-1</td></tr><tr><td>cell2-1</td></tr></tbody></table>", + "nsITableEditor.deleteTableColumn(1) should delete the second column when a cell in the second column is selected"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when a cell in the second column is selected'); + checkInputEvent(beforeInputEvents[0], "when a cell in the second column is selected"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when a cell in the second column is selected'); + checkInputEvent(inputEvents[0], "when a cell in the second column is selected"); + + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select")); + selection.addRange(range); + getTableEditor().deleteTableColumn(2); + is(editor.innerHTML, "", + "nsITableEditor.deleteTableColumn(2) should delete the <table> since there is only 2 columns"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when a cell in first column is selected and argument is same as number of rows'); + checkInputEvent(beforeInputEvents[0], "when a cell in first column is selected and argument is same as number of rows"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when a cell in first column is selected and argument is same as number of rows'); + checkInputEvent(inputEvents[0], "when a cell in first column is selected and argument is same as number of rows"); + + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select")); + selection.addRange(range); + getTableEditor().deleteTableColumn(3); + is(editor.innerHTML, "", + "nsITableEditor.deleteTableColumn(3) should delete the <table> when argument is larger than actual number of columns"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when argument is larger than actual number of columns'); + checkInputEvent(beforeInputEvents[0], "when argument is larger than actual number of columns"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when argument is larger than actual number of columns'); + checkInputEvent(inputEvents[0], "when argument is larger than actual number of columns"); + + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td>cell1-1</td><td id="select">cell1-2</td><td>cell1-3</td></tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select")); + selection.addRange(range); + getTableEditor().deleteTableColumn(2); + is(editor.innerHTML, "<table><tbody><tr><td>cell1-1</td></tr><tr><td>cell2-1</td></tr></tbody></table>", + "nsITableEditor.deleteTableColumn(2) should delete the second column containing selected cell and next column"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when a cell in second column and argument is same as the remaining columns'); + checkInputEvent(beforeInputEvents[0], "when a cell in second column and argument is same as the remaining columns"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when a cell in second column and argument is same as the remaining columns'); + checkInputEvent(inputEvents[0], "when a cell in second column and argument is same as the remaining columns"); + + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td id="select">cell2-1</td><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select")); + selection.addRange(range); + getTableEditor().deleteTableColumn(3); + is(editor.innerHTML, "", + "nsITableEditor.deleteTableColumn(3) should delete the <table> since the argument equals actual number of columns"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when a cell in first column and argument is larger than the remaining columns'); + checkInputEvent(beforeInputEvents[0], "when a cell in first column and argument is larger than the remaining columns"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when a cell in first column and argument is larger than the remaining columns'); + checkInputEvent(inputEvents[0], "when a cell in first column and argument is larger than the remaining columns"); + + // Similar to selected a cell, when selection is in a cell, the cell should + // treated as selected. + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select").firstChild); + selection.addRange(range); + getTableEditor().deleteTableColumn(1); + is(editor.innerHTML, "<table><tbody><tr><td>cell1-2</td></tr><tr><td>cell2-2</td></tr></tbody></table>", + "nsITableEditor.deleteTableColumn(1) should delete the first column when a cell in the first column contains selection range"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when a cell in the first column contains selection range'); + checkInputEvent(beforeInputEvents[0], "when a cell in the first column contains selection range"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when a cell in the first column contains selection range'); + checkInputEvent(inputEvents[0], "when a cell in the first column contains selection range"); + + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td id="select">cell2-2</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select").firstChild); + selection.addRange(range); + getTableEditor().deleteTableColumn(1); + is(editor.innerHTML, "<table><tbody><tr><td>cell1-1</td></tr><tr><td>cell2-1</td></tr></tbody></table>", + "nsITableEditor.deleteTableColumn(1) should delete the second column when a cell in the second column contains selection range"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when a cell in the second column contains selection range'); + checkInputEvent(beforeInputEvents[0], "when a cell in the second column contains selection range"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when a cell in the second column contains selection range'); + checkInputEvent(inputEvents[0], "when a cell in the second column contains selection range"); + + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select").firstChild); + selection.addRange(range); + getTableEditor().deleteTableColumn(2); + is(editor.innerHTML, "", + "nsITableEditor.deleteTableColumn(2) should delete the <table> since there is only 2 columns"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when all text in a cell in first column is selected and argument includes next row'); + checkInputEvent(beforeInputEvents[0], "when all text in a cell in first column is selected and argument includes next row"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when all text in a cell in first column is selected and argument includes next row'); + checkInputEvent(inputEvents[0], "when all text in a cell in first column is selected and argument includes next row"); + + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select").firstChild); + selection.addRange(range); + getTableEditor().deleteTableColumn(3); + is(editor.innerHTML, "", + "nsITableEditor.deleteTableColumn(3) should delete the <table> when argument is larger than actual number of columns"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when all text in a cell in first column is selected and argument is same as number of all rows'); + checkInputEvent(beforeInputEvents[0], "when all text in a cell in first column is selected and argument is same as number of all rows"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when all text in a cell in first column is selected and argument is same as number of all rows'); + checkInputEvent(inputEvents[0], "when all text in a cell in first column is selected and argument is same as number of all rows"); + + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td>cell1-1</td><td id="select">cell1-2</td><td>cell1-3</td></tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select").firstChild); + selection.addRange(range); + getTableEditor().deleteTableColumn(2); + is(editor.innerHTML, "<table><tbody><tr><td>cell1-1</td></tr><tr><td>cell2-1</td></tr></tbody></table>", + "nsITableEditor.deleteTableColumn(2) should delete the second column containing a cell containing selection range and next column"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when all text in a cell is selected and argument is same than renaming number of columns'); + checkInputEvent(beforeInputEvents[0], "when all text in a cell is selected and argument is same than renaming number of columns"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when all text in a cell is selected and argument is same than renaming number of columns'); + checkInputEvent(inputEvents[0], "when all text in a cell is selected and argument is same than renaming number of columns"); + + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td id="select">cell2-1</td><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select").firstChild); + selection.addRange(range); + getTableEditor().deleteTableColumn(3); + is(editor.innerHTML, "", + "nsITableEditor.deleteTableColumn(3) should delete the <table> since the argument equals actual number of columns"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when all text in a cell in the first column and argument is larger than renaming number of columns'); + checkInputEvent(beforeInputEvents[0], "when all text in a cell in the first column and argument is larger than renaming number of columns"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when all text in a cell in the first column and argument is larger than renaming number of columns'); + checkInputEvent(inputEvents[0], "when all text in a cell in the first column and argument is larger than renaming number of columns"); + + // The argument should be ignored when 2 or more cells are selected. + // XXX Different from deleteTableRow(), this removes the <table> completely. + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td id="select1">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td id="select2">cell2-2</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select1")); + selection.addRange(range); + range = document.createRange(); + range.selectNode(document.getElementById("select2")); + selection.addRange(range); + getTableEditor().deleteTableColumn(1); + is(editor.innerHTML, "", + "nsITableEditor.deleteTableColumn(1) should delete the <table> when both columns have selected cell"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when both columns have selected cell'); + checkInputEvent(beforeInputEvents[0], "when both columns have selected cell"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when both columns have selected cell'); + checkInputEvent(inputEvents[0], "when both columns have selected cell"); + + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td id="select1">cell1-1</td><td>cell1-2</td></tr><tr><td id="select2">cell2-1</td><td>cell2-2</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select1")); + selection.addRange(range); + range = document.createRange(); + range.selectNode(document.getElementById("select2")); + selection.addRange(range); + getTableEditor().deleteTableColumn(2); + is(editor.innerHTML, "", + "nsITableEditor.deleteTableColumn(2) should delete the <table> since 2 is number of columns of the <table>"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when cells in every column are selected #2'); + checkInputEvent(beforeInputEvents[0], "when cells in every column are selected #2"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when cells in every column are selected #2'); + checkInputEvent(inputEvents[0], "when cells in every column are selected #2"); + + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td id="select1">cell1-1</td><td>cell1-2</td></tr><tr><td id="select2">cell2-1</td><td>cell2-2</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select1")); + selection.addRange(range); + range = document.createRange(); + range.selectNode(document.getElementById("select2")); + selection.addRange(range); + getTableEditor().deleteTableColumn(2); + is(editor.innerHTML, "", + "nsITableEditor.deleteTableColumn(2) should delete the <table> since 2 is number of columns of the <table>"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when 2 cells in same column are selected'); + checkInputEvent(beforeInputEvents[0], "when 2 cells in same column are selected"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when 2 cells in same column are selected'); + checkInputEvent(inputEvents[0], "when 2 cells in same column are selected"); + + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td id="select1">cell1-1</td><td id="select2">cell1-2</td><td>cell1-3</td></tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select1")); + selection.addRange(range); + range = document.createRange(); + range.selectNode(document.getElementById("select2")); + selection.addRange(range); + getTableEditor().deleteTableColumn(1); + is(editor.innerHTML, "<table><tbody><tr><td>cell1-3</td></tr><tr><td>cell2-3</td></tr></tbody></table>", + "nsITableEditor.deleteTableColumn(1) should delete first 2 columns because cells in the both columns are selected"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when 2 cell elements in different columns are selected #1'); + checkInputEvent(beforeInputEvents[0], "when 2 cell elements in different columns are selected #1"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when 2 cell elements in different columns are selected #1'); + checkInputEvent(inputEvents[0], "when 2 cell elements in different columns are selected #1"); + + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td id="select1">cell1-1</td><td>cell1-2</td><td id="select2">cell1-3</td></tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select1")); + selection.addRange(range); + range = document.createRange(); + range.selectNode(document.getElementById("select2")); + selection.addRange(range); + getTableEditor().deleteTableColumn(1); + is(editor.innerHTML, "<table><tbody><tr><td>cell1-2</td></tr><tr><td>cell2-2</td></tr></tbody></table>", + "nsITableEditor.deleteTableColumn(1) should delete the first and the last columns because cells in the both columns are selected"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when 2 cell elements in different columns are selected #2'); + checkInputEvent(beforeInputEvents[0], "when 2 cell elements in different columns are selected #2"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when 2 cell elements in different columns are selected #2'); + checkInputEvent(inputEvents[0], "when 2 cell elements in different columns are selected #2"); + + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td id="select" colspan="2">cell1-1</td><td>cell1-3</td></tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select")); + selection.addRange(range); + getTableEditor().deleteTableColumn(1); + is(editor.innerHTML, '<table><tbody><tr><td id="select" colspan="1"><br></td><td>cell1-3</td></tr><tr><td>cell2-2</td><td>cell2-3</td></tr></tbody></table>', + "nsITableEditor.deleteTableColumn(1) with a selected cell is colspan=\"2\" should delete the first column and add empty cell to the second column"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when a cell is selected and its colspan is 2'); + checkInputEvent(beforeInputEvents[0], "when a cell is selected and its colspan is 2"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when a cell is selected and its colspan is 2'); + checkInputEvent(inputEvents[0], "when a cell is selected and its colspan is 2"); + + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td id="select" colspan="3">cell1-1</td></tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select")); + selection.addRange(range); + getTableEditor().deleteTableColumn(1); + is(editor.innerHTML, '<table><tbody><tr><td id="select" colspan="2"><br></td></tr><tr><td>cell2-2</td><td>cell2-3</td></tr></tbody></table>', + "nsITableEditor.deleteTableColumn(1) with a selected cell is colspan=\"3\" should delete the first column and add empty cell whose colspan is 2 to the second column"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when a cell is selected and its colspan is 3'); + checkInputEvent(beforeInputEvents[0], "when a cell is selected and its colspan is 3"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when a cell is selected and its colspan is 3'); + checkInputEvent(inputEvents[0], "when a cell is selected and its colspan is 3"); + + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td colspan="3">cell1-1</td></tr><tr><td>cell2-1</td><td id="select">cell2-2</td><td>cell2-3</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select")); + selection.addRange(range); + getTableEditor().deleteTableColumn(1); + is(editor.innerHTML, '<table><tbody><tr><td colspan="2">cell1-1</td></tr><tr><td>cell2-1</td><td>cell2-3</td></tr></tbody></table>', + "nsITableEditor.deleteTableColumn(1) with selected cell in the second column should delete the second column and the colspan in the first row should be adjusted"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when a cell in 2nd column is only cell defined by the column #1'); + checkInputEvent(beforeInputEvents[0], "when a cell in 2nd column is only cell defined by the column #1"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when a cell in 2nd column is only cell defined by the column #1'); + checkInputEvent(inputEvents[0], "when a cell in 2nd column is only cell defined by the column #1"); + + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td colspan="2">cell1-1</td><td>cell1-3</td></tr><tr><td>cell2-1</td><td id="select">cell2-2</td><td>cell2-3</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select")); + selection.addRange(range); + getTableEditor().deleteTableColumn(1); + is(editor.innerHTML, '<table><tbody><tr><td colspan="1">cell1-1</td><td>cell1-3</td></tr><tr><td>cell2-1</td><td>cell2-3</td></tr></tbody></table>', + "nsITableEditor.deleteTableColumn(1) with selected cell in the second column should delete the second column and the colspan should be adjusted"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when a cell in 2nd column is only cell defined by the column #2'); + checkInputEvent(beforeInputEvents[0], "when a cell in 2nd column is only cell defined by the column #2"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when a cell in 2nd column is only cell defined by the column #2'); + checkInputEvent(inputEvents[0], "when a cell in 2nd column is only cell defined by the column #2"); + + SimpleTest.finish(); +}); + +function getTableEditor() { + var editingSession = SpecialPowers.wrap(window).docShell.editingSession; + return editingSession.getEditorForWindow(window).QueryInterface(SpecialPowers.Ci.nsITableEditor); +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_nsITableEditor_deleteTableRow.html b/editor/libeditor/tests/test_nsITableEditor_deleteTableRow.html new file mode 100644 index 0000000000..820df15b46 --- /dev/null +++ b/editor/libeditor/tests/test_nsITableEditor_deleteTableRow.html @@ -0,0 +1,522 @@ +<!DOCTYPE> +<html> +<head> + <title>Test for nsITableEditor.deleteTableRow()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> +<div id="display"> +</div> +<div id="content" contenteditable>out of table<table><tr><td>default content</td></tr></table></div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> +"use strict"; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + let editor = document.getElementById("content"); + let selection = document.getSelection(); + let selectionRanges = []; + + function checkInputEvent(aEvent, aDescription) { + ok(aEvent instanceof InputEvent, + `"${aEvent.type}" event should be dispatched with InputEvent interface ${aDescription}`); + is(aEvent.cancelable, aEvent.type === "beforeinput", + `"${aEvent.type}" event should ${aEvent.type === "beforeinput" ? "be" : "be never"} cancelable ${aDescription}`); + is(aEvent.bubbles, true, + `"${aEvent.type}" event should always bubble ${aDescription}`); + is(aEvent.inputType, "deleteContent", + `inputType of "${aEvent.type}" event should be "deleteContent" ${aDescription}`); + is(aEvent.data, null, + `data of "${aEvent.type}" event should be null ${aDescription}`); + is(aEvent.dataTransfer, null, + `dataTransfer of "${aEvent.type}" event should be null ${aDescription}`); + let targetRanges = aEvent.getTargetRanges(); + if (aEvent.type === "beforeinput") { + is(targetRanges.length, selectionRanges.length, + `getTargetRanges() of "beforeinput" event should return selection ranges ${aDescription}`); + if (targetRanges.length === selectionRanges.length) { + for (let i = 0; i < selectionRanges.length; i++) { + is(targetRanges[i].startContainer, selectionRanges[i].startContainer, + `startContainer of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`); + is(targetRanges[i].startOffset, selectionRanges[i].startOffset, + `startOffset of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`); + is(targetRanges[i].endContainer, selectionRanges[i].endContainer, + `endContainer of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`); + is(targetRanges[i].endOffset, selectionRanges[i].endOffset, + `endOffset of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`); + } + } + } else { + is(targetRanges.length, 0, + `getTargetRanges() of "${aEvent.type}" event should return empty array ${aDescription}`); + } + } + + let beforeInputEvents = []; + let inputEvents = []; + function onBeforeInput(aEvent) { + beforeInputEvents.push(aEvent); + selectionRanges = []; + for (let i = 0; i < selection.rangeCount; i++) { + let range = selection.getRangeAt(i); + selectionRanges.push({startContainer: range.startContainer, startOffset: range.startOffset, + endContainer: range.endContainer, endOffset: range.endOffset}); + } + } + function onInput(aEvent) { + inputEvents.push(aEvent); + } + editor.addEventListener("beforeinput", onBeforeInput); + editor.addEventListener("input", onInput); + + beforeInputEvents = []; + inputEvents = []; + selection.collapse(editor.firstChild, 0); + getTableEditor().deleteTableRow(1); + is(editor.innerHTML, "out of table<table><tbody><tr><td>default content</td></tr></tbody></table>", + "nsITableEditor.deleteTableRow(1) should do nothing if selection is not in <table>"); + is(beforeInputEvents.length, 1, + '"beforeinput" event should be fired when a call of nsITableEditor.deleteTableRow(1) even though it will do nothing'); + checkInputEvent(beforeInputEvents[0], "when selection is collapsed outside table element (nsITableEditor.deleteTableRow(1))"); + is(inputEvents.length, 0, + 'No "input" event should be fired when a call of nsITableEditor.deleteTableRow(1) does nothing'); + + selection.removeAllRanges(); + try { + beforeInputEvents = []; + inputEvents = []; + getTableEditor().deleteTableRow(1); + ok(false, "getTableEditor().deleteTableRow(1) without selection ranges should throw exception"); + } catch (e) { + ok(true, "getTableEditor().deleteTableRow(1) without selection ranges should throw exception"); + is(beforeInputEvents.length, 0, + 'No "beforeinput" event should be fired when nsITableEditor.deleteTableRow(1) causes exception due to no selection range'); + is(inputEvents.length, 0, + 'No "input" event should be fired when nsITableEditor.deleteTableRow(1) causes exception due to no selection range'); + } + + // If a cell is selected and the argument is less than number of rows, + // specified number of rows should be removed starting from the row + // containing the selected cell. But if the argument is same or + // larger than actual number of rows when a cell in the first row is + // selected, the <table> should be removed. + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + let range = document.createRange(); + range.selectNode(document.getElementById("select")); + selection.addRange(range); + getTableEditor().deleteTableRow(1); + is(editor.innerHTML, "<table><tbody><tr><td>cell2-1</td><td>cell2-2</td></tr></tbody></table>", + "nsITableEditor.deleteTableRow(1) should delete the first row when a cell in the first row is selected"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when a cell in the first row is selected'); + checkInputEvent(beforeInputEvents[0], "when a cell in the first row is selected"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when a cell in the first row is selected'); + checkInputEvent(inputEvents[0], "when a cell in the first row is selected"); + + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td id="select">cell2-1</td><td>cell2-2</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select")); + selection.addRange(range); + getTableEditor().deleteTableRow(1); + is(editor.innerHTML, "<table><tbody><tr><td>cell1-1</td><td>cell1-2</td></tr></tbody></table>", + "nsITableEditor.deleteTableRow(1) should delete the second row when a cell in the second row is selected"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when a cell in the second row is selected'); + checkInputEvent(beforeInputEvents[0], "when a cell in the second row is selected"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when a cell in the second row is selected'); + checkInputEvent(inputEvents[0], "when a cell in the second row is selected"); + + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select")); + selection.addRange(range); + getTableEditor().deleteTableRow(2); + is(editor.innerHTML, "", + "nsITableEditor.deleteTableRow(2) should delete the <table> since there is only 2 rows"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when a cell in first row is selected and argument is same as number of rows'); + checkInputEvent(beforeInputEvents[0], "when a cell in first row is selected and argument is same as number of rows"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when a cell in first row is selected and argument is same as number of rows'); + checkInputEvent(inputEvents[0], "when a cell in first row is selected and argument is same as number of rows"); + + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select")); + selection.addRange(range); + getTableEditor().deleteTableRow(3); + is(editor.innerHTML, "", + "nsITableEditor.deleteTableRow(3) should delete the <table> when argument is larger than actual number of rows"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when argument is larger than actual number of rows'); + checkInputEvent(beforeInputEvents[0], "when argument is larger than actual number of rows"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when argument is larger than actual number of rows'); + checkInputEvent(inputEvents[0], "when argument is larger than actual number of rows"); + + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td id="select">cell2-1</td><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select")); + selection.addRange(range); + getTableEditor().deleteTableRow(2); + is(editor.innerHTML, "<table><tbody><tr><td>cell1-1</td><td>cell1-2</td></tr></tbody></table>", + "nsITableEditor.deleteTableRow(2) should delete the second row containing selected cell and next row"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when a cell in second row and argument is same as the remaining rows'); + checkInputEvent(beforeInputEvents[0], "when a cell in second row and argument is same as the remaining rows"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when a cell in second row and argument is same as the remaining rows'); + checkInputEvent(inputEvents[0], "when a cell in second row and argument is same as the remaining rows"); + + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td id="select">cell2-1</td><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select")); + selection.addRange(range); + getTableEditor().deleteTableRow(3); + is(editor.innerHTML, "<table><tbody><tr><td>cell1-1</td><td>cell1-2</td></tr></tbody></table>", + "nsITableEditor.deleteTableRow(3) should delete the second row (containing selected cell) and the third row even though the argument is larger than the rows"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when a cell in second row and argument is larger than the remaining rows'); + checkInputEvent(beforeInputEvents[0], "when a cell in second row and argument is larger than the remaining rows"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when a cell in second row and argument is larger than the remaining rows'); + checkInputEvent(inputEvents[0], "when a cell in second row and argument is larger than the remaining rows"); + + // Similar to selected a cell, when selection is in a cell, the cell should + // treated as selected. + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select").firstChild); + selection.addRange(range); + getTableEditor().deleteTableRow(1); + is(editor.innerHTML, "<table><tbody><tr><td>cell2-1</td><td>cell2-2</td></tr></tbody></table>", + "nsITableEditor.deleteTableRow(1) should delete the first row when a cell in the first row contains selection range"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when a cell in the first row contains selection range'); + checkInputEvent(beforeInputEvents[0], "when a cell in the first row contains selection range"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when a cell in the first row contains selection range'); + checkInputEvent(inputEvents[0], "when a cell in the first row contains selection range"); + + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td id="select">cell2-1</td><td>cell2-2</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select").firstChild); + selection.addRange(range); + getTableEditor().deleteTableRow(1); + is(editor.innerHTML, "<table><tbody><tr><td>cell1-1</td><td>cell1-2</td></tr></tbody></table>", + "nsITableEditor.deleteTableRow(1) should delete the second row when a cell in the second row contains selection range"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when a cell in the second row contains selection range'); + checkInputEvent(beforeInputEvents[0], "when a cell in the second row contains selection range"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when a cell in the second row contains selection range'); + checkInputEvent(inputEvents[0], "when a cell in the second row contains selection range"); + + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select").firstChild); + selection.addRange(range); + getTableEditor().deleteTableRow(2); + is(editor.innerHTML, "", + "nsITableEditor.deleteTableRow(2) should delete the <table> since there is only 2 rows"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when all text in a cell in first row is selected and argument includes next row'); + checkInputEvent(beforeInputEvents[0], "when all text in a cell in first row is selected and argument includes next row"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when all text in a cell in first row is selected and argument includes next row'); + checkInputEvent(inputEvents[0], "when all text in a cell in first row is selected and argument includes next row"); + + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td id="select">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select").firstChild); + selection.addRange(range); + getTableEditor().deleteTableRow(3); + is(editor.innerHTML, "", + "nsITableEditor.deleteTableRow(3) should delete the <table> when argument is larger than actual number of rows"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when all text in a cell in first row is selected and argument is same as number of all rows'); + checkInputEvent(beforeInputEvents[0], "when all text in a cell in first row is selected and argument is same as number of all rows"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when all text in a cell in first row is selected and argument is same as number of all rows'); + checkInputEvent(inputEvents[0], "when all text in a cell in first row is selected and argument is same as number of all rows"); + + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td id="select">cell2-1</td><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select").firstChild); + selection.addRange(range); + getTableEditor().deleteTableRow(2); + is(editor.innerHTML, "<table><tbody><tr><td>cell1-1</td><td>cell1-2</td></tr></tbody></table>", + "nsITableEditor.deleteTableRow(2) should delete the second row containing a cell containing selection range and next row"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when all text in a cell is selected and argument is same than renaming number of rows'); + checkInputEvent(beforeInputEvents[0], "when all text in a cell is selected and argument is same than renaming number of rows"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when all text in a cell is selected and argument is same than renaming number of rows'); + checkInputEvent(inputEvents[0], "when all text in a cell is selected and argument is same than renaming number of rows"); + + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td id="select">cell2-1</td><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select").firstChild); + selection.addRange(range); + getTableEditor().deleteTableRow(3); + is(editor.innerHTML, "<table><tbody><tr><td>cell1-1</td><td>cell1-2</td></tr></tbody></table>", + "nsITableEditor.deleteTableRow(3) should delete the second row (containing selection range) and the third row even though the argument is larger than the rows"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when all text in a cell in the second row and argument is larger than renaming number of rows'); + checkInputEvent(beforeInputEvents[0], "when all text in a cell in the second row and argument is larger than renaming number of rows"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when all text in a cell in the second row and argument is larger than renaming number of rows'); + checkInputEvent(inputEvents[0], "when all text in a cell in the second row and argument is larger than renaming number of rows"); + + // The argument should be ignored when 2 or more cells are selected. + // XXX If the argument is less than number of rows and cells in all rows are + // selected, only all rows are removed. However, this leaves empty <table> + // element. Is this expected? + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td id="select1">cell1-1</td><td>cell1-2</td></tr><tr><td id="select2">cell2-1</td><td>cell2-2</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select1")); + selection.addRange(range); + range = document.createRange(); + range.selectNode(document.getElementById("select2")); + selection.addRange(range); + getTableEditor().deleteTableRow(1); + is(editor.innerHTML, "<table><tbody></tbody></table>", + "nsITableEditor.deleteTableRow(1) should delete all rows if every row's cell is selected"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when cells in every row are selected #1'); + checkInputEvent(beforeInputEvents[0], "when cells in every row are selected #1"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when cells in every row are selected #1'); + checkInputEvent(inputEvents[0], "when cells in every row are selected #1"); + + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td id="select1">cell1-1</td><td>cell1-2</td></tr><tr><td id="select2">cell2-1</td><td>cell2-2</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select1")); + selection.addRange(range); + range = document.createRange(); + range.selectNode(document.getElementById("select2")); + selection.addRange(range); + getTableEditor().deleteTableRow(2); + is(editor.innerHTML, "", + "nsITableEditor.deleteTableRow(2) should delete the <table> since 2 is number of rows of the <table>"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when cells in every row are selected #2'); + checkInputEvent(beforeInputEvents[0], "when cells in every row are selected #2"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when cells in every row are selected #2'); + checkInputEvent(inputEvents[0], "when cells in every row are selected #2"); + + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td id="select1">cell1-1</td><td id="select2">cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select1")); + selection.addRange(range); + range = document.createRange(); + range.selectNode(document.getElementById("select2")); + selection.addRange(range); + getTableEditor().deleteTableRow(2); + is(editor.innerHTML, "", + "nsITableEditor.deleteTableRow(2) should delete the <table> since 2 is number of rows of the <table>"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when 2 cells in same row are selected'); + checkInputEvent(beforeInputEvents[0], "when 2 cells in same row are selected"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when 2 cells in same row are selected'); + checkInputEvent(inputEvents[0], "when 2 cells in same row are selected"); + + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td id="select1">cell1-1</td><td>cell1-2</td></tr><tr><td id="select2">cell2-1</td><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select1")); + selection.addRange(range); + range = document.createRange(); + range.selectNode(document.getElementById("select2")); + selection.addRange(range); + getTableEditor().deleteTableRow(1); + is(editor.innerHTML, "<table><tbody><tr><td>cell3-1</td><td>cell3-2</td></tr></tbody></table>", + "nsITableEditor.deleteTableRow(1) should delete first 2 rows because cells in the both rows are selected"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when 2 cell elements in different rows are selected #1'); + checkInputEvent(beforeInputEvents[0], "when 2 cell elements in different rows are selected #1"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when 2 cell elements in different rows are selected #1'); + checkInputEvent(inputEvents[0], "when 2 cell elements in different rows are selected #1"); + + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td id="select1">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr><tr><td id="select2">cell3-1</td><td>cell3-2</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select1")); + selection.addRange(range); + range = document.createRange(); + range.selectNode(document.getElementById("select2")); + selection.addRange(range); + getTableEditor().deleteTableRow(1); + is(editor.innerHTML, "<table><tbody><tr><td>cell2-1</td><td>cell2-2</td></tr></tbody></table>", + "nsITableEditor.deleteTableRow(1) should delete the first and the last rows because cells in the both rows are selected"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when 2 cell elements in different rows are selected #2'); + checkInputEvent(beforeInputEvents[0], "when 2 cell elements in different rows are selected #2"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when 2 cell elements in different rows are selected #2'); + checkInputEvent(inputEvents[0], "when 2 cell elements in different rows are selected #2"); + + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td id="select" rowspan="2">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select")); + selection.addRange(range); + getTableEditor().deleteTableRow(1); + is(editor.innerHTML, '<table><tbody><tr><td valign="top"><br></td><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></tbody></table>', + "nsITableEditor.deleteTableRow(1) with a selected cell is rowspan=\"2\" should delete the first row and add empty cell to the second row"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when a cell is selected and its rowspan is 2'); + checkInputEvent(beforeInputEvents[0], "when a cell is selected and its rowspan is 2"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when a cell is selected and its rowspan is 2'); + checkInputEvent(inputEvents[0], "when a cell is selected and its rowspan is 2"); + + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td id="select" rowspan="3">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-2</td></tr><tr><td>cell3-2</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select")); + selection.addRange(range); + getTableEditor().deleteTableRow(1); + is(editor.innerHTML, '<table><tbody><tr><td valign="top" rowspan="2"><br></td><td>cell2-2</td></tr><tr><td>cell3-2</td></tr></tbody></table>', + "nsITableEditor.deleteTableRow(1) with a selected cell is rowspan=\"3\" should delete the first row and add empty cell whose rowspan is 2 to the second row"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when a cell is selected and its rowspan is 3'); + checkInputEvent(beforeInputEvents[0], "when a cell is selected and its rowspan is 3"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when a cell is selected and its rowspan is 3'); + checkInputEvent(inputEvents[0], "when a cell is selected and its rowspan is 3"); + + // XXX Must be buggy case. When removing a row which does not have a cell due + // to rowspan, the rowspan is not changed properly. + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td rowspan="3">cell1-1</td><td>cell1-2</td></tr><tr><td id="select1">cell2-2</td></tr><tr><td>cell3-2</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select1")); + selection.addRange(range); + getTableEditor().deleteTableRow(1); + is(editor.innerHTML, '<table><tbody><tr><td rowspan="1">cell1-1</td><td>cell1-2</td></tr><tr><td>cell3-2</td></tr></tbody></table>', + "nsITableEditor.deleteTableRow(1) with selected cell in the second row should delete the second row and the row span should be adjusted"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when a cell in 2nd row which is only cell defined by the row #1'); + checkInputEvent(beforeInputEvents[0], "when a cell in 2nd row which is only cell defined by the row #1"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when a cell in 2nd row which is only cell defined by the row #1'); + checkInputEvent(inputEvents[0], "when a cell in 2nd row which is only cell defined by the row #1"); + + selection.removeAllRanges(); + editor.innerHTML = + '<table><tr><td rowspan="2">cell1-1</td><td>cell1-2</td></tr><tr><td id="select">cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table>'; + beforeInputEvents = []; + inputEvents = []; + range = document.createRange(); + range.selectNode(document.getElementById("select")); + selection.addRange(range); + getTableEditor().deleteTableRow(1); + is(editor.innerHTML, '<table><tbody><tr><td rowspan="1">cell1-1</td><td>cell1-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></tbody></table>', + "nsITableEditor.deleteTableRow(1) with selected cell in the second row should delete the second row and the row span should be adjusted"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when a cell in 2nd row which is only cell defined by the row #2'); + checkInputEvent(beforeInputEvents[0], "when a cell in 2nd row which is only cell defined by the row #2"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when a cell in 2nd row which is only cell defined by the row #2'); + checkInputEvent(inputEvents[0], "when a cell in 2nd row which is only cell defined by the row #2"); + + editor.removeEventListener("beforeinput", onBeforeInput); + editor.removeEventListener("input", onInput); + + SimpleTest.finish(); +}); + +function getTableEditor() { + var editingSession = SpecialPowers.wrap(window).docShell.editingSession; + return editingSession.getEditorForWindow(window).QueryInterface(SpecialPowers.Ci.nsITableEditor); +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_nsITableEditor_getCellAt.html b/editor/libeditor/tests/test_nsITableEditor_getCellAt.html new file mode 100644 index 0000000000..f2544a2b00 --- /dev/null +++ b/editor/libeditor/tests/test_nsITableEditor_getCellAt.html @@ -0,0 +1,138 @@ +<!DOCTYPE> +<html> +<head> + <title>Test for nsITableEditor.getCellAt()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> +<div id="display"> +</div> +<div id="content" contenteditable></div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + let editor = document.getElementById("content"); + let selection = document.getSelection(); + + try { + SpecialPowers.unwrap(getTableEditor().getCellAt(undefined, 0, 0)); + ok(false, "nsITableEditor.getCellAt(undefined) should cause throwing an exception when editor does not have Selection"); + } catch (e) { + ok(true, "nsITableEditor.getCellAt(undefined) should cause throwing an exception when editor does not have Selection"); + } + + try { + SpecialPowers.unwrap(getTableEditor().getTableSize(null, 0, 0)); + ok(false, "nsITableEditor.getCellAt(null) should cause throwing an exception when editor does not have Selection"); + } catch (e) { + ok(true, "nsITableEditor.getCellAt(null) should cause throwing an exception when editor does not have Selection"); + } + + // XXX This is inconsistent behavior with other APIs. + try { + let cell = SpecialPowers.unwrap(getTableEditor().getCellAt(editor, 0, 0)); + ok(true, "nsITableEditor.getCellAt() should not cause throwing exception even if given node is not a <table>"); + is(cell, null, "nsITableEditor.getCellAt() should return null if given node is not a <table>"); + } catch (e) { + ok(false, "nsITableEditor.getCellAt() should not cause throwing exception even if given node is not a <table>"); + } + + editor.innerHTML = + '<table id="table">' + + '<tr><td id="c1-1">cell1-1</td><td id="c1-2">cell1-2</td><td id="c1-3">cell1-3</td><td id="c1-4" colspan="2" rowspan="2">cell1-4</td></tr>' + + '<tr><td id="c2-1" rowspan="2">cell2-1</td><td id="c2-2">cell2-2<td id="c2-3">cell2-3</td></tr>' + + '<tr><td id="c3-2">cell3-2</td><td id="c3-3">cell3-3</td><td id="c3-4" colspan="2">cell3-4</td></tr>' + + '<tr><td id="c4-1" rowspan="4">cell4-1</td><td id="c4-2">' + + '<table id="inner-table"><tr><td id="c2-1-1">cell2-1-1</td><td id="c2-1-2">cell2-1-2</td></tr>' + + '<tr><td id="c2-2-1">cell2-2-1</td><td id="c2-2-2">cell2-2-2</td></table>' + + '</td><td id="c4-3">cell4-3</td><td id="c4-4">cell4-4</td><td id="c4-5">cell4-5</td></tr>' + + '<tr><td id="c5-2">cell5-2</td><td id="c5-3" colspan="2">cell5-3</td><td id="c5-5">cell5-5</td></tr>' + + '<tr><td id="c6-2">cell6-2</td><td id="c6-3">cell6-3</td><td id="c6-4"><p>cell6-4</p></td><td id="c6-5">cell6-5</td></tr>' + + '<tr><td id="c7-2" colspan="4">cell7-2</td></tr>' + + "</table>"; + + const kTestsInParent = [ + { row: 0, column: 0, expected: "c1-1" }, + { row: 0, column: 3, expected: "c1-4" }, + { row: 0, column: 4, expected: "c1-4" }, + { row: 1, column: 3, expected: "c1-4" }, + { row: 1, column: 4, expected: "c1-4" }, + { row: 1, column: 0, expected: "c2-1" }, + { row: 2, column: 0, expected: "c2-1" }, + { row: 3, column: 0, expected: "c4-1" }, + { row: 4, column: 0, expected: "c4-1" }, + { row: 5, column: 0, expected: "c4-1" }, + { row: 6, column: 0, expected: "c4-1" }, + { row: 4, column: 2, expected: "c5-3" }, + { row: 4, column: 3, expected: "c5-3" }, + { row: 4, column: 4, expected: "c5-5" }, + { row: 6, column: 1, expected: "c7-2" }, + { row: 6, column: 2, expected: "c7-2" }, + { row: 6, column: 3, expected: "c7-2" }, + { row: 6, column: 4, expected: "c7-2" }, + { row: 6, column: 5, expected: null }, + ]; + + let table = document.getElementById("table"); + for (const kTest of kTestsInParent) { + let cell = SpecialPowers.unwrap(getTableEditor().getCellAt(table, kTest.row, kTest.column)); + if (kTest.expected === null) { + is(cell, null, + `Specified the parent <table> element directly (${kTest.row} - ${kTest.column})`); + } else { + is(cell.getAttribute("id"), kTest.expected, + `Specified the parent <table> element directly (${kTest.row} - ${kTest.column})`); + } + if (cell && cell.firstChild && cell.firstChild.nodeType == Node.TEXT_NODE) { + selection.collapse(cell.firstChild, 0); + cell = getTableEditor().getCellAt(null, kTest.row, kTest.column); + is(cell.getAttribute("id"), kTest.expected, + `Selection is collapsed in a cell element in the parent <table> (${kTest.row} - ${kTest.column})`); + } + } + + const kTestsInChild = [ + { row: 0, column: 0, expected: "c2-1-1" }, + { row: 0, column: 1, expected: "c2-1-2" }, + { row: 0, column: 2, expected: null }, + { row: 1, column: 0, expected: "c2-2-1" }, + { row: 1, column: 1, expected: "c2-2-2" }, + { row: 2, column: 0, expected: null }, + ]; + + let innerTable = document.getElementById("inner-table"); + for (const kTest of kTestsInChild) { + let cell = SpecialPowers.unwrap(getTableEditor().getCellAt(innerTable, kTest.row, kTest.column)); + if (kTest.expected === null) { + is(cell, null, + `Specified the inner <table> element directly (${kTest.row} - ${kTest.column})`); + } else { + is(cell.getAttribute("id"), kTest.expected, + `Specified the inner <table> element directly (${kTest.row} - ${kTest.column})`); + } + if (cell && cell.firstChild && cell.firstChild.nodeType == Node.TEXT_NODE) { + selection.collapse(cell.firstChild, 0); + cell = getTableEditor().getCellAt(null, kTest.row, kTest.column); + is(cell.getAttribute("id"), kTest.expected, + `Selection is collapsed in a cell element in the inner <table> (${kTest.row} - ${kTest.column})`); + } + } + + SimpleTest.finish(); +}); + +function getTableEditor() { + var Ci = SpecialPowers.Ci; + var editingSession = SpecialPowers.wrap(window).docShell.editingSession; + return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsITableEditor); +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_nsITableEditor_getCellDataAt.html b/editor/libeditor/tests/test_nsITableEditor_getCellDataAt.html new file mode 100644 index 0000000000..388281c044 --- /dev/null +++ b/editor/libeditor/tests/test_nsITableEditor_getCellDataAt.html @@ -0,0 +1,751 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test for nsITableEditor.getCellDataAt()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> +<div id="display"> +</div> +<div id="content" contenteditable>out of table<table><tr><td>default content</td></tr></table></div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + let editor = document.getElementById("content"); + let selection = document.getSelection(); + + let cellElementWrapper; + let startRowIndexWrapper, startColumnIndexWrapper; + let rowspanWrapper, colspanWrapper; + let effectiveRowspanWrapper, effectiveColspanWrapper; + let isSelectedWrapper; + + function reset() { + cellElementWrapper = {}; + startRowIndexWrapper = {}; + startColumnIndexWrapper = {}; + rowspanWrapper = {}; + colspanWrapper = {}; + effectiveRowspanWrapper = {}; + effectiveColspanWrapper = {}; + isSelectedWrapper = {}; + } + + editor.focus(); + selection.collapse(editor.firstChild, 0); + try { + getTableEditor().getCellDataAt(null, 0, 0, + cellElementWrapper, + startRowIndexWrapper, startColumnIndexWrapper, + rowspanWrapper, colspanWrapper, + effectiveRowspanWrapper, effectiveColspanWrapper, + isSelectedWrapper); + ok(false, "getTableEditor().getCellDataAt(null, 0, 0) should throw exception when selection is outside of any <table>s"); + } catch (e) { + ok(true, "getTableEditor().getCellDataAt(null, 0, 0) should throw exception when selection is outside of any <table>s"); + } + + selection.removeAllRanges(); + try { + getTableEditor().getCellDataAt(null, 0, 0, + cellElementWrapper, + startRowIndexWrapper, startColumnIndexWrapper, + rowspanWrapper, colspanWrapper, + effectiveRowspanWrapper, effectiveColspanWrapper, + isSelectedWrapper); + ok(false, "getTableEditor().getCellDataAt(null, 0, 0) should throw exception when selection has no ranges"); + } catch (e) { + ok(true, "getTableEditor().getCellDataAt(null, 0, 0) should throw exception when selection has no ranges"); + } + + // Collapse in text node in the cell element. + selection.collapse(editor.firstChild.nextSibling.firstChild.firstChild.firstChild.firstChild, 0); + reset(); + getTableEditor().getCellDataAt(null, 0, 0, + cellElementWrapper, + startRowIndexWrapper, startColumnIndexWrapper, + rowspanWrapper, colspanWrapper, + effectiveRowspanWrapper, effectiveColspanWrapper, + isSelectedWrapper); + is(cellElementWrapper.value, editor.firstChild.nextSibling.firstChild.firstChild.firstChild, + "getTableEditor().getCellDataAt(null, 0, 0) should return the <td> element when selection is in it"); + is(startRowIndexWrapper.value, 0, + "getTableEditor().getCellDataAt(null, 0, 0) should return 0 for startRowIndex when selection is in the cell"); + is(startColumnIndexWrapper.value, 0, + "getTableEditor().getCellDataAt(null, 0, 0) should return 0 for startColumnIndex when selection is in the cell"); + is(rowspanWrapper.value, 1, + "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for rowspan when selection is in the cell"); + is(colspanWrapper.value, 1, + "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for colspan when selection is in the cell"); + is(effectiveRowspanWrapper.value, 1, + "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for effectiveRowspan when selection is in the cell"); + is(effectiveColspanWrapper.value, 1, + "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for effectiveColspan when selection is in the cell"); + is(isSelectedWrapper.value, false, + "getTableEditor().getCellDataAt(null, 0, 0) should return false for isSelected when selection is in the cell"); + + // Select the cell + selection.setBaseAndExtent(editor.firstChild.nextSibling.firstChild.firstChild, 0, + editor.firstChild.nextSibling.firstChild.firstChild, 1); + reset(); + getTableEditor().getCellDataAt(null, 0, 0, + cellElementWrapper, + startRowIndexWrapper, startColumnIndexWrapper, + rowspanWrapper, colspanWrapper, + effectiveRowspanWrapper, effectiveColspanWrapper, + isSelectedWrapper); + is(cellElementWrapper.value, editor.firstChild.nextSibling.firstChild.firstChild.firstChild, + "getTableEditor().getCellDataAt(null, 0, 0) should return the <td> element when it's selected"); + is(startRowIndexWrapper.value, 0, + "getTableEditor().getCellDataAt(null, 0, 0) should return 0 for startRowIndex when the cell is selected"); + is(startColumnIndexWrapper.value, 0, + "getTableEditor().getCellDataAt(null, 0, 0) should return 0 for startColumnIndex when the cell is selected"); + is(rowspanWrapper.value, 1, + "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for rowspan when the cell is selected"); + is(colspanWrapper.value, 1, + "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for colspan when the cell is selected"); + is(effectiveRowspanWrapper.value, 1, + "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for effectiveRowspan when the cell is selected"); + is(effectiveColspanWrapper.value, 1, + "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for effectiveColspan when the cell is selected"); + is(isSelectedWrapper.value, true, + "getTableEditor().getCellDataAt(null, 0, 0) should return true for isSelected when the cell is selected"); + + // Select the <tr> + selection.setBaseAndExtent(editor.firstChild.nextSibling.firstChild, 0, + editor.firstChild.nextSibling.firstChild, 1); + reset(); + getTableEditor().getCellDataAt(null, 0, 0, + cellElementWrapper, + startRowIndexWrapper, startColumnIndexWrapper, + rowspanWrapper, colspanWrapper, + effectiveRowspanWrapper, effectiveColspanWrapper, + isSelectedWrapper); + is(cellElementWrapper.value, editor.firstChild.nextSibling.firstChild.firstChild.firstChild, + "getTableEditor().getCellDataAt(null, 0, 0) should return the <td> element when the <tr> is selected"); + is(startRowIndexWrapper.value, 0, + "getTableEditor().getCellDataAt(null, 0, 0) should return 0 for startRowIndex when the <tr> is selected"); + is(startColumnIndexWrapper.value, 0, + "getTableEditor().getCellDataAt(null, 0, 0) should return 0 for startColumnIndex when the <tr> is selected"); + is(rowspanWrapper.value, 1, + "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for rowspan when the <tr> is selected"); + is(colspanWrapper.value, 1, + "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for colspan when the <tr> is selected"); + is(effectiveRowspanWrapper.value, 1, + "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for effectiveRowspan when the <tr> is selected"); + is(effectiveColspanWrapper.value, 1, + "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for effectiveColspan when the <tr> is selected"); + is(isSelectedWrapper.value, true, + "getTableEditor().getCellDataAt(null, 0, 0) should return true for isSelected when the <tr> is selected"); + + // Select the <table> + selection.setBaseAndExtent(editor, 1, editor, 2); + reset(); + getTableEditor().getCellDataAt(null, 0, 0, + cellElementWrapper, + startRowIndexWrapper, startColumnIndexWrapper, + rowspanWrapper, colspanWrapper, + effectiveRowspanWrapper, effectiveColspanWrapper, + isSelectedWrapper); + is(cellElementWrapper.value, editor.firstChild.nextSibling.firstChild.firstChild.firstChild, + "getTableEditor().getCellDataAt(null, 0, 0) should return the <td> element when the <table> is selected"); + is(startRowIndexWrapper.value, 0, + "getTableEditor().getCellDataAt(null, 0, 0) should return 0 for startRowIndex when the <table> is selected"); + is(startColumnIndexWrapper.value, 0, + "getTableEditor().getCellDataAt(null, 0, 0) should return 0 for startColumnIndex when the <table> is selected"); + is(rowspanWrapper.value, 1, + "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for rowspan when the <table> is selected"); + is(colspanWrapper.value, 1, + "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for colspan when the <table> is selected"); + is(effectiveRowspanWrapper.value, 1, + "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for effectiveRowspan when the <table> is selected"); + is(effectiveColspanWrapper.value, 1, + "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for effectiveColspan when the <table> is selected"); + is(isSelectedWrapper.value, true, + "getTableEditor().getCellDataAt(null, 0, 0) should return true for isSelected when the <table> is selected"); + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + "<tr><td>cell1-1</td><td>cell1-2</td></tr>" + + "<tr><td>cell2-1</td><td>cell2-2</td></tr>" + + "<tr><td>cell3-1</td><td>cell3-2</td></tr>" + + "</table>"; + editor.focus(); + reset(); + getTableEditor().getCellDataAt(editor.firstChild, 0, 0, + cellElementWrapper, + startRowIndexWrapper, startColumnIndexWrapper, + rowspanWrapper, colspanWrapper, + effectiveRowspanWrapper, effectiveColspanWrapper, + isSelectedWrapper); + is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.firstChild, + "getTableEditor().getCellDataAt(<table>, 0, 0) should return the first <td> element"); + is(startRowIndexWrapper.value, 0, + "getTableEditor().getCellDataAt(<table>, 0, 0) should return 0 for startRowIndex"); + is(startColumnIndexWrapper.value, 0, + "getTableEditor().getCellDataAt(<table>, 0, 0) should return 0 for startColumnIndex"); + is(rowspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 0, 0) should return 1 for rowspan"); + is(colspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 0, 0) should return 1 for colspan"); + is(effectiveRowspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 0, 0) should return 1 for effectiveRowspan"); + is(effectiveColspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 0, 0) should return 1 for effectiveColspan"); + is(isSelectedWrapper.value, false, + "getTableEditor().getCellDataAt(<table>, 0, 0) should return false for isSelected"); + + reset(); + getTableEditor().getCellDataAt(editor.firstChild, 0, 1, + cellElementWrapper, + startRowIndexWrapper, startColumnIndexWrapper, + rowspanWrapper, colspanWrapper, + effectiveRowspanWrapper, effectiveColspanWrapper, + isSelectedWrapper); + is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.firstChild.nextSibling, + "getTableEditor().getCellDataAt(<table>, 0, 1) should return the second <td> element"); + is(startRowIndexWrapper.value, 0, + "getTableEditor().getCellDataAt(<table>, 0, 1) should return 0 for startRowIndex"); + is(startColumnIndexWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 0, 1) should return 1 for startColumnIndex"); + is(rowspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 0, 1) should return 1 for rowspan"); + is(colspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 0, 1) should return 1 for colspan"); + is(effectiveRowspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 0, 1) should return 1 for effectiveRowspan"); + is(effectiveColspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 0, 1) should return 1 for effectiveColspan"); + is(isSelectedWrapper.value, false, + "getTableEditor().getCellDataAt(<table>, 0, 1) should return false for isSelected"); + + try { + getTableEditor().getCellDataAt(editor.firstChild, 0, 2, + cellElementWrapper, + startRowIndexWrapper, startColumnIndexWrapper, + rowspanWrapper, colspanWrapper, + effectiveRowspanWrapper, effectiveColspanWrapper, + isSelectedWrapper); + ok(false, "getTableEditor().getCellDataAt(<table>, 0, 2) should throw exception since column index is out of bounds"); + } catch (e) { + ok(true, "getTableEditor().getCellDataAt(<table>, 0, 2) should throw exception since column index is out of bounds"); + } + + reset(); + getTableEditor().getCellDataAt(editor.firstChild, 1, 0, + cellElementWrapper, + startRowIndexWrapper, startColumnIndexWrapper, + rowspanWrapper, colspanWrapper, + effectiveRowspanWrapper, effectiveColspanWrapper, + isSelectedWrapper); + is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.nextSibling.firstChild, + "getTableEditor().getCellDataAt(<table>, 1, 0) should return the first <td> element in the second row"); + is(startRowIndexWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 1, 0) should return 1 for startRowIndex"); + is(startColumnIndexWrapper.value, 0, + "getTableEditor().getCellDataAt(<table>, 1, 0) should return 0 for startColumnIndex"); + is(rowspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 1, 0) should return 1 for rowspan"); + is(colspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 1, 0) should return 1 for colspan"); + is(effectiveRowspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 1, 0) should return 1 for effectiveRowspan"); + is(effectiveColspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 1, 0) should return 1 for effectiveColspan"); + is(isSelectedWrapper.value, false, + "getTableEditor().getCellDataAt(<table>, 1, 0) should return false for isSelected"); + + reset(); + getTableEditor().getCellDataAt(editor.firstChild, 2, 1, + cellElementWrapper, + startRowIndexWrapper, startColumnIndexWrapper, + rowspanWrapper, colspanWrapper, + effectiveRowspanWrapper, effectiveColspanWrapper, + isSelectedWrapper); + is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.nextSibling.nextSibling.firstChild.nextSibling, + "getTableEditor().getCellDataAt(<table>, 2, 1) should return the second <td> element in the last row"); + is(startRowIndexWrapper.value, 2, + "getTableEditor().getCellDataAt(<table>, 2, 1) should return 1 for startRowIndex"); + is(startColumnIndexWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 2, 1) should return 1 for startColumnIndex"); + is(rowspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 2, 1) should return 1 for rowspan"); + is(colspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 2, 1) should return 1 for colspan"); + is(effectiveRowspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 2, 1) should return 1 for effectiveRowspan"); + is(effectiveColspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 2, 1) should return 1 for effectiveColspan"); + is(isSelectedWrapper.value, false, + "getTableEditor().getCellDataAt(<table>, 2, 1) should return false for isSelected"); + + try { + getTableEditor().getCellDataAt(editor.firstChild, 2, 2, + cellElementWrapper, + startRowIndexWrapper, startColumnIndexWrapper, + rowspanWrapper, colspanWrapper, + effectiveRowspanWrapper, effectiveColspanWrapper, + isSelectedWrapper); + ok(false, "getTableEditor().getCellDataAt(<table>, 2, 2) should throw exception since column index is out of bounds"); + } catch (e) { + ok(true, "getTableEditor().getCellDataAt(<table>, 2, 2) should throw exception since column index is out of bounds"); + } + + try { + getTableEditor().getCellDataAt(editor.firstChild, 3, 0, + cellElementWrapper, + startRowIndexWrapper, startColumnIndexWrapper, + rowspanWrapper, colspanWrapper, + effectiveRowspanWrapper, effectiveColspanWrapper, + isSelectedWrapper); + ok(false, "getTableEditor().getCellDataAt(<table>, 3, 0) should throw exception since row index is out of bounds"); + } catch (e) { + ok(true, "getTableEditor().getCellDataAt(<table>, 3, 0) should throw exception since row index is out of bounds"); + } + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td rowspan="3">cell1-1</td><td>cell1-2</td><td>cell1-3</td><td>cell1-4</td></tr>' + + "<tr><td>cell2-2</td></tr>" + + "<tr><td>cell3-2</td><td>cell3-3</td></tr>" + + '<tr><td colspan="3">cell4-1</td><td>cell4-4</td></tr>' + + "</table>"; + editor.focus(); + reset(); + getTableEditor().getCellDataAt(editor.firstChild, 0, 0, + cellElementWrapper, + startRowIndexWrapper, startColumnIndexWrapper, + rowspanWrapper, colspanWrapper, + effectiveRowspanWrapper, effectiveColspanWrapper, + isSelectedWrapper); + is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.firstChild, + "getTableEditor().getCellDataAt(<table>, 0, 0) should return the first <td> element whose rowspan is 3"); + is(startRowIndexWrapper.value, 0, + "getTableEditor().getCellDataAt(<table>, 0, 0) should return 0 for startRowIndex (the cell's rowspan is 3)"); + is(startColumnIndexWrapper.value, 0, + "getTableEditor().getCellDataAt(<table>, 0, 0) should return 0 for startColumnIndex (the cell's rowspan is 3)"); + is(rowspanWrapper.value, 3, + "getTableEditor().getCellDataAt(<table>, 0, 0) should return 3 for rowspan (the cell's rowspan is 3)"); + is(colspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 0, 0) should return 1 for colspan (the cell's rowspan is 3)"); + is(effectiveRowspanWrapper.value, 3, + "getTableEditor().getCellDataAt(<table>, 0, 0) should return 3 for effectiveRowspan (the cell's rowspan is 3)"); + is(effectiveColspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 0, 0) should return 1 for effectiveColspan (the cell's rowspan is 3)"); + is(isSelectedWrapper.value, false, + "getTableEditor().getCellDataAt(<table>, 0, 0) should return false for isSelected (the cell's rowspan is 3)"); + + reset(); + getTableEditor().getCellDataAt(editor.firstChild, 1, 0, + cellElementWrapper, + startRowIndexWrapper, startColumnIndexWrapper, + rowspanWrapper, colspanWrapper, + effectiveRowspanWrapper, effectiveColspanWrapper, + isSelectedWrapper); + is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.firstChild, + "getTableEditor().getCellDataAt(<table>, 1, 0) should return the first <td> element whose rowspan is 3"); + is(startRowIndexWrapper.value, 0, + "getTableEditor().getCellDataAt(<table>, 1, 0) should return 0 for startRowIndex (the cell's rowspan is 3)"); + is(startColumnIndexWrapper.value, 0, + "getTableEditor().getCellDataAt(<table>, 1, 0) should return 0 for startColumnIndex (the cell's rowspan is 3)"); + is(rowspanWrapper.value, 3, + "getTableEditor().getCellDataAt(<table>, 1, 0) should return 3 for rowspan (the cell's rowspan is 3)"); + is(colspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 1, 0) should return 1 for colspan (the cell's rowspan is 3)"); + is(effectiveRowspanWrapper.value, 2, + "getTableEditor().getCellDataAt(<table>, 1, 0) should return 2 for effectiveRowspan (the cell's rowspan is 3)"); + is(effectiveColspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 1, 0) should return 1 for effectiveColspan (the cell's rowspan is 3)"); + is(isSelectedWrapper.value, false, + "getTableEditor().getCellDataAt(<table>, 1, 0) should return false for isSelected (the cell's rowspan is 3)"); + + reset(); + getTableEditor().getCellDataAt(editor.firstChild, 2, 0, + cellElementWrapper, + startRowIndexWrapper, startColumnIndexWrapper, + rowspanWrapper, colspanWrapper, + effectiveRowspanWrapper, effectiveColspanWrapper, + isSelectedWrapper); + is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.firstChild, + "getTableEditor().getCellDataAt(<table>, 2, 0) should return the first <td> element whose rowspan is 3"); + is(startRowIndexWrapper.value, 0, + "getTableEditor().getCellDataAt(<table>, 2, 0) should return 0 for startRowIndex (the cell's rowspan is 3)"); + is(startColumnIndexWrapper.value, 0, + "getTableEditor().getCellDataAt(<table>, 2, 0) should return 0 for startColumnIndex (the cell's rowspan is 3)"); + is(rowspanWrapper.value, 3, + "getTableEditor().getCellDataAt(<table>, 2, 0) should return 3 for rowspan (the cell's rowspan is 3)"); + is(colspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 2, 0) should return 1 for colspan (the cell's rowspan is 3)"); + is(effectiveRowspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 2, 0) should return 1 for effectiveRowspan (the cell's rowspan is 3)"); + is(effectiveColspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 2, 0) should return 1 for effectiveColspan (the cell's rowspan is 3)"); + is(isSelectedWrapper.value, false, + "getTableEditor().getCellDataAt(<table>, 2, 0) should return false for isSelected (the cell's rowspan is 3)"); + + reset(); + getTableEditor().getCellDataAt(editor.firstChild, 3, 0, + cellElementWrapper, + startRowIndexWrapper, startColumnIndexWrapper, + rowspanWrapper, colspanWrapper, + effectiveRowspanWrapper, effectiveColspanWrapper, + isSelectedWrapper); + is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.nextSibling.nextSibling.nextSibling.firstChild, + "getTableEditor().getCellDataAt(<table>, 3, 0) should return the first <td> element in the last row whose colspan is 3"); + is(startRowIndexWrapper.value, 3, + "getTableEditor().getCellDataAt(<table>, 3, 0) should return 3 for startRowIndex (the cell's colspan is 3)"); + is(startColumnIndexWrapper.value, 0, + "getTableEditor().getCellDataAt(<table>, 3, 0) should return 0 for startColumnIndex (the cell's colspan is 3)"); + is(rowspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 3, 0) should return 1 for rowspan (the cell's colspan is 3)"); + is(colspanWrapper.value, 3, + "getTableEditor().getCellDataAt(<table>, 3, 0) should return 3 for colspan (the cell's colspan is 3)"); + is(effectiveRowspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 3, 0) should return 1 for effectiveRowspan (the cell's colspan is 3)"); + is(effectiveColspanWrapper.value, 3, + "getTableEditor().getCellDataAt(<table>, 3, 0) should return 3 for effectiveColspan (the cell's colspan is 3)"); + is(isSelectedWrapper.value, false, + "getTableEditor().getCellDataAt(<table>, 3, 0) should return false for isSelected (the cell's colspan is 3)"); + + reset(); + getTableEditor().getCellDataAt(editor.firstChild, 3, 1, + cellElementWrapper, + startRowIndexWrapper, startColumnIndexWrapper, + rowspanWrapper, colspanWrapper, + effectiveRowspanWrapper, effectiveColspanWrapper, + isSelectedWrapper); + is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.nextSibling.nextSibling.nextSibling.firstChild, + "getTableEditor().getCellDataAt(<table>, 3, 1) should return the first <td> element in the last row whose colspan is 3"); + is(startRowIndexWrapper.value, 3, + "getTableEditor().getCellDataAt(<table>, 3, 1) should return 3 for startRowIndex (the cell's colspan is 3)"); + is(startColumnIndexWrapper.value, 0, + "getTableEditor().getCellDataAt(<table>, 3, 1) should return 0 for startColumnIndex (the cell's colspan is 3)"); + is(rowspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 3, 1) should return 1 for rowspan (the cell's colspan is 3)"); + is(colspanWrapper.value, 3, + "getTableEditor().getCellDataAt(<table>, 3, 1) should return 3 for colspan (the cell's colspan is 3)"); + is(effectiveRowspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 3, 1) should return 1 for effectiveRowspan (the cell's colspan is 3)"); + is(effectiveColspanWrapper.value, 2, + "getTableEditor().getCellDataAt(<table>, 3, 1) should return 2 for effectiveColspan (the cell's colspan is 3)"); + is(isSelectedWrapper.value, false, + "getTableEditor().getCellDataAt(<table>, 3, 1) should return false for isSelected (the cell's colspan is 3)"); + + reset(); + getTableEditor().getCellDataAt(editor.firstChild, 3, 2, + cellElementWrapper, + startRowIndexWrapper, startColumnIndexWrapper, + rowspanWrapper, colspanWrapper, + effectiveRowspanWrapper, effectiveColspanWrapper, + isSelectedWrapper); + is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.nextSibling.nextSibling.nextSibling.firstChild, + "getTableEditor().getCellDataAt(<table>, 3, 2) should return the first <td> element in the last row whose colspan is 3"); + is(startRowIndexWrapper.value, 3, + "getTableEditor().getCellDataAt(<table>, 3, 2) should return 3 for startRowIndex (the cell's colspan is 3)"); + is(startColumnIndexWrapper.value, 0, + "getTableEditor().getCellDataAt(<table>, 3, 2) should return 0 for startColumnIndex (the cell's colspan is 3)"); + is(rowspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 3, 2) should return 1 for rowspan (the cell's colspan is 3)"); + is(colspanWrapper.value, 3, + "getTableEditor().getCellDataAt(<table>, 3, 2) should return 3 for colspan (the cell's colspan is 3)"); + is(effectiveRowspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 3, 2) should return 1 for effectiveRowspan (the cell's colspan is 3)"); + is(effectiveColspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 3, 2) should return 1 for effectiveColspan (the cell's colspan is 3)"); + is(isSelectedWrapper.value, false, + "getTableEditor().getCellDataAt(<table>, 3, 2) should return false for isSelected (the cell's colspan is 3)"); + + reset(); + getTableEditor().getCellDataAt(editor.firstChild, 3, 3, + cellElementWrapper, + startRowIndexWrapper, startColumnIndexWrapper, + rowspanWrapper, colspanWrapper, + effectiveRowspanWrapper, effectiveColspanWrapper, + isSelectedWrapper); + is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.nextSibling.nextSibling.nextSibling.firstChild.nextSibling, + "getTableEditor().getCellDataAt(<table>, 3, 3) should return the second <td> element in the last row"); + is(startRowIndexWrapper.value, 3, + "getTableEditor().getCellDataAt(<table>, 3, 3) should return 3 for startRowIndex (right cell of the cell whose colspan is 3)"); + is(startColumnIndexWrapper.value, 3, + "getTableEditor().getCellDataAt(<table>, 3, 3) should return 3 for startColumnIndex (right cell of the cell whose colspan is 3)"); + is(rowspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 3, 3) should return 1 for rowspan (right cell of the cell whose colspan is 3)"); + is(colspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 3, 3) should return 1 for colspan (right cell of the cell whose colspan is 3)"); + is(effectiveRowspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 3, 3) should return 1 for effectiveRowspan (right cell of the cell whose colspan is 3)"); + is(effectiveColspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 3, 3) should return 1 for effectiveColspan (right cell of the cell whose colspan is 3)"); + is(isSelectedWrapper.value, false, + "getTableEditor().getCellDataAt(<table>, 3, 3) should return false for isSelected (right cell of the cell whose colspan is 3)"); + + try { + getTableEditor().getCellDataAt(editor.firstChild, 3, 4, + cellElementWrapper, + startRowIndexWrapper, startColumnIndexWrapper, + rowspanWrapper, colspanWrapper, + effectiveRowspanWrapper, effectiveColspanWrapper, + isSelectedWrapper); + ok(false, "getTableEditor().getCellDataAt(<table>, 3, 4) should throw exception since column index is out of bounds"); + } catch (e) { + ok(true, "getTableEditor().getCellDataAt(<table>, 3, 4) should throw exception since column index is out of bounds"); + } + + reset(); + getTableEditor().getCellDataAt(editor.firstChild, 0, 1, + cellElementWrapper, + startRowIndexWrapper, startColumnIndexWrapper, + rowspanWrapper, colspanWrapper, + effectiveRowspanWrapper, effectiveColspanWrapper, + isSelectedWrapper); + is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.firstChild.nextSibling, + "getTableEditor().getCellDataAt(<table>, 0, 1) should return the second <td> element in the first row"); + is(startRowIndexWrapper.value, 0, + "getTableEditor().getCellDataAt(<table>, 0, 1) should return 0 for startRowIndex (right cell of the cell whose rowspan is 3)"); + is(startColumnIndexWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 0, 1) should return 1 for startColumnIndex (right cell of the cell whose rowspan is 3)"); + is(rowspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 0, 1) should return 1 for rowspan (right cell of the cell whose rowspan is 3)"); + is(colspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 0, 1) should return 1 for colspan (right cell of the cell whose rowspan is 3)"); + is(effectiveRowspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 0, 1) should return 1 for effectiveRowspan (right cell of the cell whose rowspan is 3)"); + is(effectiveColspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 0, 1) should return 1 for effectiveColspan (right cell of the cell whose rowspan is 3)"); + is(isSelectedWrapper.value, false, + "getTableEditor().getCellDataAt(<table>, 0, 1) should return false for isSelected (right cell of the cell whose rowspan is 3)"); + + reset(); + getTableEditor().getCellDataAt(editor.firstChild, 1, 1, + cellElementWrapper, + startRowIndexWrapper, startColumnIndexWrapper, + rowspanWrapper, colspanWrapper, + effectiveRowspanWrapper, effectiveColspanWrapper, + isSelectedWrapper); + is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.nextSibling.firstChild, + "getTableEditor().getCellDataAt(<table>, 1, 1) should return the first <td> element in the second row"); + is(startRowIndexWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 1, 1) should return 1 for startRowIndex (right cell of the cell whose rowspan is 3)"); + is(startColumnIndexWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 1, 1) should return 1 for startColumnIndex (right cell of the cell whose rowspan is 3)"); + is(rowspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 1, 1) should return 1 for rowspan (right cell of the cell whose rowspan is 3)"); + is(colspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 1, 1) should return 1 for colspan (right cell of the cell whose rowspan is 3)"); + is(effectiveRowspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 1, 1) should return 1 for effectiveRowspan (right cell of the cell whose rowspan is 3)"); + is(effectiveColspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 1, 1) should return 1 for effectiveColspan (right cell of the cell whose rowspan is 3)"); + is(isSelectedWrapper.value, false, + "getTableEditor().getCellDataAt(<table>, 1, 1) should return false for isSelected (right cell of the cell whose rowspan is 3)"); + + try { + getTableEditor().getCellDataAt(editor.firstChild, 1, 2, + cellElementWrapper, + startRowIndexWrapper, startColumnIndexWrapper, + rowspanWrapper, colspanWrapper, + effectiveRowspanWrapper, effectiveColspanWrapper, + isSelectedWrapper); + ok(false, "getTableEditor().getCellDataAt(<table>, 1, 2) should throw exception since there is no cell due to non-rectangular table"); + } catch (e) { + ok(true, "getTableEditor().getCellDataAt(<table>, 1, 2) should throw exception since there is no cell due to non-rectangular table"); + } + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td rowspan="3" colspan="2">cell1-1</td></tr>' + + "</table>"; + editor.focus(); + reset(); + getTableEditor().getCellDataAt(editor.firstChild, 0, 0, + cellElementWrapper, + startRowIndexWrapper, startColumnIndexWrapper, + rowspanWrapper, colspanWrapper, + effectiveRowspanWrapper, effectiveColspanWrapper, + isSelectedWrapper); + is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.firstChild, + "getTableEditor().getCellDataAt(<table>, 0, 0) should return the first <td> element whose rowspan is 3 and colspan is 2"); + is(startRowIndexWrapper.value, 0, + "getTableEditor().getCellDataAt(<table>, 0, 0) should return 0 for startRowIndex (the cell's rowspan is 3 and colspan is 2)"); + is(startColumnIndexWrapper.value, 0, + "getTableEditor().getCellDataAt(<table>, 0, 0) should return 0 for startColumnIndex (the cell's rowspan is 3 and colspan is 2)"); + is(rowspanWrapper.value, 3, + "getTableEditor().getCellDataAt(<table>, 0, 0) should return 3 for rowspan (the cell's rowspan is 3 and colspan is 2)"); + is(colspanWrapper.value, 2, + "getTableEditor().getCellDataAt(<table>, 0, 0) should return 2 for colspan (the cell's rowspan is 3 and colspan is 2)"); + // XXX Not sure whether expected behavior or not. + todo_is(effectiveRowspanWrapper.value, 3, + "getTableEditor().getCellDataAt(<table>, 0, 0) should return 3 for effectiveRowspan (the cell's rowspan is 3 and colspan is 2)"); + is(effectiveColspanWrapper.value, 2, + "getTableEditor().getCellDataAt(<table>, 0, 0) should return 2 for effectiveColspan (the cell's rowspan is 3 and colspan is 2)"); + is(isSelectedWrapper.value, false, + "getTableEditor().getCellDataAt(<table>, 0, 0) should return false for isSelected (the cell's rowspan is 3 and colspan is 2)"); + + reset(); + getTableEditor().getCellDataAt(editor.firstChild, 0, 1, + cellElementWrapper, + startRowIndexWrapper, startColumnIndexWrapper, + rowspanWrapper, colspanWrapper, + effectiveRowspanWrapper, effectiveColspanWrapper, + isSelectedWrapper); + is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.firstChild, + "getTableEditor().getCellDataAt(<table>, 0, 1) should return the first <td> element whose rowspan is 3 and colspan is 2"); + is(startRowIndexWrapper.value, 0, + "getTableEditor().getCellDataAt(<table>, 0, 1) should return 0 for startRowIndex (the cell's rowspan is 3 and colspan is 2)"); + is(startColumnIndexWrapper.value, 0, + "getTableEditor().getCellDataAt(<table>, 0, 1) should return 0 for startColumnIndex (the cell's rowspan is 3 and colspan is 2)"); + is(rowspanWrapper.value, 3, + "getTableEditor().getCellDataAt(<table>, 0, 1) should return 3 for rowspan (the cell's rowspan is 3 and colspan is 2)"); + is(colspanWrapper.value, 2, + "getTableEditor().getCellDataAt(<table>, 0, 1) should return 2 for colspan (the cell's rowspan is 3 and colspan is 2)"); + // XXX Not sure whether expected behavior or not. + todo_is(effectiveRowspanWrapper.value, 3, + "getTableEditor().getCellDataAt(<table>, 0, 1) should return 3 for effectiveRowspan (the cell's rowspan is 3 and colspan is 2)"); + is(effectiveColspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 0, 1) should return 1 for effectiveColspan (the cell's rowspan is 3 and colspan is 2)"); + is(isSelectedWrapper.value, false, + "getTableEditor().getCellDataAt(<table>, 0, 1) should return false for isSelected (the cell's rowspan is 3 and colspan is 2)"); + + try { + getTableEditor().getCellDataAt(editor.firstChild, 1, 0, + cellElementWrapper, + startRowIndexWrapper, startColumnIndexWrapper, + rowspanWrapper, colspanWrapper, + effectiveRowspanWrapper, effectiveColspanWrapper, + isSelectedWrapper); + is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.firstChild, + "getTableEditor().getCellDataAt(<table>, 1, 0) should return the first <td> element whose rowspan is 3 and colspan is 2"); + is(startRowIndexWrapper.value, 0, + "getTableEditor().getCellDataAt(<table>, 1, 0) should return 0 for startRowIndex (the cell's rowspan is 3 and colspan is 2)"); + is(startColumnIndexWrapper.value, 0, + "getTableEditor().getCellDataAt(<table>, 1, 0) should return 0 for startColumnIndex (the cell's rowspan is 3 and colspan is 2)"); + is(rowspanWrapper.value, 3, + "getTableEditor().getCellDataAt(<table>, 1, 0) should return 3 for rowspan (the cell's rowspan is 3 and colspan is 2)"); + is(colspanWrapper.value, 2, + "getTableEditor().getCellDataAt(<table>, 1, 0) should return 2 for colspan (the cell's rowspan is 3 and colspan is 2)"); + // XXX Not sure whether expected behavior or not. + todo_is(effectiveRowspanWrapper.value, 2, + "getTableEditor().getCellDataAt(<table>, 1, 0) should return 2 for effectiveRowspan (the cell's rowspan is 3 and colspan is 2)"); + is(effectiveColspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 1, 0) should return 1 for effectiveColspan (the cell's rowspan is 3 and colspan is 2)"); + is(isSelectedWrapper.value, false, + "getTableEditor().getCellDataAt(<table>, 1, 0) should return false for isSelected (the cell's rowspan is 3 and colspan is 2)"); + } catch (e) { + todo(false, "getTableEditor().getCellDataAt(<table>, 1, 0) shouldn't throw exception since rowspan expands the table"); + } + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td rowspan="0">cell1-1</td><td>cell1-2</td></tr>' + + "<tr><td>cell2-2</td></tr>" + + "<tr><td>cell3-2</td></tr>" + + "</table>"; + editor.focus(); + reset(); + getTableEditor().getCellDataAt(editor.firstChild, 0, 0, + cellElementWrapper, + startRowIndexWrapper, startColumnIndexWrapper, + rowspanWrapper, colspanWrapper, + effectiveRowspanWrapper, effectiveColspanWrapper, + isSelectedWrapper); + is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.firstChild, + "getTableEditor().getCellDataAt(<table>, 0, 0) should return the first <td> element whose rowspan is 0"); + is(startRowIndexWrapper.value, 0, + "getTableEditor().getCellDataAt(<table>, 0, 0) should return 0 for startRowIndex (the cell's rowspan is 0)"); + is(startColumnIndexWrapper.value, 0, + "getTableEditor().getCellDataAt(<table>, 0, 0) should return 0 for startColumnIndex (the cell's rowspan is 0)"); + is(rowspanWrapper.value, 0, + "getTableEditor().getCellDataAt(<table>, 0, 0) should return 0 for rowspan (the cell's rowspan is 0)"); + is(colspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 0, 0) should return 1 for colspan (the cell's rowspan is 0)"); + is(effectiveRowspanWrapper.value, 3, + "getTableEditor().getCellDataAt(<table>, 0, 0) should return 3 for effectiveRowspan (the cell's rowspan is 0)"); + is(effectiveColspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 0, 0) should return 1 for effectiveColspan (the cell's rowspan is 0)"); + is(isSelectedWrapper.value, false, + "getTableEditor().getCellDataAt(<table>, 0, 0) should return false for isSelected (the cell's rowspan is 0)"); + + reset(); + getTableEditor().getCellDataAt(editor.firstChild, 1, 0, + cellElementWrapper, + startRowIndexWrapper, startColumnIndexWrapper, + rowspanWrapper, colspanWrapper, + effectiveRowspanWrapper, effectiveColspanWrapper, + isSelectedWrapper); + is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.firstChild, + "getTableEditor().getCellDataAt(<table>, 1, 0) should return the first <td> element whose rowspan is 0"); + is(startRowIndexWrapper.value, 0, + "getTableEditor().getCellDataAt(<table>, 1, 0) should return 0 for startRowIndex (the cell's rowspan is 0)"); + is(startColumnIndexWrapper.value, 0, + "getTableEditor().getCellDataAt(<table>, 1, 0) should return 0 for startColumnIndex (the cell's rowspan is 0)"); + is(rowspanWrapper.value, 0, + "getTableEditor().getCellDataAt(<table>, 1, 0) should return 0 for rowspan (the cell's rowspan is 0)"); + is(colspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 1, 0) should return 1 for colspan (the cell's rowspan is 0)"); + is(effectiveRowspanWrapper.value, 2, + "getTableEditor().getCellDataAt(<table>, 1, 0) should return 3 for effectiveRowspan (the cell's rowspan is 0)"); + is(effectiveColspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 1, 0) should return 1 for effectiveColspan (the cell's rowspan is 0)"); + is(isSelectedWrapper.value, false, + "getTableEditor().getCellDataAt(<table>, 1, 0) should return false for isSelected (the cell's rowspan is 0)"); + + // FYI: colspan must be 1 - 1000, 0 is invalid. + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td colspan="0">cell1-1</td></tr>' + + "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" + + "</table>"; + editor.focus(); + reset(); + getTableEditor().getCellDataAt(editor.firstChild, 0, 0, + cellElementWrapper, + startRowIndexWrapper, startColumnIndexWrapper, + rowspanWrapper, colspanWrapper, + effectiveRowspanWrapper, effectiveColspanWrapper, + isSelectedWrapper); + is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.firstChild, + "getTableEditor().getCellDataAt(<table>, 0, 0) should return the first <td> element whose colspan is 0"); + is(startRowIndexWrapper.value, 0, + "getTableEditor().getCellDataAt(<table>, 0, 0) should return 0 for startRowIndex (the cell's colspan is 0)"); + is(startColumnIndexWrapper.value, 0, + "getTableEditor().getCellDataAt(<table>, 0, 0) should return 0 for startColumnIndex (the cell's colspan is 0)"); + is(rowspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 0, 0) should return 1 for rowspan (the cell's colspan is 0)"); + is(colspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 0, 0) should return 1 for colspan (the cell's colspan is 0)"); + is(effectiveRowspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 0, 0) should return 3 for effectiveRowspan (the cell's colspan is 0)"); + is(effectiveColspanWrapper.value, 1, + "getTableEditor().getCellDataAt(<table>, 0, 0) should return 1 for effectiveColspan (the cell's colspan is 0)"); + is(isSelectedWrapper.value, false, + "getTableEditor().getCellDataAt(<table>, 0, 0) should return false for isSelected (the cell's colspan is 0)"); + + try { + getTableEditor().getCellDataAt(editor.firstChild, 0, 1, + cellElementWrapper, + startRowIndexWrapper, startColumnIndexWrapper, + rowspanWrapper, colspanWrapper, + effectiveRowspanWrapper, effectiveColspanWrapper, + isSelectedWrapper); + ok(false, "getTableEditor().getCellDataAt(<table>, 0, 1) should throw exception since there is no cell due to right side of a cell whose colspan is 0"); + } catch (e) { + ok(true, "getTableEditor().getCellDataAt(<table>, 0, 1) should throw exception since there is no cell due to right side of a cell whose colspan is 0"); + } + + SimpleTest.finish(); +}); + +function getTableEditor() { + var editingSession = SpecialPowers.wrap(window).docShell.editingSession; + return editingSession.getEditorForWindow(window).QueryInterface(SpecialPowers.Ci.nsITableEditor); +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_nsITableEditor_getCellIndexes.html b/editor/libeditor/tests/test_nsITableEditor_getCellIndexes.html new file mode 100644 index 0000000000..08f0e83838 --- /dev/null +++ b/editor/libeditor/tests/test_nsITableEditor_getCellIndexes.html @@ -0,0 +1,91 @@ +<!DOCTYPE> +<html> +<head> + <title>Test for nsITableEditor.getCellIndexes()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> +<div id="display"> +</div> +<div id="content" contenteditable></div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + let editor = document.getElementById("content"); + let selection = document.getSelection(); + let rowIndex = {}, columnIndex = {}; + + try { + getTableEditor().getCellIndexes(undefined, rowIndex, columnIndex); + ok(false, "nsITableEditor.getCellIndexes(undefined) should cause throwing an exception"); + } catch (e) { + ok(true, "nsITableEditor.getCellIndexes(undefined) should cause throwing an exception"); + } + + try { + getTableEditor().getCellIndexes(null, rowIndex, columnIndex); + ok(false, "nsITableEditor.getCellIndexes(null) should cause throwing an exception"); + } catch (e) { + ok(true, "nsITableEditor.getCellIndexes(null) should cause throwing an exception"); + } + + try { + getTableEditor().getCellIndexes(editor, rowIndex, columnIndex); + ok(false, "nsITableEditor.getCellIndexes() should cause throwing an exception if given node is not a <td> nor a <th>"); + } catch (e) { + ok(true, "nsITableEditor.getCellIndexes() should cause throwing an exception if given node is not a <td> nor a <th>"); + } + + // Set id to "test" for the argument for getCellIndexes(). + // Set data-row and data-col to expected indexes. + const kTests = [ + '<table><tr><td id="test" data-row="0" data-col="0">cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>', + '<table><tr><td>cell1-1</td><td id="test" data-row="0" data-col="1">cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>', + '<table><tr><td>cell1-1</td><td>cell1-2</td><td id="test" data-row="0" data-col="2">cell1-3</tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>', + '<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td id="test" data-row="1" data-col="0">cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>', + '<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td id="test" data-row="1" data-col="1">cell2-2</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>', + '<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td>cell2-2</td><td id="test" data-row="1" data-col="2">cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>', + '<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr><tr><td id="test" data-row="2" data-col="0">cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>', + '<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td id="test" data-row="2" data-col="1">cell3-2</td><td>cell3-3</td></tr></table>', + '<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td id="test" data-row="2" data-col="2">cell3-3</td></tr></table>', + '<table><tr><td>cell1-1</td><td id="test" data-row="0" data-col="1" rowspan="2">cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>', + '<table><tr><td>cell1-1</td><td rowspan="2">cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td id="test" data-row="2" data-col="1">cell3-2</td><td>cell3-3</td></tr></table>', + '<table><tr><td>cell1-1</td><td id="test" data-row="0" data-col="1">cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td rowspan="2">cell2-2</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-3</td></tr></table>', + '<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td id="test" data-row="1" data-col="1" rowspan="2">cell2-2</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-3</td></tr></table>', + '<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td id="test" data-row="1" data-col="0" colspan="2">cell2-1</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>', + '<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td colspan="2">cell2-1</td><td id="test" data-row="1" data-col="2">cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>', + '<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td id="test" data-row="1" data-col="0">cell2-1</td><td colspan="2">cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>', + '<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td id="test" data-row="1" data-col="1" colspan="2">cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>', + '<table><tr><th id="test" data-row="0" data-col="0">cell1-1</th><th>cell1-2</th><th>cell1-3</tr><tr><th>cell2-1</th><th>cell2-2</th><th>cell2-3</th></tr><tr><th>cell3-1</th><th>cell3-2</th><th>cell3-3</th></tr></table>', + ]; + + for (const kTest of kTests) { + editor.innerHTML = kTest; + let cell = document.getElementById("test"); + getTableEditor().getCellIndexes(cell, rowIndex, columnIndex); + is(rowIndex.value.toString(10), cell.getAttribute("data-row"), `Specified cell element directly, row Index value of ${kTest}`); + is(columnIndex.value.toString(10), cell.getAttribute("data-col"), `Specified cell element directly, column Index value of ${kTest}`); + selection.collapse(cell.firstChild, 0); + getTableEditor().getCellIndexes(null, rowIndex, columnIndex); + is(rowIndex.value.toString(10), cell.getAttribute("data-row"), `Selection is collapsed in the cell element, row Index value of ${kTest}`); + is(columnIndex.value.toString(10), cell.getAttribute("data-col"), `Selection is collapsed in the cell element, column Index value of ${kTest}`); + } + + SimpleTest.finish(); +}); + +function getTableEditor() { + var Ci = SpecialPowers.Ci; + var editingSession = SpecialPowers.wrap(window).docShell.editingSession; + return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsITableEditor); +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_nsITableEditor_getFirstRow.html b/editor/libeditor/tests/test_nsITableEditor_getFirstRow.html new file mode 100644 index 0000000000..9234538733 --- /dev/null +++ b/editor/libeditor/tests/test_nsITableEditor_getFirstRow.html @@ -0,0 +1,105 @@ +<!DOCTYPE> +<html> +<head> + <title>Test for nsITableEditor.getFirstRow()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> +<div id="display"> +</div> +<div id="content" contenteditable></div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + let editor = document.getElementById("content"); + + try { + SpecialPowers.unwrap(getTableEditor().getFirstRow(undefined)); + ok(false, "nsITableEditor.getFirstRow(undefined) should cause throwing an exception"); + } catch (e) { + ok(true, "nsITableEditor.getFirstRow(undefined) should cause throwing an exception"); + } + + try { + SpecialPowers.unwrap(getTableEditor().getFirstRow(null)); + ok(false, "nsITableEditor.getFirstRow(null) should cause throwing an exception"); + } catch (e) { + ok(true, "nsITableEditor.getFirstRow(null) should cause throwing an exception"); + } + + try { + SpecialPowers.unwrap(getTableEditor().getFirstRow(editor)); + ok(false, "nsITableEditor.getFirstRow() should cause throwing an exception if given node is not in <table>"); + } catch (e) { + ok(true, "nsITableEditor.getFirstRow() should cause throwing an exception if given node is not in <table>"); + } + + // Set id to "test" for the argument for getFirstRow(). + // Set id to "expected" for the expected <tr> element result (if there is). + // Set class of <table> to "hasAnonymousRow" if it does not has <tr> but will be anonymous <tr> element is created. + const kTests = [ + '<table id="test"><tr id="expected"><td>cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>', + '<table><tr id="expected"><td id="test">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>', + '<table><tr id="expected"><td id="test">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>', + '<table><tr id="expected"><td>cell1-1</td><td id="test">cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table>', + '<table><tr id="expected"><td>cell1-1</td><td>cell1-2</td></tr><tr id="test"><td>cell2-1</td><td>cell2-2</td></tr></table>', + '<table><tr id="expected"><td>cell1-1</td><td>cell1-2</td></tr><tr><td id="test">cell2-1</td><td>cell2-2</td></tr></table>', + '<table><tr id="expected"><td>cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td id="test">cell2-2</td></tr></table>', + '<table><tbody id="test"><tr id="expected"><td>cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></tbody></table>', + '<table><tbody><tr id="expected"><td id="test">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></tbody></table>', + '<table><thead id="test"><tr id="expected"><td>cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></thead></table>', + '<table><thead><tr id="expected"><td id="test">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></thead></table>', + '<table><tfoot id="test"><tr id="expected"><td>cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></tfoot></table>', + '<table><tfoot><tr id="expected"><td id="test">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></tfoot></table>', + '<table><thead id="test"><tr id="expected"><td>cell1-1</td><td>cell1-2</td></tr></thead><tbody><tr><td>cell2-1</td><td>cell2-2</td></tr></tbody></table>', + '<table><thead><tr id="expected"><td id="test">cell1-1</td><td>cell1-2</td></tr></thead><tbody><tr><td>cell2-1</td><td>cell2-2</td></tr></tbody></table>', + '<table><thead><tr id="expected"><td>cell1-1</td><td>cell1-2</td></tr></thead><tbody><tr><td id="test">cell2-1</td><td>cell2-2</td></tr></tbody></table>', + '<table><tfoot id="test"><tr id="expected"><td>cell1-1</td><td>cell1-2</td></tr></tfoot><tbody><tr><td>cell2-1</td><td>cell2-2</td></tr></tbody></table>', + '<table><tfoot><tr id="expected"><td id="test">cell1-1</td><td>cell1-2</td></tr></tfoot><tbody><tr><td>cell2-1</td><td>cell2-2</td></tr></tbody></table>', + '<table><tfoot><tr id="expected"><td>cell1-1</td><td>cell1-2</td></tr></tfoot><tbody><tr><td id="test">cell2-1</td><td>cell2-2</td></tr></tbody></table>', + '<table><tr><td><table id="test"><tr id="expected"><td>cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr></table></td></tr></table>', + '<table><tr><td><table><tr id="expected"><td>cell1-1</td><td>cell1-2</td></tr><tr><td id="test">cell2-1</td><td>cell2-2</td></tr></table></td></tr></table>', + '<table id="test"></table>', + '<table><caption id="test">table-caption</caption></table>', + '<table><caption>table-caption</caption><tr id="expected"><td id="test">cell</td></tr></table>', + '<table class="hasAnonymousRow"><td id="test">cell</td></table>', + '<table class="hasAnonymousRow"><td>cell-1</td><td id="test">cell-2</td></table>', + '<table><tr><td><table id="test"></table></td></tr></table>', + '<table><tr><td><table><caption id="test">table-caption</caption></table></td></tr></table>', + '<table><tr><td><table class="hasAnonymousRow"><td id="test">cell</td></table></td></tr></table>', + '<table><tr><td><table class="hasAnonymousRow"><td>cell-1</td><td id="test">cell-2</td></table></td></tr></table>', + '<table><tr id="expected"><td><p id="test">paragraph</p></td></tr></table>', + ]; + + for (const kTest of kTests) { + editor.innerHTML = kTest; + let firstRow = SpecialPowers.unwrap(getTableEditor().getFirstRow(document.getElementById("test"))); + if (document.getElementById("expected")) { + is(firstRow.tagName, "TR", `Result should be a <tr>: ${kTest}`); + is(firstRow.getAttribute("id"), "expected", `Result should be the first <tr> element in the <table>: ${kTest}`); + } else if (document.querySelector(".hasAnonymousRow")) { + is(firstRow.tagName, "TR", `Result should be a <tr>: ${kTest}`); + is(firstRow, document.querySelector(".hasAnonymousRow tr"), `Result should be the anonymous <tr> element in the <table>: ${kTest}`); + } else { + is(firstRow, null, `Result should be null if there is no <tr> element in the <table>: ${kTest}`); + } + } + + SimpleTest.finish(); +}); + +function getTableEditor() { + var Ci = SpecialPowers.Ci; + var editingSession = SpecialPowers.wrap(window).docShell.editingSession; + return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsITableEditor); +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_nsITableEditor_getFirstSelectedCellInTable.html b/editor/libeditor/tests/test_nsITableEditor_getFirstSelectedCellInTable.html new file mode 100644 index 0000000000..f85c338f99 --- /dev/null +++ b/editor/libeditor/tests/test_nsITableEditor_getFirstSelectedCellInTable.html @@ -0,0 +1,201 @@ +<!DOCTYPE> +<html> +<head> + <title>Test for nsITableEditor.getFirstSelectedCellInTable()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> +<div id="display"> +</div> +<div id="content" contenteditable></div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + let editor = document.getElementById("content"); + let selection = document.getSelection(); + + selection.collapse(editor, 0); + let rowWrapper = {}; + let colWrapper = {}; + let cell = SpecialPowers.unwrap(getTableEditor().getFirstSelectedCellInTable(rowWrapper, colWrapper)); + is(cell, null, + "nsITableEditor.getFirstSelectedCellInTable() should return null if Selection does not select cells"); + is(rowWrapper.value, 0, + "nsITableEditor.getFirstSelectedCellInTable() should return 0 to row number if Selection does not select cells"); + is(colWrapper.value, 0, + "nsITableEditor.getFirstSelectedCellInTable() should return 0 to row number if Selection does not select cells"); + + editor.innerHTML = + '<table id="table">' + + '<tr id="r1"><td id="c1-1">cell1-1</td><td id="c1-2">cell1-2</td><td id="c1-3">cell1-3</td><td id="c1-4" colspan="2" rowspan="2">cell1-4</td></tr>' + + '<tr id="r2"><th id="c2-1" rowspan="2">cell2-1</th><td id="c2-2">cell2-2<td id="c2-3">cell2-3</td></tr>' + + '<tr id="r3"><td id="c3-2">cell3-2</td><td id="c3-3">cell3-3</td><td id="c3-4" colspan="2">cell3-4</td></tr>' + + '<tr id="r4"><td id="c4-1" rowspan="4">cell4-1</td><td id="c4-2">cell4-2</td><td id="c4-3">cell4-3</td><th id="c4-4">cell4-4</th><td id="c4-5">cell4-5</td></tr>' + + '<tr id="r5"><th id="c5-2">cell5-2</th><th id="c5-3" colspan="2">cell5-3</th><td id="c5-5">cell5-5</td></tr>' + + '<tr id="r6"><td id="c6-2">cell6-2</td><td id="c6-3">cell6-3</td><td id="c6-4"><p>cell6-4</p></td><td id="c6-5">cell6-5</td></tr>' + + '<tr id="r7"><td id="c7-2" colspan="4">cell7-2</td></tr>' + + "</table>"; + + let tr = document.getElementById("r1"); + selection.setBaseAndExtent(tr, 0, tr, 1); + cell = SpecialPowers.unwrap(getTableEditor().getFirstSelectedCellInTable(rowWrapper, colWrapper)); + is(cell, document.getElementById("c1-1"), + "#1-1 nsITableEditor.getFirstSelectedCellInTable() should return the first cell element in the first row"); + is(rowWrapper.value, 0, + "#1-1 nsITableEditor.getFirstSelectedCellInTable() should return 0 to row number for the first row"); + is(colWrapper.value, 0, + "#1-1 nsITableEditor.getFirstSelectedCellInTable() should return 0 to column number for the first column"); + + tr = document.getElementById("r1"); + selection.setBaseAndExtent(tr, 3, tr, 4); + cell = SpecialPowers.unwrap(getTableEditor().getFirstSelectedCellInTable(rowWrapper, colWrapper)); + is(cell, document.getElementById("c1-4"), + "#1-4 nsITableEditor.getFirstSelectedCellInTable() should return the last cell element whose colspan and rowspan are 2 in the first row"); + is(rowWrapper.value, 0, + "#1-4 nsITableEditor.getFirstSelectedCellInTable() should return 0 to row number for the first row"); + is(colWrapper.value, 3, + "#1-4 nsITableEditor.getFirstSelectedCellInTable() should return 3 to column number for the forth column"); + + tr = document.getElementById("r2"); + selection.setBaseAndExtent(tr, 0, tr, 1); + cell = SpecialPowers.unwrap(getTableEditor().getFirstSelectedCellInTable(rowWrapper, colWrapper)); + is(cell, document.getElementById("c2-1"), + "#2-1 nsITableEditor.getFirstSelectedCellInTable() should return the first cell element in the second row"); + is(rowWrapper.value, 1, + "#2-1 nsITableEditor.getFirstSelectedCellInTable() should return 1 to row number for the second row"); + is(colWrapper.value, 0, + "#2-1 nsITableEditor.getFirstSelectedCellInTable() should return 0 to column number for the first column"); + + tr = document.getElementById("r7"); + selection.setBaseAndExtent(tr, 0, tr, 1); + cell = SpecialPowers.unwrap(getTableEditor().getFirstSelectedCellInTable(rowWrapper, colWrapper)); + is(cell, document.getElementById("c7-2"), + "#7-2 nsITableEditor.getFirstSelectedCellInTable() should return the second cell element in the last row"); + is(rowWrapper.value, 6, + "#7-2 nsITableEditor.getFirstSelectedCellInTable() should return 6 to row number for the seventh row"); + is(colWrapper.value, 1, + "#7-2 nsITableEditor.getFirstSelectedCellInTable() should return 1 to column number for the second column"); + + selection.removeAllRanges(); + let range = document.createRange(); + range.selectNode(document.getElementById("c2-2")); + selection.addRange(range); + range = document.createRange(); + range.selectNode(document.getElementById("c2-3")); + selection.addRange(range); + cell = SpecialPowers.unwrap(getTableEditor().getFirstSelectedCellInTable(rowWrapper, colWrapper)); + is(cell, document.getElementById("c2-2"), + "#2-2 nsITableEditor.getFirstSelectedCellInTable() should return the second cell element in the second row"); + is(rowWrapper.value, 1, + "#2-2 nsITableEditor.getFirstSelectedCellInTable() should return 1 to row number for the second row"); + is(colWrapper.value, 1, + "#2-2 nsITableEditor.getFirstSelectedCellInTable() should return 1 to column number for the second column"); + + selection.removeAllRanges(); + range = document.createRange(); + range.selectNode(document.getElementById("c3-4")); + selection.addRange(range); + range = document.createRange(); + range.selectNode(document.getElementById("c5-2")); + selection.addRange(range); + cell = SpecialPowers.unwrap(getTableEditor().getFirstSelectedCellInTable(rowWrapper, colWrapper)); + is(cell, document.getElementById("c3-4"), + "#3-4 nsITableEditor.getFirstSelectedCellInTable() should return the last cell element in the third row"); + is(rowWrapper.value, 2, + "#3-4 nsITableEditor.getFirstSelectedCellInTable() should return 2 to row number for the third row"); + is(colWrapper.value, 3, + "#3-4 nsITableEditor.getFirstSelectedCellInTable() should return 3 to column number for the forth column"); + + cell = document.getElementById("c6-4"); + selection.selectAllChildren(cell); + cell = SpecialPowers.unwrap(getTableEditor().getFirstSelectedCellInTable(rowWrapper, colWrapper)); + is(cell, null, + "nsITableEditor.getFirstSelectedCellInTable() should return null if neither <td> nor <th> element node is selected"); + is(rowWrapper.value, 0, + "#3-4 nsITableEditor.getFirstSelectedCellInTable() should return 0 to row number if neither <td> nor <th> element node is selected"); + is(colWrapper.value, 0, + "#3-4 nsITableEditor.getFirstSelectedCellInTable() should return 0 to column number if neither <td> nor <th> element node is selected"); + + cell = document.getElementById("c6-5"); + selection.setBaseAndExtent(cell.firstChild, 0, cell.firstChild, 0); + cell = SpecialPowers.unwrap(getTableEditor().getFirstSelectedCellInTable(rowWrapper, colWrapper)); + is(cell, null, + "nsITableEditor.getFirstSelectedCellInTable() should return null if a text node is selected"); + is(rowWrapper.value, 0, + "#3-4 nsITableEditor.getFirstSelectedCellInTable() should return 0 to row number if a text node is selected"); + is(colWrapper.value, 0, + "#3-4 nsITableEditor.getFirstSelectedCellInTable() should return 0 to column number if a text node is selected"); + + // XXX If cell is not selected, nsITableEditor.getFirstSelectedCellInTable() + // returns null without throwing exception, however, if there is no + // selection ranges, throwing an exception. This inconsistency is odd. + selection.removeAllRanges(); + try { + cell = SpecialPowers.unwrap(getTableEditor().getFirstSelectedCellInTable(rowWrapper, colWrapper)); + ok(false, "nsITableEditor.getFirstSelectedCellInTable() should throw an exception if there is no selection ranges"); + } catch (e) { + ok(true, "nsITableEditor.getFirstSelectedCellInTable() should throw an exception if there is no selection ranges"); + } + + tr = document.getElementById("r6"); + selection.setBaseAndExtent(tr, 0, tr, 1); + try { + cell = SpecialPowers.unwrap(getTableEditor().getFirstSelectedCellInTable()); + ok(false, "nsITableEditor.getFirstSelectedCellInTable() should throw an exception if it does not have argument"); + } catch (e) { + ok(true, "nsITableEditor.getFirstSelectedCellInTable() should throw an exception if it does not have argument"); + } + + tr = document.getElementById("r6"); + selection.setBaseAndExtent(tr, 0, tr, 1); + try { + cell = SpecialPowers.unwrap(getTableEditor().getFirstSelectedCellInTable(null)); + ok(false, "nsITableEditor.getFirstSelectedCellInTable() should throw an exception if its argument is only one null"); + } catch (e) { + ok(true, "nsITableEditor.getFirstSelectedCellInTable() should throw an exception if its argument is only one null"); + } + + tr = document.getElementById("r6"); + selection.setBaseAndExtent(tr, 0, tr, 1); + try { + cell = SpecialPowers.unwrap(getTableEditor().getFirstSelectedCellInTable(null, null)); + ok(false, "nsITableEditor.getFirstSelectedCellInTable() should throw an exception if its arguments are all null"); + } catch (e) { + ok(true, "nsITableEditor.getFirstSelectedCellInTable() should throw an exception if its arguments are all null"); + } + + tr = document.getElementById("r6"); + selection.setBaseAndExtent(tr, 0, tr, 1); + try { + cell = SpecialPowers.unwrap(getTableEditor().getFirstSelectedCellInTable(rowWrapper, null)); + ok(false, "nsITableEditor.getFirstSelectedCellInTable() should throw an exception if its column argument is null"); + } catch (e) { + ok(true, "nsITableEditor.getFirstSelectedCellInTable() should throw an exception if its column argument is null"); + } + + tr = document.getElementById("r6"); + selection.setBaseAndExtent(tr, 0, tr, 1); + try { + cell = SpecialPowers.unwrap(getTableEditor().getFirstSelectedCellInTable(null, colWrapper)); + ok(false, "nsITableEditor.getFirstSelectedCellInTable() should throw an exception if its row argument is null"); + } catch (e) { + ok(true, "nsITableEditor.getFirstSelectedCellInTable() should throw an exception if its row argument is null"); + } + + SimpleTest.finish(); +}); + +function getTableEditor() { + let editingSession = SpecialPowers.wrap(window).docShell.editingSession; + return editingSession.getEditorForWindow(window).QueryInterface(SpecialPowers.Ci.nsITableEditor); +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_nsITableEditor_getSelectedCells.html b/editor/libeditor/tests/test_nsITableEditor_getSelectedCells.html new file mode 100644 index 0000000000..cff75652df --- /dev/null +++ b/editor/libeditor/tests/test_nsITableEditor_getSelectedCells.html @@ -0,0 +1,295 @@ +<!DOCTYPE> +<html> +<head> + <title>Test for nsITableEditor.getSelectedCells()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> +<div id="display"> +</div> +<div id="content" contenteditable></div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> +"use strict"; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + let editor = document.getElementById("content"); + let selection = document.getSelection(); + + (function test_with_collapsed_selection() { + selection.collapse(editor, 0); + let cells = getTableEditor().getSelectedCells(); + is(cells.length, 0, + "nsITableEditor.getSelectedCells() should return empty array if Selection does not select cells"); + })(); + + editor.innerHTML = + '<table id="table">' + + '<tr id="r1"><td id="c1-1">cell1-1</td><td id="c1-2">cell1-2</td><td id="c1-3">cell1-3</td><td id="c1-4" colspan="2" rowspan="2">cell1-4</td></tr>' + + '<tr id="r2"><th id="c2-1" rowspan="2">cell2-1</th><td id="c2-2">cell2-2</td><td id="c2-3">cell2-3</td></tr>' + + '<tr id="r3"><td id="c3-2">cell3-2</td><td id="c3-3">cell3-3</td><td id="c3-4" colspan="2">cell3-4</td></tr>' + + '<tr id="r4"><td id="c4-1" rowspan="4">cell4-1</td><td id="c4-2">cell4-2</td><td id="c4-3">cell4-3</td><th id="c4-4">cell4-4</th><td id="c4-5">cell4-5</td></tr>' + + '<tr id="r5"><th id="c5-2">cell5-2</th><th id="c5-3" colspan="2">' + + '<table><tr id="r2-1"><td id="c2-1-1">cell2-1-1</td></tr></table>' + + '</th><td id="c5-5">cell5-5</td></tr>' + + '<tr id="r6"><td id="c6-2">cell6-2</td><td id="c6-3">cell6-3</td><td id="c6-4"><p>cell6-4</p></td><td id="c6-5">cell6-5</td></tr>' + + '<tr id="r7"><td id="c7-2" colspan="4">cell7-2</td></tr>' + + "</table>"; + + (function test_with_selecting_1_1() { + let tr = document.getElementById("r1"); + selection.setBaseAndExtent(tr, 0, tr, 1); + let cells = getTableEditor().getSelectedCells(); + is(cells.length, 1, + "#1-1 nsITableEditor.getSelectedCells() should return an array whose length is 1"); + is(SpecialPowers.unwrap(cells[0]), document.getElementById("c1-1"), + "#1-1 nsITableEditor.getSelectedCells() should set the first item of the result to the first cell element in the first row"); + })(); + + (function test_with_selecting_1_4() { + let tr = document.getElementById("r1"); + selection.setBaseAndExtent(tr, 3, tr, 4); + let cells = getTableEditor().getSelectedCells(); + is(cells.length, 1, + "#1-4 nsITableEditor.getSelectedCells() should return an array whose length is 1"); + is(SpecialPowers.unwrap(cells[0]), document.getElementById("c1-4"), + "#1-4 nsITableEditor.getSelectedCells() should set the first item of the result to the last cell element whose colspan and rowspan are 2 in the first row"); + })(); + + (function test_with_selecting_2_1() { + let tr = document.getElementById("r2"); + selection.setBaseAndExtent(tr, 0, tr, 1); + let cells = getTableEditor().getSelectedCells(); + is(cells.length, 1, + "#2-1 nsITableEditor.getSelectedCells() should return an array whose length is 1"); + is(SpecialPowers.unwrap(cells[0]), document.getElementById("c2-1"), + "#2-1 nsITableEditor.getSelectedCells() should set the first item of the result to the first cell element in the second row"); + })(); + + (function test_with_selecting_7_2() { + let tr = document.getElementById("r7"); + selection.setBaseAndExtent(tr, 0, tr, 1); + let cells = getTableEditor().getSelectedCells(); + is(cells.length, 1, + "#7-2 nsITableEditor.getSelectedCells() should return an array whose length is 1"); + is(SpecialPowers.unwrap(cells[0]), document.getElementById("c7-2"), + "#7-2 nsITableEditor.getSelectedCells() should set the first item of the result to the second cell element in the last row"); + })(); + + (function test_with_selecting_2_2_and_2_3() { + let tr = document.getElementById("r2"); + selection.removeAllRanges(); + let range = document.createRange(); + range.setStart(tr, 1); + range.setEnd(tr, 2); + selection.addRange(range); + range = document.createRange(); + range.setStart(tr, 2); + range.setEnd(tr, 3); + selection.addRange(range); + let cells = getTableEditor().getSelectedCells(); + is(cells.length, 2, + "#2-2 nsITableEditor.getSelectedCells() should return an array whose length is 2"); + is(SpecialPowers.unwrap(cells[0]), document.getElementById("c2-2"), + "#2-2 nsITableEditor.getSelectedCells() should set the first item of the result to the second cell element in the second row"); + })(); + + (function test_with_selecting_3_4_and_5_2() { + let tr = document.getElementById("r3"); + selection.removeAllRanges(); + let range = document.createRange(); + range.setStart(tr, 2); + range.setEnd(tr, 3); + selection.addRange(range); + range = document.createRange(); + range.setStart(document.getElementById("r5"), 0); + range.setEnd(document.getElementById("r5"), 1); + selection.addRange(range); + let cells = getTableEditor().getSelectedCells(); + is(cells.length, 2, + "#3-4, #5-2 nsITableEditor.getSelectedCells() should return an array whose length is 2"); + is(SpecialPowers.unwrap(cells[0]), document.getElementById("c3-4"), + "#3-4, #5-2 nsITableEditor.getSelectedCells() should set the first item of the result to the last cell element in the third row"); + is(SpecialPowers.unwrap(cells[1]), document.getElementById("c5-2"), + "#3-4, #5-2 nsITableEditor.getSelectedCells() should set the second item of the result to the first cell element in the fifth row"); + })(); + + (function test_with_selecting_1_2_and_1_3_and_1_4_and_2_1_and_2_2() { + selection.removeAllRanges(); + let tr = document.getElementById("r1"); + let range = document.createRange(); + range.setStart(tr, 1); + range.setEnd(tr, 2); + selection.addRange(range); + range = document.createRange(); + range.setStart(tr, 2); + range.setEnd(tr, 3); + selection.addRange(range); + range = document.createRange(); + range.setStart(tr, 3); + range.setEnd(tr, 4); + selection.addRange(range); + tr = document.getElementById("r2"); + range = document.createRange(); + range.setStart(tr, 0); + range.setEnd(tr, 1); + selection.addRange(range); + range = document.createRange(); + range.setStart(tr, 1); + range.setEnd(tr, 2); + selection.addRange(range); + + let cells = getTableEditor().getSelectedCells(); + is(cells.length, 5, + "#1-2, #1-3, #1-4, #2-1, #2-2 nsITableEditor.getSelectedCells() should return an array whose length is 5"); + is(SpecialPowers.unwrap(cells[0]), document.getElementById("c1-2"), + "#1-2, #1-3, #1-4, #2-1, #2-2 nsITableEditor.getSelectedCells() should set the first item of the result to the second cell element in the first row"); + is(SpecialPowers.unwrap(cells[1]), document.getElementById("c1-3"), + "#1-2, #1-3, #1-4, #2-1, #2-2 nsITableEditor.getSelectedCells() should set the second item of the result to the third cell element in the first row"); + is(SpecialPowers.unwrap(cells[2]), document.getElementById("c1-4"), + "#1-2, #1-3, #1-4, #2-1, #2-2 nsITableEditor.getSelectedCells() should set the third item of the result to the forth cell element in the first row"); + is(SpecialPowers.unwrap(cells[3]), document.getElementById("c2-1"), + "#1-2, #1-3, #1-4, #2-1, #2-2 nsITableEditor.getSelectedCells() should set the forth item of the result to the first cell element in the second row"); + is(SpecialPowers.unwrap(cells[4]), document.getElementById("c2-2"), + "#1-2, #1-3, #1-4, #2-1, #2-2 nsITableEditor.getSelectedCells() should set the forth item of the result to the second cell element in the second row"); + })(); + + (function test_with_selecting_6_3_and_paragraph_in_6_4_and_6_5() { + selection.removeAllRanges(); + let tr = document.getElementById("r6"); + let range = document.createRange(); + range.setStart(tr, 1); + range.setEnd(tr, 2); + selection.addRange(range); + range = document.createRange(); + range.setStart(document.getElementById("c6-4").firstChild, 0); + range.setEnd(document.getElementById("c6-4").firstChild, 1); + selection.addRange(range); + range = document.createRange(); + range.setStart(tr, 3); + range.setEnd(tr, 4); + selection.addRange(range); + + let cells = getTableEditor().getSelectedCells(); + is(cells.length, 2, + "#6-3, #6-5 nsITableEditor.getSelectedCells() should return an array whose length is 2"); + is(SpecialPowers.unwrap(cells[0]), document.getElementById("c6-3"), + "#6-3, #6-5 nsITableEditor.getSelectedCells() should set the first item of the result to the second cell element in the sixth row"); + // The <p> element in c6-4 is selected, this does not select the cell + // element so that it should be ignored. + is(SpecialPowers.unwrap(cells[1]), document.getElementById("c6-5"), + "#6-3, #6-5 nsITableEditor.getSelectedCells() should set the first item of the result to the forth cell element in the sixth row"); + })(); + + (function test_with_selecting_2_3_and_text_in_4_1_and_7_2() { + selection.removeAllRanges(); + let tr = document.getElementById("r2"); + let range = document.createRange(); + range.setStart(tr, 2); + range.setEnd(tr, 3); + selection.addRange(range); + range = document.createRange(); + range.setStart(document.getElementById("c4-1").firstChild, 0); + range.setEnd(document.getElementById("c4-1").firstChild, 7); + selection.addRange(range); + tr = document.getElementById("r7"); + range = document.createRange(); + range.setStart(tr, 0); + range.setEnd(tr, 1); + selection.addRange(range); + + let cells = getTableEditor().getSelectedCells(); + is(cells.length, 2, + "#2-3, #7-2 nsITableEditor.getSelectedCells() should return an array whose length is 2"); + is(SpecialPowers.unwrap(cells[0]), document.getElementById("c2-3"), + "#2-3, #7-2 nsITableEditor.getSelectedCells() should set the first item of the result to the third cell element in the second row"); + // Text in c4-1 is selected, this does not select the cell element so that + // it should be ignored. Note that we've ignored the following selected + // cell elements in old API, but it causes inconsistent behavior with the + // previous test case. Therefore, we take this behavior. + is(SpecialPowers.unwrap(cells[1]), document.getElementById("c7-2"), + "#2-3, #7-2 nsITableEditor.getSelectedCells() should set the second item of the result to the cell element in the seventh row"); + })(); + + (function test_with_selecting_3_2_and_2_1_1_and_7_2() { + selection.removeAllRanges(); + let tr = document.getElementById("r3"); + let range = document.createRange(); + range.setStart(tr, 0); + range.setEnd(tr, 1); + selection.addRange(range); + tr = document.getElementById("r2-1"); + range = document.createRange(); + range.setStart(tr, 0); + range.setEnd(tr, 1); + selection.addRange(range); + tr = document.getElementById("r7"); + range = document.createRange(); + range.setStart(tr, 0); + range.setEnd(tr, 1); + selection.addRange(range); + + let cells = getTableEditor().getSelectedCells(); + is(cells.length, 3, + "#3-2, #2-1-1, #7-2 nsITableEditor.getSelectedCells() should return an array whose length is 3"); + is(SpecialPowers.unwrap(cells[0]), document.getElementById("c3-2"), + "#3-2, #2-1-1, #7-2 nsITableEditor.getSelectedCells() should set the first item of the result to the first cell element in the third row"); + // c2-1-1 is in another <table>, however, getSelectedCells() returns it + // since it works only with ranges of Selection. + is(SpecialPowers.unwrap(cells[1]), document.getElementById("c2-1-1"), + "#3-2, #2-1-1, #7-2 nsITableEditor.getSelectedCells() should set the second item of the result to the cell element in the child <table> element"); + is(SpecialPowers.unwrap(cells[2]), document.getElementById("c7-2"), + "#3-2, #2-1-1, #7-2 nsITableEditor.getSelectedCells() should set the third item of the result to the cell element in the seventh row"); + })(); + + (function test_with_selecting_all_children_of_cell() { + selection.selectAllChildren(document.getElementById("c6-4")); + let cells = getTableEditor().getSelectedCells(); + is(cells.length, 0, + "nsITableEditor.getSelectedCells() should return an empty array when no cell element is selected"); + })(); + + (function test_with_selecting_text_in_cell() { + let cell = document.getElementById("c6-5"); + selection.collapse(cell.firstChild, 0); + let cells = getTableEditor().getSelectedCells(); + is(cells.length, 0, + "nsITableEditor.getSelectedCells() should return an empty array when selecting text in a cell element"); + })(); + + (function test_with_selecting_text_in_1_1_and_1_2() { + let cell = document.getElementById("c1-1"); + selection.setBaseAndExtent(cell.firstChild, 0, cell.firstChild, 3); + let range = document.createRange(); + range.setStart(cell.parentNode, 1); + range.setEnd(cell.parentNode, 2); + selection.addRange(range); + let cells = getTableEditor().getSelectedCells(); + is(cells.length, 0, + "nsITableEditor.getSelectedCells() should return an empty array when the first range does not select a cell element"); + })(); + + (function test_without_selection_ranges() { + selection.removeAllRanges(); + let cells = getTableEditor().getSelectedCells(); + is(cells.length, 0, + "nsITableEditor.getSelectedCells() should return an empty array even when there is no selection range"); + })(); + + SimpleTest.finish(); +}); + +function getTableEditor() { + var Ci = SpecialPowers.Ci; + var editingSession = SpecialPowers.wrap(window).docShell.editingSession; + return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsITableEditor); +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_nsITableEditor_getSelectedCellsType.html b/editor/libeditor/tests/test_nsITableEditor_getSelectedCellsType.html new file mode 100644 index 0000000000..33884b8e20 --- /dev/null +++ b/editor/libeditor/tests/test_nsITableEditor_getSelectedCellsType.html @@ -0,0 +1,251 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test for nsITableEditor.getSelectedCellsType()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> +<div id="display"> +</div> +<div id="content" contenteditable></div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> +"use strict"; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + let editor = document.getElementById("content"); + let selection = document.getSelection(); + + const kTableSelectionMode_None = 0; + const kTableSelectionMode_Cell = 1; + const kTableSelectionMode_Row = 2; + const kTableSelectionMode_Column = 3; + + (function test_without_selection_range() { + selection.removeAllRanges(); + try { + getTableEditor().getSelectedCellsType(null); + ok(false, "nsITableEditor.getSelectedCellsType(null) should cause throwing an exception when editor does not have Selection"); + } catch (e) { + ok(true, "nsITableEditor.getSelectedCellsType(null) should cause throwing an exception when editor does not have Selection"); + } + })(); + + (function test_without_table() { + editor.innerHTML = "<p>Here is a paragraph</p>"; + selection.collapse(editor.querySelector("p").firstChild, 0); + try { + getTableEditor().getSelectedCellsType(null); + ok(false, "nsITableEditor.getSelectedCellsType(null) should cause throwing an exception when editor does not have a <table>"); + } catch (e) { + ok(true, "nsITableEditor.getSelectedCellsType(null) should cause throwing an exception when editor does not have a <table>"); + } + })(); + + (function test_with_selection_outside_table() { + editor.innerHTML = "<p>Here is a paragraph before the table</p>" + + "<table><tr><td>cell</td></tr></table>"; + selection.collapse(editor.querySelector("p").firstChild, 0); + try { + getTableEditor().getSelectedCellsType(null); + ok(false, "nsITableEditor.getSelectedCellsType(null) should cause throwing an exception when selection is outside the <table>"); + } catch (e) { + ok(true, "nsITableEditor.getSelectedCellsType(null) should cause throwing an exception when selection is outside the <table>"); + } + })(); + + (function test_with_selection_in_text_in_cell() { + editor.innerHTML = "<table><tr><td>cell</td></tr></table>"; + selection.collapse(editor.querySelector("td").firstChild, 0); + is(getTableEditor().getSelectedCellsType(null), kTableSelectionMode_None, + "nsITableEditor.getSelectedCellsType(null) should return None when selection is collapsed in a text node in a cell"); + })(); + + (function test_with_selecting_a_cell_which_is_only_one_cell_in_table() { + editor.innerHTML = "<table><tr><td>cell</td></tr></table>"; + selection.setBaseAndExtent(editor.querySelector("tr"), 0, editor.querySelector("tr"), 1); + is(getTableEditor().getSelectedCellsType(null), kTableSelectionMode_Row, + "nsITableEditor.getSelectedCellsType(null) should return Row when selection selects the cell which is only one in the table"); + })(); + + (function test_with_selecting_a_cell_whose_table_has_one_row() { + editor.innerHTML = "<table><tr><td>cell1</td><td>cell2</td></tr></table>"; + selection.setBaseAndExtent(editor.querySelector("tr"), 0, editor.querySelector("tr"), 1); + is(getTableEditor().getSelectedCellsType(null), kTableSelectionMode_Column, + "nsITableEditor.getSelectedCellsType(null) should return Column when selection selects a cell in the first row which is only one in the table"); + })(); + + (function test_with_selecting_a_cell_whose_table_has_one_column() { + editor.innerHTML = "<table><tr><td>cell1</td></tr><tr><td>cell2</td></tr></table>"; + selection.setBaseAndExtent(editor.querySelector("tr"), 0, editor.querySelector("tr"), 1); + is(getTableEditor().getSelectedCellsType(null), kTableSelectionMode_Row, + "nsITableEditor.getSelectedCellsType(null) should return Row when selection selects a cell in the first row which is only one in the row"); + })(); + + (function test_with_selecting_a_cell_at_1_1_whose_table_has_2x2_cells() { + editor.innerHTML = "<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>"; + selection.setBaseAndExtent(editor.querySelector("tr"), 0, editor.querySelector("tr"), 1); + is(getTableEditor().getSelectedCellsType(null), kTableSelectionMode_Cell, + "nsITableEditor.getSelectedCellsType(null) should return Cell when selection selects a cell at first row and first column"); + })(); + + (function test_with_selecting_a_cell_at_1_2_whose_table_has_2x2_cells() { + editor.innerHTML = "<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>"; + selection.setBaseAndExtent(editor.querySelector("tr"), 1, editor.querySelector("tr"), 2); + is(getTableEditor().getSelectedCellsType(null), kTableSelectionMode_Cell, + "nsITableEditor.getSelectedCellsType(null) should return Cell when selection selects a cell at first row and second column"); + })(); + + (function test_with_selecting_a_cell_at_2_1_whose_table_has_2x2_cells() { + editor.innerHTML = "<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>"; + selection.setBaseAndExtent(editor.querySelector("tr + tr"), 0, editor.querySelector("tr + tr"), 1); + is(getTableEditor().getSelectedCellsType(null), kTableSelectionMode_Cell, + "nsITableEditor.getSelectedCellsType(null) should return Cell when selection selects a cell at second row and first column"); + })(); + + (function test_with_selecting_a_cell_at_2_2_whose_table_has_2x2_cells() { + editor.innerHTML = "<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>"; + selection.setBaseAndExtent(editor.querySelector("tr + tr"), 1, editor.querySelector("tr + tr"), 2); + is(getTableEditor().getSelectedCellsType(null), kTableSelectionMode_Cell, + "nsITableEditor.getSelectedCellsType(null) should return Cell when selection selects a cell at second row and second column"); + })(); + + (function test_with_selecting_a_cell_at_1_1_whose_colspan_2_and_whose_table_has_2x2_cells() { + editor.innerHTML = "<table><tr><td colspan=\"2\">cell1</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>"; + selection.setBaseAndExtent(editor.querySelector("tr"), 0, editor.querySelector("tr"), 1); + is(getTableEditor().getSelectedCellsType(null), kTableSelectionMode_Row, + "nsITableEditor.getSelectedCellsType(null) should return Row when selection selects a cell whose colspan is 2 at first row"); + })(); + + (function test_with_selecting_a_cell_at_1_1_whose_rowspan_2_and_whose_table_has_2x2_cells() { + editor.innerHTML = "<table><tr><td rowspan=\"2\">cell1</td><td>cell2</td></tr><tr><td>cell4</td></tr></table>"; + selection.setBaseAndExtent(editor.querySelector("tr"), 0, editor.querySelector("tr"), 1); + is(getTableEditor().getSelectedCellsType(null), kTableSelectionMode_Column, + "nsITableEditor.getSelectedCellsType(null) should return Column when selection selects a cell whose row is 2 at first column"); + })(); + + (function test_with_selecting_all_cells_in_first_row_whose_table_has_2x2_cells() { + editor.innerHTML = "<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>"; + synthesizeMouseAtCenter(editor.querySelector("td"), {accelKey: true}); + synthesizeMouseAtCenter(editor.querySelector("td + td"), {accelKey: true}); + is(getTableEditor().getSelectedCellsType(null), kTableSelectionMode_Row, + "nsITableEditor.getSelectedCellsType(null) should return Row when selection selects all cells in the first row whose table has 2 rows and 2 columns"); + })(); + + (function test_with_selecting_all_cells_in_first_column_whose_table_has_2x2_cells() { + selection.removeAllRanges(); + editor.innerHTML = "<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>"; + synthesizeMouseAtCenter(editor.querySelector("td"), {accelKey: true}); + synthesizeMouseAtCenter(editor.querySelector("tr + tr > td"), {accelKey: true}); + is(getTableEditor().getSelectedCellsType(null), kTableSelectionMode_Column, + "nsITableEditor.getSelectedCellsType(null) should return Row when selection selects all cells in the first column whose table has 2 rows and 2 columns"); + })(); + + (function test_with_selecting_all_cells_whose_table_has_2x2_cells() { + selection.removeAllRanges(); + editor.innerHTML = "<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>"; + synthesizeMouseAtCenter(editor.querySelector("td"), {accelKey: true}); + synthesizeMouseAtCenter(editor.querySelector("td + td"), {accelKey: true}); + synthesizeMouseAtCenter(editor.querySelector("tr + tr > td"), {accelKey: true}); + synthesizeMouseAtCenter(editor.querySelector("tr + tr > td + td"), {accelKey: true}); + is(getTableEditor().getSelectedCellsType(null), kTableSelectionMode_Row, + "nsITableEditor.getSelectedCellsType(null) should return Row when selection selects all cells whose table has 2 rows and 2 columns"); + })(); + + (function test_with_selecting_a_raw() { + editor.innerHTML = "<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>"; + selection.setBaseAndExtent(editor.querySelector("tbody"), 0, editor.querySelector("tbody"), 1); + is(getTableEditor().getSelectedCellsType(null), kTableSelectionMode_None, + "nsITableEditor.getSelectedCellsType(null) should return None when selection selects a row"); + })(); + + (function test_with_selecting_a_tbody() { + editor.innerHTML = "<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>"; + selection.setBaseAndExtent(editor.querySelector("table"), 0, editor.querySelector("table"), 1); + is(getTableEditor().getSelectedCellsType(null), kTableSelectionMode_None, + "nsITableEditor.getSelectedCellsType(null) should return None when selection selects a tbody"); + })(); + + (function test_with_selecting_a_table() { + editor.innerHTML = "<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>"; + selection.setBaseAndExtent(editor, 0, editor, 1); + is(getTableEditor().getSelectedCellsType(null), kTableSelectionMode_None, + "nsITableEditor.getSelectedCellsType(null) should return None when selection selects a table"); + })(); + + (function test_with_selecting_cells_in_different_table() { + selection.removeAllRanges(); + editor.innerHTML = "<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>" + + "<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>"; + let range = document.createRange(); + range.selectNode(editor.querySelector("td")); + selection.addRange(range); + range = document.createRange(); + range.selectNode(editor.querySelector("table + table td")); + selection.addRange(range); + is(getTableEditor().getSelectedCellsType(null), kTableSelectionMode_Cell, + "nsITableEditor.getSelectedCellsType(null) should return Cell when selection selects 2 cells in different tables"); + })(); + + (function test_with_selecting_a_cell_in_the_table() { + selection.removeAllRanges(); + editor.innerHTML = "<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>" + + "<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>"; + let range = document.createRange(); + range.selectNode(editor.querySelector("td")); + selection.addRange(range); + is(getTableEditor().getSelectedCellsType(editor.querySelector("table")), kTableSelectionMode_Cell, + "nsITableEditor.getSelectedCellsType(editor.querySelector(\"table\")) should return Cell when selection selects a cell in the table"); + })(); + + (function test_with_selecting_a_cell_in_the_table_specifying_different_cell() { + selection.removeAllRanges(); + editor.innerHTML = "<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>" + + "<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>"; + let range = document.createRange(); + range.selectNode(editor.querySelector("tr + tr > td")); + selection.addRange(range); + is(getTableEditor().getSelectedCellsType(editor.querySelector("td")), kTableSelectionMode_Cell, + "nsITableEditor.getSelectedCellsType(editor.querySelector(\"td\")) should return Cell when selection selects a cell in the table"); + })(); + + (function test_with_selecting_a_cell_in_the_other_table() { + selection.removeAllRanges(); + editor.innerHTML = "<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>" + + "<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>"; + let range = document.createRange(); + range.selectNode(editor.querySelector("td")); + selection.addRange(range); + todo_is(getTableEditor().getSelectedCellsType(editor.querySelector("table + table")), kTableSelectionMode_None, + "nsITableEditor.getSelectedCellsType(editor.querySelector(\"table + table\")) should return None when selection selects a cell in the other table"); + })(); + + (function test_with_selecting_a_cell_in_the_other_table_specifying_a_cell_in_the_other_one() { + selection.removeAllRanges(); + editor.innerHTML = "<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>" + + "<table><tr><td>cell1</td><td>cell2</td></tr><tr><td>cell3</td><td>cell4</td></tr></table>"; + let range = document.createRange(); + range.selectNode(editor.querySelector("td")); + selection.addRange(range); + todo_is(getTableEditor().getSelectedCellsType(editor.querySelector("table + table td")), kTableSelectionMode_None, + "nsITableEditor.getSelectedCellsType(editor.querySelector(\"table + table td\")) should return None when selection selects a cell in the other table"); + })(); + + SimpleTest.finish(); +}); + +function getTableEditor() { + var Ci = SpecialPowers.Ci; + var editingSession = SpecialPowers.wrap(window).docShell.editingSession; + return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsITableEditor); +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_nsITableEditor_getSelectedOrParentTableElement.html b/editor/libeditor/tests/test_nsITableEditor_getSelectedOrParentTableElement.html new file mode 100644 index 0000000000..665359545c --- /dev/null +++ b/editor/libeditor/tests/test_nsITableEditor_getSelectedOrParentTableElement.html @@ -0,0 +1,283 @@ +<!DOCTYPE> +<html> +<head> + <title>Test for nsITableEditor.getSelectedOrParentTableElement()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> +<div id="display"> +</div> +<div id="content" contenteditable></div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + let editor = document.getElementById("content"); + let selection = document.getSelection(); + + selection.collapse(editor, 0); + let tagNameWrapper = {}; + let countWrapper = {}; + let cell = SpecialPowers.unwrap(getTableEditor().getSelectedOrParentTableElement(tagNameWrapper, countWrapper)); + is(cell, null, + "nsITableEditor.getSelectedOrParentTableElement() should return null if Selection does not select cells nor in <table>"); + is(tagNameWrapper.value, "", + "nsITableEditor.getSelectedOrParentTableElement() should return empty string to tag name if Selection does not select cells nor in <table>"); + is(countWrapper.value, 0, + "nsITableEditor.getSelectedOrParentTableElement() should return 0 to count if Selection does not select cells nor in <table>"); + + editor.innerHTML = + '<table id="table">' + + '<tr id="r1"><td id="c1-1">cell1-1</td><td id="c1-2">cell1-2</td><td id="c1-3">cell1-3</td><td id="c1-4" colspan="2" rowspan="2">cell1-4</td></tr>' + + '<tr id="r2"><th id="c2-1" rowspan="2">cell2-1</th><th id="c2-2">cell2-2</th><td id="c2-3">cell2-3</td></tr>' + + '<tr id="r3"><td id="c3-2">cell3-2</td><td id="c3-3">cell3-3</td><td id="c3-4" colspan="2">cell3-4</td></tr>' + + '<tr id="r4"><td id="c4-1" rowspan="4">cell4-1</td><td id="c4-2">cell4-2</td><td id="c4-3">' + + '<table id="table2">' + + '<tr id="r2-1"><th id="c2-1-1">cell2-1-1-</td><td id="c2-1-2">cell2-1-2</td></tr>' + + '<tr id="r2-2"><td id="c2-2-1">cell2-2-1-</td><th id="c2-2-2">cell2-2-2</th></tr>' + + "</table>" + + '</td><th id="c4-4">cell4-4</th><td id="c4-5">cell4-5</td></tr>' + + '<tr id="r5"><th id="c5-2">cell5-2</th><th id="c5-3" colspan="2">cell5-3</th><td id="c5-5">cell5-5</td></tr>' + + '<tr id="r6"><td id="c6-2">cell6-2</td><td id="c6-3">cell6-3</td><td id="c6-4"><p>cell6-4</p></td><td id="c6-5">cell6-5</td></tr>' + + '<tr id="r7"><td id="c7-2" colspan="4">cell7-2</td></tr>' + + "</table>"; + + let tr = document.getElementById("r1"); + selection.setBaseAndExtent(tr, 0, tr, 1); + cell = SpecialPowers.unwrap(getTableEditor().getSelectedOrParentTableElement(tagNameWrapper, countWrapper)); + is(cell, document.getElementById("c1-1"), + "#1-1 nsITableEditor.getSelectedOrParentTableElement() should return the first cell element in the first row"); + is(tagNameWrapper.value, "td", + "#1-1 nsITableEditor.getSelectedOrParentTableElement() should return 'td' to tag name when a cell is selected"); + is(countWrapper.value, 1, + "#1-1 nsITableEditor.getSelectedOrParentTableElement() should return 1 to count when a cell is selected"); + + tr = document.getElementById("r1"); + selection.setBaseAndExtent(tr, 3, tr, 4); + cell = SpecialPowers.unwrap(getTableEditor().getSelectedOrParentTableElement(tagNameWrapper, countWrapper)); + is(cell, document.getElementById("c1-4"), + "#1-4 nsITableEditor.getSelectedOrParentTableElement() should return the last cell element whose colspan and rowspan are 2 in the first row"); + is(tagNameWrapper.value, "td", + "#1-4 nsITableEditor.getSelectedOrParentTableElement() should return 'td' to tag name when a cell is selected"); + is(countWrapper.value, 1, + "#1-4 nsITableEditor.getSelectedOrParentTableElement() should return 1 to count when a cell is selected"); + + tr = document.getElementById("r2"); + selection.setBaseAndExtent(tr, 0, tr, 1); + cell = SpecialPowers.unwrap(getTableEditor().getSelectedOrParentTableElement(tagNameWrapper, countWrapper)); + is(cell, document.getElementById("c2-1"), + "#2-1 nsITableEditor.getSelectedOrParentTableElement() should return the first cell element in the second row"); + is(tagNameWrapper.value, "td", + "#2-1 nsITableEditor.getSelectedOrParentTableElement() should return 'td' to tag name when a cell is selected but even if the cell is <th>"); + is(countWrapper.value, 1, + "#2-1 nsITableEditor.getSelectedOrParentTableElement() should return 1 to count when a cell is selected"); + + tr = document.getElementById("r7"); + selection.setBaseAndExtent(tr, 0, tr, 1); + cell = SpecialPowers.unwrap(getTableEditor().getSelectedOrParentTableElement(tagNameWrapper, countWrapper)); + is(cell, document.getElementById("c7-2"), + "#7-2 nsITableEditor.getSelectedOrParentTableElement() should return the second cell element in the last row"); + is(tagNameWrapper.value, "td", + "#7-2 nsITableEditor.getSelectedOrParentTableElement() should return 'td' to tag name when a cell is selected"); + is(countWrapper.value, 1, + "#7-2 nsITableEditor.getSelectedOrParentTableElement() should return 1 to count when a cell is selected"); + + selection.removeAllRanges(); + let range = document.createRange(); + range.selectNode(document.getElementById("c2-2")); + selection.addRange(range); + range = document.createRange(); + range.selectNode(document.getElementById("c2-3")); + selection.addRange(range); + cell = SpecialPowers.unwrap(getTableEditor().getSelectedOrParentTableElement(tagNameWrapper, countWrapper)); + is(cell, document.getElementById("c2-2"), + "#2-2 nsITableEditor.getSelectedOrParentTableElement() should return the second cell element in the second row"); + is(tagNameWrapper.value, "td", + "#2-2 nsITableEditor.getSelectedOrParentTableElement() should return 'td' to tag name when first range selects a cell"); + is(countWrapper.value, 2, + "#2-2 nsITableEditor.getSelectedOrParentTableElement() should return 2 to count when there are 2 selection ranges"); + + selection.removeAllRanges(); + range = document.createRange(); + range.selectNode(document.getElementById("c3-4")); + selection.addRange(range); + range = document.createRange(); + range.selectNode(document.getElementById("c5-2")); + selection.addRange(range); + cell = SpecialPowers.unwrap(getTableEditor().getSelectedOrParentTableElement(tagNameWrapper, countWrapper)); + is(cell, document.getElementById("c3-4"), + "#3-4 nsITableEditor.getSelectedOrParentTableElement() should return the last cell element in the third row"); + is(tagNameWrapper.value, "td", + "#3-4 nsITableEditor.getSelectedOrParentTableElement() should return 'td' to tag name when first range selects a cell"); + is(countWrapper.value, 2, + "#3-4 nsITableEditor.getSelectedOrParentTableElement() should return 2 to count when there are 2 selection ranges"); + + cell = document.getElementById("c2-2"); + selection.removeAllRanges(); + range = document.createRange(); + range.setStart(cell.firstChild, 0); + selection.addRange(range); + cell = document.getElementById("c2-1-1"); + range = document.createRange(); + range.setStart(cell.firstChild, 1); + range.setEnd(cell.firstChild, 2); + selection.addRange(range); + cell = SpecialPowers.unwrap(getTableEditor().getSelectedOrParentTableElement(tagNameWrapper, countWrapper)); + is(cell, document.getElementById("c2-1-1"), + "#2-1-1 nsITableEditor.getSelectedOrParentTableElement() should return the cell which contains the last selection range if first selection range does not select a cell"); + is(tagNameWrapper.value, "td", + "#2-1-1 nsITableEditor.getSelectedOrParentTableElement() should return 'td' to tag name when the first range does not select a cell and the last range is in a cell"); + is(countWrapper.value, 0, + "#2-1-1 nsITableEditor.getSelectedOrParentTableElement() should return 0 to count when the first range does not select a cell"); + + tr = document.getElementById("r2-2"); + selection.setBaseAndExtent(tr, 0, tr, 1); + cell = SpecialPowers.unwrap(getTableEditor().getSelectedOrParentTableElement(tagNameWrapper, countWrapper)); + is(cell, document.getElementById("c2-2-1"), + "#2-2-1 nsITableEditor.getSelectedOrParentTableElement() should return the first cell element in the first row of nested <table>"); + is(tagNameWrapper.value, "td", + "#2-2-1 nsITableEditor.getSelectedOrParentTableElement() should return 'td' to tag name when a cell in nested <table> is selected"); + is(countWrapper.value, 1, + "#2-2-1 nsITableEditor.getSelectedOrParentTableElement() should return 1 to count when a cell in nested <table> is selected"); + + cell = document.getElementById("c2-1-2"); + selection.setBaseAndExtent(cell.firstChild, 0, cell.firstChild, 0); + cell = SpecialPowers.unwrap(getTableEditor().getSelectedOrParentTableElement(tagNameWrapper, countWrapper)); + is(cell, document.getElementById("c2-1-2"), + "#2-1-2 nsITableEditor.getSelectedOrParentTableElement() should return the first cell element in the first row of nested <table>"); + is(tagNameWrapper.value, "td", + "#2-1-2 nsITableEditor.getSelectedOrParentTableElement() should return 'td' to tag name when a cell in nested <table> contains the first selection range"); + is(countWrapper.value, 0, + "#2-1-2 nsITableEditor.getSelectedOrParentTableElement() should return 0 to count when a cell in nested <table> contains the first selection range"); + + let table = document.getElementById("table2"); + selection.removeAllRanges(); + range = document.createRange(); + range.selectNode(table); + selection.addRange(range); + table = SpecialPowers.unwrap(getTableEditor().getSelectedOrParentTableElement(tagNameWrapper, countWrapper)); + is(table, document.getElementById("table2"), + "nsITableEditor.getSelectedOrParentTableElement() should return a <table> element which is selected"); + is(tagNameWrapper.value, "table", + "nsITableEditor.getSelectedOrParentTableElement() should return 'table' to tag name when a <table> is selected"); + is(countWrapper.value, 1, + "nsITableEditor.getSelectedOrParentTableElement() should return 1 to count when a <table> is selected"); + + selection.removeAllRanges(); + range = document.createRange(); + range.selectNode(document.getElementById("r2-1")); + selection.addRange(range); + range = document.createRange(); + range.selectNode(document.getElementById("r2-2")); + selection.addRange(range); + table = SpecialPowers.unwrap(getTableEditor().getSelectedOrParentTableElement(tagNameWrapper, countWrapper)); + is(table, document.getElementById("r2-2"), + "nsITableEditor.getSelectedOrParentTableElement() should return a <tr> element which is selected by the last selection range"); + is(tagNameWrapper.value, "tr", + "nsITableEditor.getSelectedOrParentTableElement() should return 'tr' to tag name when a <tr> is selected"); + is(countWrapper.value, 1, + "nsITableEditor.getSelectedOrParentTableElement() should return 1 to count when a <tr> is selected"); + + selection.removeAllRanges(); + range = document.createRange(); + range.selectNode(document.getElementById("r1")); + selection.addRange(range); + range = document.createRange(); + range.selectNode(document.getElementById("c5-5")); + selection.addRange(range); + cell = SpecialPowers.unwrap(getTableEditor().getSelectedOrParentTableElement(tagNameWrapper, countWrapper)); + is(cell, document.getElementById("c5-5"), + "#5-5 nsITableEditor.getSelectedOrParentTableElement() should return the cell selected by the last range when first range selects <tr>"); + is(tagNameWrapper.value, "td", + "#5-5 nsITableEditor.getSelectedOrParentTableElement() should return 'td' to tag name if the last range selects the cell when first range selects <tr>"); + is(countWrapper.value, 2, + "#5-5 nsITableEditor.getSelectedOrParentTableElement() should return 2 to count if the last range selects <td> when first range selects <tr>"); + + selection.removeAllRanges(); + range = document.createRange(); + range.selectNode(document.getElementById("r1")); + selection.addRange(range); + range = document.createRange(); + range.selectNode(document.getElementById("c5-2")); + selection.addRange(range); + cell = SpecialPowers.unwrap(getTableEditor().getSelectedOrParentTableElement(tagNameWrapper, countWrapper)); + is(cell, null, + "#5-5 nsITableEditor.getSelectedOrParentTableElement() should return null if the last range selects <th> when first range selects <tr>"); + is(tagNameWrapper.value, "", + "#5-5 nsITableEditor.getSelectedOrParentTableElement() should return empty string to tag name if the last range selects <th> when first range selects <tr>"); + is(countWrapper.value, 0, + "#5-5 nsITableEditor.getSelectedOrParentTableElement() should return 0 to count if the last range selects <th> when first range selects <tr>"); + + // XXX If cell is not selected, nsITableEditor.getSelectedOrParentTableElement() + // returns null without throwing exception, however, if there is no + // selection ranges, throwing an exception. This inconsistency is odd. + selection.removeAllRanges(); + try { + cell = SpecialPowers.unwrap(getTableEditor().getSelectedOrParentTableElement(tagNameWrapper, countWrapper)); + ok(false, "nsITableEditor.getSelectedOrParentTableElement() should throw an exception if there is no selection ranges"); + } catch (e) { + ok(true, "nsITableEditor.getSelectedOrParentTableElement() should throw an exception if there is no selection ranges"); + } + + tr = document.getElementById("r6"); + selection.setBaseAndExtent(tr, 0, tr, 1); + try { + cell = SpecialPowers.unwrap(getTableEditor().getSelectedOrParentTableElement()); + ok(false, "nsITableEditor.getSelectedOrParentTableElement() should throw an exception if it does not have argument"); + } catch (e) { + ok(true, "nsITableEditor.getSelectedOrParentTableElement() should throw an exception if it does not have argument"); + } + + tr = document.getElementById("r6"); + selection.setBaseAndExtent(tr, 0, tr, 1); + try { + cell = SpecialPowers.unwrap(getTableEditor().getSelectedOrParentTableElement(null)); + ok(false, "nsITableEditor.getSelectedOrParentTableElement() should throw an exception if its argument is only one null"); + } catch (e) { + ok(true, "nsITableEditor.getSelectedOrParentTableElement() should throw an exception if its argument is only one null"); + } + + tr = document.getElementById("r6"); + selection.setBaseAndExtent(tr, 0, tr, 1); + try { + cell = SpecialPowers.unwrap(getTableEditor().getSelectedOrParentTableElement(null, null)); + ok(false, "nsITableEditor.getSelectedOrParentTableElement() should throw an exception if its arguments are all null"); + } catch (e) { + ok(true, "nsITableEditor.getSelectedOrParentTableElement() should throw an exception if its arguments are all null"); + } + + tr = document.getElementById("r6"); + selection.setBaseAndExtent(tr, 0, tr, 1); + try { + cell = SpecialPowers.unwrap(getTableEditor().getSelectedOrParentTableElement(tagNameWrapper, null)); + ok(false, "nsITableEditor.getSelectedOrParentTableElement() should throw an exception if its count argument is null"); + } catch (e) { + ok(true, "nsITableEditor.getSelectedOrParentTableElement() should throw an exception if its count argument is null"); + } + + tr = document.getElementById("r6"); + selection.setBaseAndExtent(tr, 0, tr, 1); + try { + cell = SpecialPowers.unwrap(getTableEditor().getSelectedOrParentTableElement(null, countWrapper)); + ok(false, "nsITableEditor.getSelectedOrParentTableElement() should throw an exception if its tag name argument is null"); + } catch (e) { + ok(true, "nsITableEditor.getSelectedOrParentTableElement() should throw an exception if its tag name argument is null"); + } + + SimpleTest.finish(); +}); + +function getTableEditor() { + var Ci = SpecialPowers.Ci; + var editingSession = SpecialPowers.wrap(window).docShell.editingSession; + return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsITableEditor); +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_nsITableEditor_getTableSize.html b/editor/libeditor/tests/test_nsITableEditor_getTableSize.html new file mode 100644 index 0000000000..986d8e3e49 --- /dev/null +++ b/editor/libeditor/tests/test_nsITableEditor_getTableSize.html @@ -0,0 +1,94 @@ +<!DOCTYPE> +<html> +<head> + <title>Test for nsITableEditor.getTableSize()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> +<div id="display"> +</div> +<div id="content" contenteditable></div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + let editor = document.getElementById("content"); + let selection = document.getSelection(); + let rowCount = {}, columnCount = {}; + + try { + getTableEditor().getTableSize(undefined, rowCount, columnCount); + ok(false, "nsITableEditor.getTableSize(undefined) should cause throwing an exception"); + } catch (e) { + ok(true, "nsITableEditor.getTableSize(undefined) should cause throwing an exception"); + } + + try { + getTableEditor().getTableSize(null, rowCount, columnCount); + ok(false, "nsITableEditor.getTableSize(null) should cause throwing an exception"); + } catch (e) { + ok(true, "nsITableEditor.getTableSize(null) should cause throwing an exception"); + } + + try { + getTableEditor().getTableSize(editor, rowCount, columnCount); + ok(false, "nsITableEditor.getTableSize() should cause throwing an exception if given node is not in a <table>"); + } catch (e) { + ok(true, "nsITableEditor.getTableSize() should cause throwing an exception if given node is not in a <table>"); + } + + // Set id to "test" for the argument for getTableSize(). + // Set data-rows and data-cols to expected count of them. + const kTests = [ + '<table><tr><td id="test" data-rows="2" data-cols="3">cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr></table>', + '<table><tr id="test" data-rows="2" data-cols="3"><td>cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr></table>', + '<table id="test" data-rows="2" data-cols="3"><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr></table>', + '<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr><tr><td>cell2-1</td><td>cell2-2</td><td><p id="test" data-rows="2" data-cols="3">cell2-3</p></td></tr></table>', + '<table><caption id="test" data-rows="2" data-cols="3">caption</caption><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr></table>', + '<table id="test" data-rows="0" data-cols="0"></table>', + '<table id="test" data-rows="0" data-cols="0"><caption>caption</caption></table>', + '<table id="test" data-rows="1" data-cols="1"><td>cell1-1</td></table>', + // rowspan does not affect, but colspan affects... + '<table id="test" data-rows="1" data-cols="12"><tr><td rowspan="8" colspan="12">cell1-1</td></tr></table>', + '<table id="test" data-rows="1" data-cols="1"><tr><td><table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table></td></tr></table>', + '<table><tr><td id="test" data-rows="1" data-cols="1"><table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table></td></tr></table>', + '<table><tr><td><table id="test" data-rows="3" data-cols="2"><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table></td></tr></table>', + '<table><tr><td><table><tr><td id="test" data-rows="3" data-cols="2">cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td>cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table></td></tr></table>', + '<table><tr><td><table><tr><td>cell1-1</td><td>cell1-2</td></tr><tr><td>cell2-1</td><td><p id="test" data-rows="3" data-cols="2">cell2-2</p></td></tr><tr><td>cell3-1</td><td>cell3-2</td></tr></table></td></tr></table>', + ]; + + for (const kTest of kTests) { + editor.innerHTML = kTest; + let element = document.getElementById("test"); + getTableEditor().getTableSize(element, rowCount, columnCount); + is(rowCount.value.toString(10), element.getAttribute("data-rows"), + `Specified an element in a <table> directly, its parent table row count should be retrieved: ${kTest}`); + is(columnCount.value.toString(10), element.getAttribute("data-cols"), + `Specified an element in a <table> directly, its parent table column count should be retrieved: ${kTest}`); + if (element.firstChild && element.firstChild.nodeType == Node.TEXT_NODE) { + selection.collapse(element.firstChild, 0); + getTableEditor().getTableSize(null, rowCount, columnCount); + is(rowCount.value.toString(10), element.getAttribute("data-rows"), + `Selection is collapsed in a cell element, its parent table row count should be retrieved: ${kTest}`); + is(columnCount.value.toString(10), element.getAttribute("data-cols"), + `Selection is collapsed in a cell element, its parent table column count should be retrieved: ${kTest}`); + } + } + + SimpleTest.finish(); +}); + +function getTableEditor() { + var Ci = SpecialPowers.Ci; + var editingSession = SpecialPowers.wrap(window).docShell.editingSession; + return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsITableEditor); +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_nsITableEditor_insertTableCell.html b/editor/libeditor/tests/test_nsITableEditor_insertTableCell.html new file mode 100644 index 0000000000..ccd8ed5c7e --- /dev/null +++ b/editor/libeditor/tests/test_nsITableEditor_insertTableCell.html @@ -0,0 +1,558 @@ +<!DOCTYPE> +<html> +<head> + <title>Test for nsITableEditor.insertTableCell()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> +<div id="display"> +</div> +<div id="content" contenteditable>out of table<table><tr><td>default content</td></tr></table></div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> +"use strict"; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + let editor = document.getElementById("content"); + let selection = document.getSelection(); + let selectionRanges = []; + + function checkInputEvent(aEvent, aDescription) { + ok(aEvent instanceof InputEvent, + `"${aEvent.type}" event should be dispatched with InputEvent interface ${aDescription}`); + is(aEvent.cancelable, false, + `"${aEvent.type}" event should be never cancelable ${aDescription}`); + is(aEvent.bubbles, true, + `"${aEvent.type}" event should always bubble ${aDescription}`); + is(aEvent.inputType, "", + `inputType of "${aEvent.type}" event should be empty string ${aDescription}`); + is(aEvent.data, null, + `data of "${aEvent.type}" event should be null ${aDescription}`); + is(aEvent.dataTransfer, null, + `dataTransfer of "${aEvent.type}" event should be null ${aDescription}`); + let targetRanges = aEvent.getTargetRanges(); + if (aEvent.type === "beforeinput") { + is(targetRanges.length, selectionRanges.length, + `getTargetRanges() of "beforeinput" event should return selection ranges ${aDescription}`); + if (targetRanges.length === selectionRanges.length) { + for (let i = 0; i < selectionRanges.length; i++) { + is(targetRanges[i].startContainer, selectionRanges[i].startContainer, + `startContainer of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`); + is(targetRanges[i].startOffset, selectionRanges[i].startOffset, + `startOffset of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`); + is(targetRanges[i].endContainer, selectionRanges[i].endContainer, + `endContainer of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`); + is(targetRanges[i].endOffset, selectionRanges[i].endOffset, + `endOffset of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`); + } + } + } else { + is(targetRanges.length, 0, + `getTargetRanges() of "${aEvent.type}" event should return empty array ${aDescription}`); + } + } + + let beforeInputEvents = []; + let inputEvents = []; + function onBeforeInput(aEvent) { + beforeInputEvents.push(aEvent); + selectionRanges = []; + for (let i = 0; i < selection.rangeCount; i++) { + let range = selection.getRangeAt(i); + selectionRanges.push({startContainer: range.startContainer, startOffset: range.startOffset, + endContainer: range.endContainer, endOffset: range.endOffset}); + } + } + function onInput(aEvent) { + inputEvents.push(aEvent); + } + editor.addEventListener("beforeinput", onBeforeInput); + editor.addEventListener("input", onInput); + + beforeInputEvents = []; + inputEvents = []; + selection.collapse(editor.firstChild, 0); + getTableEditor().insertTableCell(1, false); + is(editor.innerHTML, "out of table<table><tbody><tr><td>default content</td></tr></tbody></table>", + "nsITableEditor.insertTableCell(1, false) should do nothing if selection is not in <table>"); + is(beforeInputEvents.length, 1, + '"beforeinput" event should be fired when a call of nsITableEditor.insertTableCell(1, false) even though it will do nothing'); + checkInputEvent(beforeInputEvents[0], "when selection collapsed outside table element (nsITableEditor.insertTableCell(1, false))"); + is(inputEvents.length, 0, + 'No "input" event should be fired when a call of nsITableEditor.insertTableCell(1, false) does nothing'); + + beforeInputEvents = []; + inputEvents = []; + getTableEditor().insertTableCell(1, true); + is(editor.innerHTML, "out of table<table><tbody><tr><td>default content</td></tr></tbody></table>", + "nsITableEditor.insertTableCell(1, true) should do nothing if selection is not in <table>"); + is(beforeInputEvents.length, 1, + '"beforeinput" event should be fired when a call of nsITableEditor.insertTableCell(1, true) even though it will do nothing'); + checkInputEvent(beforeInputEvents[0], "when selection is collapsed outside table element (nsITableEditor.insertTableCell(1, true))"); + is(inputEvents.length, 0, + 'No "input" event should be fired when a call of nsITableEditor.insertTableCell(1, true) does nothing'); + + selection.removeAllRanges(); + try { + beforeInputEvents = []; + inputEvents = []; + getTableEditor().insertTableCell(1, false); + ok(false, "getTableEditor().insertTableCell(1, false) without selection ranges should throw exception"); + } catch (e) { + ok(true, "getTableEditor().insertTableCell(1, false) without selection ranges should throw exception"); + is(beforeInputEvents.length, 0, + 'No "beforeinput" event should be fired when nsITableEditor.insertTableCell(1, false) causes exception due to no selection range'); + is(inputEvents.length, 0, + 'No "input" event should be fired when nsITableEditor.insertTableCell(1, false) causes exception due to no selection range'); + } + try { + beforeInputEvents = []; + inputEvents = []; + getTableEditor().insertTableCell(1, true); + ok(false, "getTableEditor().insertTableCell(1, true) without selection ranges should throw exception"); + } catch (e) { + ok(true, "getTableEditor().insertTableCell(1, true) without selection ranges should throw exception"); + is(beforeInputEvents.length, 0, + 'No "beforeinput" event should be fired when nsITableEditor.insertTableCell(1, true) causes exception due to no selection range'); + is(inputEvents.length, 0, + 'No "input" event should be fired when nsITableEditor.insertTableCell(1, true) causes exception due to no selection range'); + } + + (function testInsertZeroCellsBefore() { + selection.removeAllRanges(); + editor.innerHTML = + "<table>" + + "<tr><td>cell1-1</td><td>cell1-2</td></tr>" + + '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>' + + "<tr><td>cell3-1</td><td>cell3-2</td></tr>" + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + selection.setBaseAndExtent( + document.getElementById("select").firstChild, + 0, + document.getElementById("select").firstChild, + 0 + ); + getTableEditor().insertTableCell(0, false); + is( + editor.innerHTML, + "<table><tbody>" + + "<tr><td>cell1-1</td><td>cell1-2</td></tr>" + + '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>' + + "<tr><td>cell3-1</td><td>cell3-2</td></tr>" + + "</tbody></table>", + "testInsertZeroCellsBefore: nsITableEditor.insertTableCell(0, false) should do nothing without throwing exception" + ); + is( + beforeInputEvents.length, + 0, + 'testInsertZeroCellsBefore: No "beforeinput" event should be fired' + ); + is( + inputEvents.length, + 0, + 'testInsertZeroCellsBefore: No "input" event should be fired' + ); + })(); + + (function testInsertZeroCellsAfter() { + selection.removeAllRanges(); + editor.innerHTML = + "<table>" + + "<tr><td>cell1-1</td><td>cell1-2</td></tr>" + + '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>' + + "<tr><td>cell3-1</td><td>cell3-2</td></tr>" + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + selection.setBaseAndExtent( + document.getElementById("select").firstChild, + 0, + document.getElementById("select").firstChild, + 0 + ); + getTableEditor().insertTableCell(0, true); + is( + editor.innerHTML, + "<table><tbody>" + + "<tr><td>cell1-1</td><td>cell1-2</td></tr>" + + '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>' + + "<tr><td>cell3-1</td><td>cell3-2</td></tr>" + + "</tbody></table>", + "testInsertZeroCellsAfter: nsITableEditor.insertTableCell(0, true) should do nothing without throwing exception" + ); + is( + beforeInputEvents.length, + 0, + 'testInsertZeroCellsAfter: No "beforeinput" event should be fired' + ); + is( + inputEvents.length, + 0, + 'testInsertZeroCellsAfter: No "input" event should be fired' + ); + })(); + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + "<tr><td>cell1-1</td><td>cell1-2</td></tr>" + + '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>' + + "<tr><td>cell3-1</td><td>cell3-2</td></tr>" + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + selection.setBaseAndExtent(document.getElementById("select").firstChild, 0, + document.getElementById("select").firstChild, 0); + getTableEditor().insertTableCell(1, false); + is(editor.innerHTML, "<table><tbody>" + + "<tr><td>cell1-1</td><td>cell1-2</td></tr>" + + '<tr><td valign="top"><br></td><td id="select">cell2-1</td><td>cell2-2</td></tr>' + + "<tr><td>cell3-1</td><td>cell3-2</td></tr>" + + "</tbody></table>", + "nsITableEditor.insertTableCell(1, false) should insert a cell before the cell containing selection"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when selection collapsed in a cell in middle row (before)'); + checkInputEvent(beforeInputEvents[0], "when selection collapsed in a cell in middle row (before)"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when selection collapsed in a cell in middle row (before)'); + checkInputEvent(inputEvents[0], "when selection collapsed in a cell in middle row (before)"); + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + "<tr><td>cell1-1</td><td>cell1-2</td></tr>" + + '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>' + + "<tr><td>cell3-1</td><td>cell3-2</td></tr>" + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + selection.setBaseAndExtent(document.getElementById("select").firstChild, 0, + document.getElementById("select").firstChild, 0); + getTableEditor().insertTableCell(1, true); + is(editor.innerHTML, "<table><tbody>" + + "<tr><td>cell1-1</td><td>cell1-2</td></tr>" + + '<tr><td id="select">cell2-1</td><td valign="top"><br></td><td>cell2-2</td></tr>' + + "<tr><td>cell3-1</td><td>cell3-2</td></tr>" + + "</tbody></table>", + "nsITableEditor.insertTableCell(1, true) should insert a cell after the cell containing selection"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when selection collapsed in a cell in middle row (after)'); + checkInputEvent(beforeInputEvents[0], "when selection collapsed in a cell in middle row (after)"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when selection collapsed in a cell in middle row (after)'); + checkInputEvent(inputEvents[0], "when selection collapsed in a cell in middle row (after)"); + + (function testInsertBeforeCellFollowingTextNode() { + selection.removeAllRanges(); + editor.innerHTML = + "<table>" + + "<tr><td>cell1-1</td> <td>cell1-2</td> <td>cell1-3</td></tr>" + + '<tr><td>cell2-1</td> <td id="select">cell2-2</td> <td>cell2-3</tr>' + + "<tr><td>cell3-1</td> <td>cell3-2</td> <td>cell3-3</td></tr>" + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + selection.setBaseAndExtent( + document.getElementById("select").firstChild, + 0, + document.getElementById("select").firstChild, + 0 + ); + getTableEditor().insertTableCell(1, false); + is( + editor.innerHTML, + "<table><tbody>" + + "<tr><td>cell1-1</td> <td>cell1-2</td> <td>cell1-3</td></tr>" + + '<tr><td>cell2-1</td> <td valign="top"><br></td><td id="select">cell2-2</td> <td>cell2-3</td></tr>' + + "<tr><td>cell3-1</td> <td>cell3-2</td> <td>cell3-3</td></tr>" + + "</tbody></table>", + "testInsertBeforeCellFollowingTextNode: nsITableEditor.insertTableCell(1, false) should insert a cell before the cell containing selection" + ); + is( + beforeInputEvents.length, + 1, + 'testInsertBeforeCellFollowingTextNode: Only one "beforeinput" event should be fired' + ); + checkInputEvent( + beforeInputEvents[0], + "when selection collapsed in a cell following a text node (testInsertBeforeCellFollowingTextNode)" + ); + is( + inputEvents.length, + 1, + 'testInsertBeforeCellFollowingTextNode: Only one "input" event should be fired' + ); + checkInputEvent( + inputEvents[0], + "when selection collapsed in a cell following a text node (testInsertBeforeCellFollowingTextNode)" + ); + })(); + + (function testInsertAfterCellFollowedByTextNode() { + selection.removeAllRanges(); + editor.innerHTML = + "<table>" + + "<tr><td>cell1-1</td> <td>cell1-2</td> <td>cell1-3</td></tr>" + + '<tr><td>cell2-1</td> <td id="select">cell2-2</td> <td>cell2-3</tr>' + + "<tr><td>cell3-1</td> <td>cell3-2</td> <td>cell3-3</td></tr>" + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + selection.setBaseAndExtent( + document.getElementById("select").firstChild, + 0, + document.getElementById("select").firstChild, + 0 + ); + getTableEditor().insertTableCell(1, true); + is( + editor.innerHTML, + "<table><tbody>" + + "<tr><td>cell1-1</td> <td>cell1-2</td> <td>cell1-3</td></tr>" + + '<tr><td>cell2-1</td> <td id="select">cell2-2</td><td valign="top"><br></td> <td>cell2-3</td></tr>' + + "<tr><td>cell3-1</td> <td>cell3-2</td> <td>cell3-3</td></tr>" + + "</tbody></table>", + "testInsertAfterCellFollowedByTextNode: nsITableEditor.insertTableCell(1, true) should insert a cell after the cell containing selection" + ); + is( + beforeInputEvents.length, + 1, + 'testInsertAfterCellFollowedByTextNode: Only one "beforeinput" event should be fired' + ); + checkInputEvent( + beforeInputEvents[0], + "when selection collapsed in a cell followed by a text node (testInsertAfterCellFollowedByTextNode)" + ); + is( + inputEvents.length, + 1, + 'testInsertAfterCellFollowedByTextNode: Only one "input" event should be fired' + ); + checkInputEvent( + inputEvents[0], + "when selection collapsed in a cell followed by a text node (testInsertAfterCellFollowedByTextNode)" + ); + })(); + + // with rowspan. + + // Odd case. This puts the cell containing selection moves right of row-spanning cell. + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td>cell1-1</td><td rowspan="2">cell1-2</td></tr>' + + '<tr><td id="select">cell2-1</td></tr>' + + "<tr><td>cell3-1</td><td>cell3-2</td></tr>" + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + selection.setBaseAndExtent(document.getElementById("select").firstChild, 0, + document.getElementById("select").firstChild, 0); + getTableEditor().insertTableCell(1, false); + is(editor.innerHTML, "<table><tbody>" + + '<tr><td>cell1-1</td><td rowspan="2">cell1-2</td></tr>' + + '<tr><td valign="top"><br></td><td id="select">cell2-1</td></tr>' + + "<tr><td>cell3-1</td><td>cell3-2</td></tr>" + + "</tbody></table>", + "nsITableEditor.insertTableCell(1, false) should insert a cell before the cell containing selection and moves the cell to right of the row-spanning cell element"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when selection collapsed in a cell in middle row and it has row-spanned cell (before)'); + checkInputEvent(beforeInputEvents[0], "when selection collapsed in a cell in middle row and it has row-spanned cell (before)"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when selection collapsed in a cell in middle row and it has row-spanned cell (before)'); + checkInputEvent(inputEvents[0], "when selection collapsed in a cell in middle row and it has row-spanned cell (before)"); + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td>cell1-1</td><td rowspan="3">cell1-2</td></tr>' + + '<tr><td id="select">cell2-1</td></tr>' + + "<tr><td>cell3-1</td></tr>" + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + selection.setBaseAndExtent(document.getElementById("select").firstChild, 0, + document.getElementById("select").firstChild, 0); + getTableEditor().insertTableCell(1, true); + is(editor.innerHTML, "<table><tbody>" + + '<tr><td>cell1-1</td><td rowspan="3">cell1-2</td></tr>' + + '<tr><td id="select">cell2-1</td><td valign="top"><br></td></tr>' + + "<tr><td>cell3-1</td></tr>" + + "</tbody></table>", + "nsITableEditor.insertTableCell(1, true) should insert a cell after the cell containing selection and moves the cell to right of the row-spanning cell element"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when selection collapsed in a cell in middle row and it has row-spanned cell (after)'); + checkInputEvent(beforeInputEvents[0], "when selection collapsed in a cell in middle row and it has row-spanned cell (after)"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when selection collapsed in a cell in middle row and it has row-spanned cell (after)'); + checkInputEvent(inputEvents[0], "when selection collapsed in a cell in middle row and it has row-spanned cell (after)"); + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td>cell1-1</td><td id="select" rowspan="2">cell1-2</td></tr>' + + "<tr><td>cell2-1</td></tr>" + + "<tr><td>cell3-1</td><td>cell3-2</td></tr>" + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + selection.setBaseAndExtent(document.getElementById("select").firstChild, 0, + document.getElementById("select").firstChild, 1); + getTableEditor().insertTableCell(2, false); + is(editor.innerHTML, "<table><tbody>" + + '<tr><td>cell1-1</td><td valign="top"><br></td><td valign="top"><br></td><td id="select" rowspan="2">cell1-2</td></tr>' + + "<tr><td>cell2-1</td></tr>" + + "<tr><td>cell3-1</td><td>cell3-2</td></tr>" + + "</tbody></table>", + "nsITableEditor.insertTableCell(2, false) should insert 2 cells before the row-spanning cell containing selection"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when selection collapsed in a cell in row-spanning (before)'); + checkInputEvent(beforeInputEvents[0], "when selection collapsed in a cell in row-spanning (before)"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when selection collapsed in a cell in row-spanning (before)'); + checkInputEvent(inputEvents[0], "when selection collapsed in a cell in row-spanning (before)"); + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td>cell1-1</td><td id="select" rowspan="2">cell1-2</td></tr>' + + "<tr><td>cell2-1</td></tr>" + + "<tr><td>cell3-1</td><td>cell3-2</td></tr>" + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + selection.setBaseAndExtent(document.getElementById("select").firstChild, 0, + document.getElementById("select").firstChild, 1); + getTableEditor().insertTableCell(2, true); + is(editor.innerHTML, "<table><tbody>" + + '<tr><td>cell1-1</td><td id="select" rowspan="2">cell1-2</td><td valign="top"><br></td><td valign="top"><br></td></tr>' + + "<tr><td>cell2-1</td></tr>" + + "<tr><td>cell3-1</td><td>cell3-2</td></tr>" + + "</tbody></table>", + "nsITableEditor.insertTableCell(2, false) should insert 2 cells after the row-spanning cell containing selection"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when selection collapsed in a cell in row-spanning (after)'); + checkInputEvent(beforeInputEvents[0], "when selection collapsed in a cell in row-spanning (after)"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when selection collapsed in a cell in row-spanning (after)'); + checkInputEvent(inputEvents[0], "when selection collapsed in a cell in row-spanning (after)"); + + // with colspan + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td>cell1-1</td><td id="select">cell1-2</td><td>cell1-3</td></tr>' + + '<tr><td colspan="2">cell2-1</td><td>cell2-3</td></tr>' + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + selection.setBaseAndExtent(document.getElementById("select").firstChild, 0, + document.getElementById("select").firstChild, 0); + getTableEditor().insertTableCell(1, false); + is(editor.innerHTML, "<table><tbody>" + + '<tr><td>cell1-1</td><td valign="top"><br></td><td id="select">cell1-2</td><td>cell1-3</td></tr>' + + '<tr><td colspan="2">cell2-1</td><td>cell2-3</td></tr>' + + "</tbody></table>", + "nsITableEditor.insertTableCell(1, false) should insert a cell before the cell containing selection but do not modify col-spanning cell"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when selection collapsed in a cell whose next row cell is col-spanned (before)'); + checkInputEvent(beforeInputEvents[0], "when selection collapsed in a cell whose next row cell is col-spanned (before)"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when selection collapsed in a cell whose next row cell is col-spanned (before)'); + checkInputEvent(inputEvents[0], "when selection collapsed in a cell whose next row cell is col-spanned (before)"); + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td>cell1-1</td><td id="select">cell1-2</td><td>cell1-3</td></tr>' + + '<tr><td colspan="3">cell2-1</td></tr>' + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + selection.setBaseAndExtent(document.getElementById("select").firstChild, 0, + document.getElementById("select").firstChild, 0); + getTableEditor().insertTableCell(1, true); + is(editor.innerHTML, "<table><tbody>" + + '<tr><td>cell1-1</td><td id="select">cell1-2</td><td valign="top"><br></td><td>cell1-3</td></tr>' + + '<tr><td colspan="3">cell2-1</td></tr>' + + "</tbody></table>", + "nsITableEditor.insertTableCell(1, true) should insert a cell after the cell containing selection but do not modify col-spanning cell"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when selection collapsed in a cell whose next row cell is col-spanned (after)'); + checkInputEvent(beforeInputEvents[0], "when selection collapsed in a cell whose next row cell is col-spanned (after)"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when selection collapsed in a cell whose next row cell is col-spanned (after)'); + checkInputEvent(inputEvents[0], "when selection collapsed in a cell whose next row cell is col-spanned (after)"); + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + "<tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>" + + '<tr><td id="select" colspan="2">cell2-1</td><td>cell2-3</td></tr>' + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + selection.setBaseAndExtent(document.getElementById("select").firstChild, 0, + document.getElementById("select").firstChild, 1); + getTableEditor().insertTableCell(2, false); + is(editor.innerHTML, "<table><tbody>" + + "<tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>" + + '<tr><td valign="top"><br></td><td valign="top"><br></td><td id="select" colspan="2">cell2-1</td><td>cell2-3</td></tr>' + + "</tbody></table>", + "nsITableEditor.insertTableCell(2, false) should insert 2 cells before the col-spanning cell containing selection"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when selection collapsed in a cell which is col-spanning (before)'); + checkInputEvent(beforeInputEvents[0], "when selection collapsed in a cell which is col-spanning (before)"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when selection collapsed in a cell which is col-spanning (before)'); + checkInputEvent(inputEvents[0], "when selection collapsed in a cell which is col-spanning (before)"); + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + "<tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>" + + '<tr><td id="select" colspan="2">cell2-1</td><td>cell2-3</td></tr>' + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + selection.setBaseAndExtent(document.getElementById("select").firstChild, 0, + document.getElementById("select").firstChild, 1); + getTableEditor().insertTableCell(2, true); + is(editor.innerHTML, "<table><tbody>" + + "<tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>" + + '<tr><td id="select" colspan="2">cell2-1</td><td valign="top"><br></td><td valign="top"><br></td><td>cell2-3</td></tr>' + + "</tbody></table>", + "nsITableEditor.insertTableCell(2, false) should insert 2 cells after the col-spanning cell containing selection"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when selection collapsed in a cell which is col-spanning (after)'); + checkInputEvent(beforeInputEvents[0], "when selection collapsed in a cell which is col-spanning (after)"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when selection collapsed in a cell which is col-spanning (after)'); + checkInputEvent(inputEvents[0], "when selection collapsed in a cell which is col-spanning (after)"); + + editor.removeEventListener("beforeinput", onBeforeInput); + editor.removeEventListener("input", onInput); + + SimpleTest.finish(); +}); + +function getTableEditor() { + var editingSession = SpecialPowers.wrap(window).docShell.editingSession; + return editingSession.getEditorForWindow(window).QueryInterface(SpecialPowers.Ci.nsITableEditor); +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_nsITableEditor_insertTableColumn.html b/editor/libeditor/tests/test_nsITableEditor_insertTableColumn.html new file mode 100644 index 0000000000..acf853aed1 --- /dev/null +++ b/editor/libeditor/tests/test_nsITableEditor_insertTableColumn.html @@ -0,0 +1,547 @@ +<!DOCTYPE> +<html> +<head> + <title>Test for nsITableEditor.insertTableColumn()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> +<div id="display"> +</div> +<div id="content" contenteditable>out of table<table><tr><td>default content</td></tr></table></div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> +"use strict"; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + let editor = document.getElementById("content"); + let selection = document.getSelection(); + let selectionRanges = []; + + function checkInputEvent(aEvent, aDescription) { + ok(aEvent instanceof InputEvent, + `"${aEvent.type}" event should be dispatched with InputEvent interface ${aDescription}`); + is(aEvent.cancelable, false, + `"${aEvent.type}" event should be never cancelable ${aDescription}`); + is(aEvent.bubbles, true, + `"${aEvent.type}" event should always bubble ${aDescription}`); + is(aEvent.inputType, "", + `inputType of "${aEvent.type}" event should be empty string ${aDescription}`); + is(aEvent.data, null, + `data of "${aEvent.type}" event should be null ${aDescription}`); + is(aEvent.dataTransfer, null, + `dataTransfer of "${aEvent.type}" event should be null ${aDescription}`); + let targetRanges = aEvent.getTargetRanges(); + if (aEvent.type === "beforeinput") { + is(targetRanges.length, selectionRanges.length, + `getTargetRanges() of "beforeinput" event should return selection ranges ${aDescription}`); + if (targetRanges.length === selectionRanges.length) { + for (let i = 0; i < selectionRanges.length; i++) { + is(targetRanges[i].startContainer, selectionRanges[i].startContainer, + `startContainer of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`); + is(targetRanges[i].startOffset, selectionRanges[i].startOffset, + `startOffset of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`); + is(targetRanges[i].endContainer, selectionRanges[i].endContainer, + `endContainer of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`); + is(targetRanges[i].endOffset, selectionRanges[i].endOffset, + `endOffset of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`); + } + } + } else { + is(targetRanges.length, 0, + `getTargetRanges() of "${aEvent.type}" event should return empty array ${aDescription}`); + } + } + + let beforeInputEvents = []; + let inputEvents = []; + function onBeforeInput(aEvent) { + beforeInputEvents.push(aEvent); + selectionRanges = []; + for (let i = 0; i < selection.rangeCount; i++) { + let range = selection.getRangeAt(i); + selectionRanges.push({startContainer: range.startContainer, startOffset: range.startOffset, + endContainer: range.endContainer, endOffset: range.endOffset}); + } + } + function onInput(aEvent) { + inputEvents.push(aEvent); + } + editor.addEventListener("beforeinput", onBeforeInput); + editor.addEventListener("input", onInput); + + beforeInputEvents = []; + inputEvents = []; + selection.collapse(editor.firstChild, 0); + getTableEditor().insertTableColumn(1, false); + is(editor.innerHTML, "out of table<table><tbody><tr><td>default content</td></tr></tbody></table>", + "nsITableEditor.insertTableColumn(1, false) should do nothing if selection is not in <table>"); + is(beforeInputEvents.length, 1, + 'beforeinput" event should be fired when a call of nsITableEditor.insertTableColumn(1, false) even though it will do nothing'); + checkInputEvent(beforeInputEvents[0], "when selection is collapsed outside table element (nsITableEditor.insertTableColumn(1, false))"); + is(inputEvents.length, 0, + 'No "input" event should be fired when a call of nsITableEditor.insertTableColumn(1, false) does nothing'); + + beforeInputEvents = []; + inputEvents = []; + getTableEditor().insertTableColumn(1, true); + is(editor.innerHTML, "out of table<table><tbody><tr><td>default content</td></tr></tbody></table>", + "nsITableEditor.insertTableColumn(1, true) should do nothing if selection is not in <table>"); + is(beforeInputEvents.length, 1, + '"beforeinput" event should be fired when a call of nsITableEditor.insertTableColumn(1, true) even though it will do nothing'); + checkInputEvent(beforeInputEvents[0], "when selection is collapsed outside table element (nsITableEditor.insertTableColumn(1, true))"); + is(inputEvents.length, 0, + 'No "input" event should be fired when a call of nsITableEditor.insertTableColumn(1, true) does nothing'); + + selection.removeAllRanges(); + try { + beforeInputEvents = []; + inputEvents = []; + getTableEditor().insertTableColumn(1, false); + ok(false, "getTableEditor().insertTableColumn(1, false) without selection ranges should throw exception"); + } catch (e) { + ok(true, "getTableEditor().insertTableColumn(1, false) without selection ranges should throw exception"); + is(beforeInputEvents.length, 0, + 'No "beforeinput" event should be fired when nsITableEditor.insertTableColumn(1, false) causes exception due to no selection range'); + is(inputEvents.length, 0, + 'No "input" event should be fired when nsITableEditor.insertTableColumn(1, false) causes exception due to no selection range'); + } + try { + beforeInputEvents = []; + inputEvents = []; + getTableEditor().insertTableColumn(1, true); + ok(false, "getTableEditor().insertTableColumn(1, true) without selection ranges should throw exception"); + } catch (e) { + ok(true, "getTableEditor().insertTableColumn(1, true) without selection ranges should throw exception"); + is(beforeInputEvents.length, 0, + 'No "beforeinput" event should be fired when nsITableEditor.insertTableColumn(1, true) causes exception due to no selection range'); + is(inputEvents.length, 0, + 'No "input" event should be fired when nsITableEditor.insertTableColumn(1, true) causes exception due to no selection range'); + } + + (function testInsertBeforeFirstColumn() { + selection.removeAllRanges(); + editor.innerHTML = + "<table>" + + '<tr><td id="select">cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>' + + "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + selection.setBaseAndExtent( + document.getElementById("select").firstChild, + 0, + document.getElementById("select").firstChild, + 0 + ); + getTableEditor().insertTableColumn(1, false); + is( + editor.innerHTML, + "<table><tbody>" + + '<tr><td valign="top"><br></td><td id="select">cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>' + + '<tr><td valign="top"><br></td><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>' + + "</tbody></table>", + "testInsertBeforeFirstColumn: nsITableEditor.insertTableColumn(1, false) should insert a column before the first column" + ); + is( + beforeInputEvents.length, + 1, + 'testInsertBeforeFirstColumn: Only one "beforeinput" event should be fired' + ); + checkInputEvent( + beforeInputEvents[0], + "when selection is collapsed in the first column (testInsertBeforeFirstColumn)" + ); + is( + inputEvents.length, + 1, + 'testInsertBeforeFirstColumn: Only one "input" event should be fired' + ); + checkInputEvent( + inputEvents[0], + "when selection is collapsed in the first column (testInsertBeforeFirstColumn)" + ); + })(); + + (function testInsertAfterLastColumn() { + selection.removeAllRanges(); + editor.innerHTML = + "<table>" + + '<tr><td>cell1-1</td><td>cell1-2</td><td id="select">cell1-3</td></tr>' + + "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + selection.setBaseAndExtent( + document.getElementById("select").firstChild, + 0, + document.getElementById("select").firstChild, + 0 + ); + getTableEditor().insertTableColumn(1, true); + is( + editor.innerHTML, + "<table><tbody>" + + '<tr><td>cell1-1</td><td>cell1-2</td><td id="select">cell1-3</td><td valign="top"><br></td></tr>' + + '<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td><td valign="top"><br></td></tr>' + + "</tbody></table>", + "testInsertAfterLastColumn: nsITableEditor.insertTableColumn(1, true) should insert a column after the last column" + ); + is( + beforeInputEvents.length, + 1, + 'testInsertAfterLastColumn: Only one "beforeinput" event should be fired' + ); + checkInputEvent( + beforeInputEvents[0], + "when selection is collapsed in the last column (testInsertAfterLastColumn)" + ); + is( + inputEvents.length, + 1, + 'testInsertAfterLastColumn: One "input" event should be fired' + ); + checkInputEvent( + inputEvents[0], + "when selection is collapsed in the last column (testInsertAfterLastColumn)" + ); + })(); + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td>cell1-1</td><td id="select">cell1-2</td><td>cell1-3</td></tr>' + + "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + selection.setBaseAndExtent(document.getElementById("select").firstChild, 0, + document.getElementById("select").firstChild, 0); + getTableEditor().insertTableColumn(1, false); + is(editor.innerHTML, "<table><tbody>" + + '<tr><td>cell1-1</td><td valign="top"><br></td><td id="select">cell1-2</td><td>cell1-3</td></tr>' + + '<tr><td>cell2-1</td><td valign="top"><br></td><td>cell2-2</td><td>cell2-3</td></tr>' + + "</tbody></table>", + "nsITableEditor.insertTableColumn(1, false) should insert a column to left of the second column"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when selection is collapsed in a cell in second column (before)'); + checkInputEvent(beforeInputEvents[0], "when selection is collapsed in a cell in second column (before)"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when selection is collapsed in a cell in second column (before)'); + checkInputEvent(inputEvents[0], "when selection is collapsed in a cell in second column (before)"); + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td>cell1-1</td><td id="select">cell1-2</td><td>cell1-3</td></tr>' + + "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + selection.setBaseAndExtent(document.getElementById("select").firstChild, 0, + document.getElementById("select").firstChild, 0); + getTableEditor().insertTableColumn(1, true); + is(editor.innerHTML, "<table><tbody>" + + '<tr><td>cell1-1</td><td id="select">cell1-2</td><td valign="top"><br></td><td>cell1-3</td></tr>' + + '<tr><td>cell2-1</td><td>cell2-2</td><td valign="top"><br></td><td>cell2-3</td></tr>' + + "</tbody></table>", + "nsITableEditor.insertTableColumn(1, false) should insert a column to right of the second column"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when selection is collapsed in a cell in second column (after)'); + checkInputEvent(beforeInputEvents[0], "when selection is collapsed in a cell in second column (after)"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when selection is collapsed in a cell in second column (after)'); + checkInputEvent(inputEvents[0], "when selection is collapsed in a cell in second column (after)"); + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td>cell1-1</td><td id="select">cell1-2</td><td>cell1-3</td></tr>' + + '<tr><td colspan="2">cell2-1</td><td>cell2-3</td></tr>' + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + selection.setBaseAndExtent(document.getElementById("select").firstChild, 0, + document.getElementById("select").firstChild, 0); + getTableEditor().insertTableColumn(1, false); + is(editor.innerHTML, "<table><tbody>" + + '<tr><td>cell1-1</td><td valign="top"><br></td><td id="select">cell1-2</td><td>cell1-3</td></tr>' + + '<tr><td colspan="3">cell2-1</td><td>cell2-3</td></tr>' + + "</tbody></table>", + "nsITableEditor.insertTableColumn(1, false) should insert a column to left of the second column and colspan in the first column should be increased"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when selection is collapsed in a cell in second column whose next row cell is col-spanned (before)'); + checkInputEvent(beforeInputEvents[0], "when selection is collapsed in a cell in second column whose next row cell is col-spanned (before)"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when selection is collapsed in a cell in second column whose next row cell is col-spanned (before)'); + checkInputEvent(inputEvents[0], "when selection is collapsed in a cell in second column whose next row cell is col-spanned (before)"); + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td>cell1-1</td><td id="select">cell1-2</td><td>cell1-3</td></tr>' + + '<tr><td colspan="3">cell2-1</td></tr>' + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + selection.setBaseAndExtent(document.getElementById("select").firstChild, 0, + document.getElementById("select").firstChild, 0); + getTableEditor().insertTableColumn(1, true); + is(editor.innerHTML, "<table><tbody>" + + '<tr><td>cell1-1</td><td id="select">cell1-2</td><td valign="top"><br></td><td>cell1-3</td></tr>' + + '<tr><td colspan="4">cell2-1</td></tr>' + + "</tbody></table>", + "nsITableEditor.insertTableColumn(1, true) should insert a column to right of the second column and colspan in the first column should be increased"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when selection is collapsed in a cell in second column whose next row cell is col-spanned (after)'); + checkInputEvent(beforeInputEvents[0], "when selection is collapsed in a cell in second column whose next row cell is col-spanned (after)"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when selection is collapsed in a cell in second column whose next row cell is col-spanned (after)'); + checkInputEvent(inputEvents[0], "when selection is collapsed in a cell in second column whose next row cell is col-spanned (after)"); + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + "<tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>" + + '<tr><td id="select" colspan="2">cell2-1</td><td>cell2-3</td></tr>' + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + selection.setBaseAndExtent(document.getElementById("select").firstChild, 0, + document.getElementById("select").firstChild, 1); + getTableEditor().insertTableColumn(2, false); + is(editor.innerHTML, "<table><tbody>" + + '<tr><td valign="top"><br></td><td valign="top"><br></td><td>cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>' + + '<tr><td valign="top"><br></td><td valign="top"><br></td><td id="select" colspan="2">cell2-1</td><td>cell2-3</td></tr>' + + "</tbody></table>", + "nsITableEditor.insertTableColumn(2, false) should insert 2 columns to left of the first column"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when selection is collapsed in a cell which is col-spanning (before)'); + checkInputEvent(beforeInputEvents[0], "when selection is collapsed in a cell which is col-spanning (before)"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when selection is collapsed in a cell which is col-spanning (before)'); + checkInputEvent(inputEvents[0], "when selection is collapsed in a cell which is col-spanning (before)"); + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + "<tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</td></tr>" + + '<tr><td id="select" colspan="2">cell2-1</td><td>cell2-3</td></tr>' + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + selection.setBaseAndExtent(document.getElementById("select").firstChild, 0, + document.getElementById("select").firstChild, 1); + getTableEditor().insertTableColumn(2, true); + is(editor.innerHTML, "<table><tbody>" + + '<tr><td>cell1-1</td><td>cell1-2</td><td valign="top"><br></td><td valign="top"><br></td><td>cell1-3</td></tr>' + + '<tr><td id="select" colspan="2">cell2-1</td><td valign="top"><br></td><td valign="top"><br></td><td>cell2-3</td></tr>' + + "</tbody></table>", + "nsITableEditor.insertTableColumn(2, false) should insert 2 columns to right of the second column (i.e., right of the right-most column of the column-spanning cell"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when selection is collapsed in a cell which is col-spanning (after)'); + checkInputEvent(beforeInputEvents[0], "when selection is collapsed in a cell which is col-spanning (after)"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when selection is collapsed in a cell which is col-spanning (after)'); + checkInputEvent(inputEvents[0], "when selection is collapsed in a cell which is col-spanning (after)"); + + (function testInsertBeforeFirstColumnFollowingTextNode() { + selection.removeAllRanges(); + editor.innerHTML = + "<table>" + + '<tr> <td id="select">cell1-1</td><td>cell1-2</td><td>cell1-3</td> </tr>' + + "<tr> <td>cell2-1</td><td>cell2-2</td><td>cell2-3</td> </tr>" + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + selection.setBaseAndExtent( + document.getElementById("select").firstChild, + 0, + document.getElementById("select").firstChild, + 0 + ); + getTableEditor().insertTableColumn(1, false); + is( + editor.innerHTML, + "<table><tbody>" + + '<tr> <td valign="top"><br></td><td id="select">cell1-1</td><td>cell1-2</td><td>cell1-3</td> </tr>' + + '<tr> <td valign="top"><br></td><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td> </tr>' + + "</tbody></table>", + "testInsertBeforeFirstColumnFollowingTextNode: nsITableEditor.insertTableColumn(1, false) should insert a column before the first column" + ); + is( + beforeInputEvents.length, + 1, + 'testInsertBeforeFirstColumnFollowingTextNode: Only one "beforeinput" event should be fired' + ); + checkInputEvent( + beforeInputEvents[0], + "when selection is collapsed in the first column (testInsertBeforeFirstColumnFollowingTextNode)" + ); + is( + inputEvents.length, + 1, + 'testInsertBeforeFirstColumnFollowingTextNode: Only one "input" event should be fired' + ); + checkInputEvent( + inputEvents[0], + "when selection is collapsed in the first column (testInsertBeforeFirstColumnFollowingTextNode)" + ); + })(); + + (function testInsertAfterLastColumnFollowedByTextNode() { + selection.removeAllRanges(); + editor.innerHTML = + "<table>" + + '<tr> <td>cell1-1</td><td>cell1-2</td><td id="select">cell1-3</td> </tr>' + + "<tr> <td>cell2-1</td><td>cell2-2</td><td>cell2-3</td> </tr>" + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + selection.setBaseAndExtent( + document.getElementById("select").firstChild, + 0, + document.getElementById("select").firstChild, + 0 + ); + getTableEditor().insertTableColumn(1, true); + is( + editor.innerHTML, + "<table><tbody>" + + '<tr> <td>cell1-1</td><td>cell1-2</td><td id="select">cell1-3</td><td valign="top"><br></td> </tr>' + + '<tr> <td>cell2-1</td><td>cell2-2</td><td>cell2-3</td><td valign="top"><br></td> </tr>' + + "</tbody></table>", + "testInsertAfterLastColumnFollowedByTextNode: nsITableEditor.insertTableColumn(1, true) should insert a column after the last column" + ); + is( + beforeInputEvents.length, + 1, + 'testInsertAfterLastColumnFollowedByTextNode: Only one "beforeinput" event should be fired' + ); + checkInputEvent( + beforeInputEvents[0], + "when selection is collapsed in the last column (testInsertAfterLastColumnFollowedByTextNode)" + ); + is( + inputEvents.length, + 1, + 'testInsertAfterLastColumnFollowedByTextNode: One "input" event should be fired' + ); + checkInputEvent( + inputEvents[0], + "when selection is collapsed in the last column (testInsertAfterLastColumnFollowedByTextNode)" + ); + })(); + + (function testInsertBeforeColumnFollowingTextNode() { + selection.removeAllRanges(); + editor.innerHTML = + "<table>" + + '<tr><td>cell1-1</td> <td id="select">cell1-2</td> <td>cell1-3</td></tr>' + + "<tr><td>cell2-1</td> <td>cell2-2</td> <td>cell2-3</td></tr>" + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + selection.setBaseAndExtent( + document.getElementById("select").firstChild, + 0, + document.getElementById("select").firstChild, + 0 + ); + getTableEditor().insertTableColumn(1, false); + is( + editor.innerHTML, + "<table><tbody>" + + '<tr><td>cell1-1</td> <td valign="top"><br></td><td id="select">cell1-2</td> <td>cell1-3</td></tr>' + + '<tr><td>cell2-1</td> <td valign="top"><br></td><td>cell2-2</td> <td>cell2-3</td></tr>' + + "</tbody></table>", + "testInsertBeforeColumnFollowingTextNode: nsITableEditor.insertTableColumn(1, false) should insert a column before the first column" + ); + is( + beforeInputEvents.length, + 1, + 'testInsertBeforeColumnFollowingTextNode: Only one "beforeinput" event should be fired' + ); + checkInputEvent( + beforeInputEvents[0], + "when selection is collapsed in the column following a text node (testInsertBeforeColumnFollowingTextNode)" + ); + is( + inputEvents.length, + 1, + 'testInsertBeforeColumnFollowingTextNode: Only one "input" event should be fired' + ); + checkInputEvent( + inputEvents[0], + "when selection is collapsed in the column following a text node (testInsertBeforeColumnFollowingTextNode)" + ); + })(); + + (function testInsertAfterColumnFollowedByTextNode() { + selection.removeAllRanges(); + editor.innerHTML = + "<table>" + + '<tr><td>cell1-1</td> <td id="select">cell1-2</td> <td>cell1-3</td></tr>' + + "<tr><td>cell2-1</td> <td>cell2-2</td> <td>cell2-3</td></tr>" + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + selection.setBaseAndExtent( + document.getElementById("select").firstChild, + 0, + document.getElementById("select").firstChild, + 0 + ); + getTableEditor().insertTableColumn(1, true); + is( + editor.innerHTML, + "<table><tbody>" + + '<tr><td>cell1-1</td> <td id="select">cell1-2</td><td valign="top"><br></td> <td>cell1-3</td></tr>' + + '<tr><td>cell2-1</td> <td>cell2-2</td><td valign="top"><br></td> <td>cell2-3</td></tr>' + + "</tbody></table>", + "testInsertAfterColumnFollowedByTextNode: nsITableEditor.insertTableColumn(1, true) should insert a column before the first column" + ); + is( + beforeInputEvents.length, + 1, + 'testInsertAfterColumnFollowedByTextNode: Only one "beforeinput" event should be fired' + ); + checkInputEvent( + beforeInputEvents[0], + "when selection is collapsed in the column followed by a text node (testInsertAfterColumnFollowedByTextNode)" + ); + is( + inputEvents.length, + 1, + 'testInsertAfterColumnFollowedByTextNode: Only one "input" event should be fired' + ); + checkInputEvent( + inputEvents[0], + "when selection is collapsed in the column followed by a text node (testInsertAfterColumnFollowedByTextNode)" + ); + })(); + + editor.removeEventListener("beforeinput", onBeforeInput); + editor.removeEventListener("input", onInput); + + SimpleTest.finish(); +}); + +function getTableEditor() { + var editingSession = SpecialPowers.wrap(window).docShell.editingSession; + return editingSession.getEditorForWindow(window).QueryInterface(SpecialPowers.Ci.nsITableEditor); +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_nsITableEditor_insertTableRow.html b/editor/libeditor/tests/test_nsITableEditor_insertTableRow.html new file mode 100644 index 0000000000..84c2443af8 --- /dev/null +++ b/editor/libeditor/tests/test_nsITableEditor_insertTableRow.html @@ -0,0 +1,432 @@ +<!DOCTYPE> +<html> +<head> + <title>Test for nsITableEditor.insertTableRow()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> +<div id="display"> +</div> +<div id="content" contenteditable>out of table<table><tr><td>default content</td></tr></table></div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> +"use strict"; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + let editor = document.getElementById("content"); + let selection = document.getSelection(); + let selectionRanges = []; + + function checkInputEvent(aEvent, aDescription) { + ok(aEvent instanceof InputEvent, + `"${aEvent.type}" event should be dispatched with InputEvent interface ${aDescription}`); + is(aEvent.cancelable, false, + `"${aEvent.type}" event should be never cancelable ${aDescription}`); + is(aEvent.bubbles, true, + `"${aEvent.type}" event should always bubble ${aDescription}`); + is(aEvent.inputType, "", + `inputType of "${aEvent.type}" event should be empty string ${aDescription}`); + is(aEvent.data, null, + `data of "${aEvent.type}" event should be null ${aDescription}`); + is(aEvent.dataTransfer, null, + `dataTransfer of "${aEvent.type}" event should be null ${aDescription}`); + let targetRanges = aEvent.getTargetRanges(); + if (aEvent.type === "beforeinput") { + is(targetRanges.length, selectionRanges.length, + `getTargetRanges() of "beforeinput" event should return selection ranges ${aDescription}`); + if (targetRanges.length === selectionRanges.length) { + for (let i = 0; i < selectionRanges.length; i++) { + is(targetRanges[i].startContainer, selectionRanges[i].startContainer, + `startContainer of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`); + is(targetRanges[i].startOffset, selectionRanges[i].startOffset, + `startOffset of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`); + is(targetRanges[i].endContainer, selectionRanges[i].endContainer, + `endContainer of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`); + is(targetRanges[i].endOffset, selectionRanges[i].endOffset, + `endOffset of getTargetRanges()[${i}] of "beforeinput" event does not match ${aDescription}`); + } + } + } else { + is(targetRanges.length, 0, + `getTargetRanges() of "${aEvent.type}" event should return empty array ${aDescription}`); + } + } + + let beforeInputEvents = []; + let inputEvents = []; + function onBeforeInput(aEvent) { + beforeInputEvents.push(aEvent); + selectionRanges = []; + for (let i = 0; i < selection.rangeCount; i++) { + let range = selection.getRangeAt(i); + selectionRanges.push({startContainer: range.startContainer, startOffset: range.startOffset, + endContainer: range.endContainer, endOffset: range.endOffset}); + } + } + function onInput(aEvent) { + inputEvents.push(aEvent); + } + editor.addEventListener("beforeinput", onBeforeInput); + editor.addEventListener("input", onInput); + + beforeInputEvents = []; + inputEvents = []; + selection.collapse(editor.firstChild, 0); + getTableEditor().insertTableRow(1, false); + is(editor.innerHTML, "out of table<table><tbody><tr><td>default content</td></tr></tbody></table>", + "nsITableEditor.insertTableRow(1, false) should do nothing if selection is not in <table>"); + is(beforeInputEvents.length, 1, + '"beforeinput" event should be fired when a call of nsITableEditor.insertTableRow(1, false) even though it will do nothing'); + checkInputEvent(beforeInputEvents[0], "when selection is collapsed outside of table element (nsITableEditor.insertTableRow(1, false))"); + is(inputEvents.length, 0, + 'No "input" event should be fired when a call of nsITableEditor.insertTableRow(1, false) does nothing'); + + beforeInputEvents = []; + inputEvents = []; + getTableEditor().insertTableRow(1, true); + is(editor.innerHTML, "out of table<table><tbody><tr><td>default content</td></tr></tbody></table>", + "nsITableEditor.insertTableRow(1, true) should do nothing if selection is not in <table>"); + is(beforeInputEvents.length, 1, + '"beforeinput" event should be fired when a call of nsITableEditor.insertTableRow(1, true) even though it will do nothing'); + checkInputEvent(beforeInputEvents[0], "when selection is collapsed outside of table element (nsITableEditor.insertTableRow(1, true))"); + is(inputEvents.length, 0, + 'No "input" event should be fired when a call of nsITableEditor.insertTableRow(1, true) does nothing'); + + selection.removeAllRanges(); + try { + beforeInputEvents = []; + inputEvents = []; + getTableEditor().insertTableRow(1, false); + ok(false, "getTableEditor().insertTableRow(1, false) without selection ranges should throw exception"); + } catch (e) { + ok(true, "getTableEditor().insertTableRow(1, false) without selection ranges should throw exception"); + is(beforeInputEvents.length, 0, + 'No "beforeinput" event should be fired when nsITableEditor.insertTableRow(1, false) causes exception due to no selection range'); + is(inputEvents.length, 0, + 'No "input" event should be fired when nsITableEditor.insertTableRow(1, false) causes exception due to no selection range'); + } + try { + beforeInputEvents = []; + inputEvents = []; + getTableEditor().insertTableRow(1, true); + ok(false, "getTableEditor().insertTableRow(1, true) without selection ranges should throw exception"); + } catch (e) { + ok(true, "getTableEditor().insertTableRow(1, true) without selection ranges should throw exception"); + is(beforeInputEvents.length, 0, + 'No "beforeinput" event should be fired when nsITableEditor.insertTableRow(1, true) causes exception due to no selection range'); + is(inputEvents.length, 0, + 'No "input" event should be fired when nsITableEditor.insertTableRow(1, true) causes exception due to no selection range'); + } + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + "<tr><td>cell1-1</td><td>cell1-2</td></tr>" + + '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>' + + "<tr><td>cell3-1</td><td>cell3-2</td></tr>" + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + selection.setBaseAndExtent(document.getElementById("select").firstChild, 0, + document.getElementById("select").firstChild, 0); + getTableEditor().insertTableRow(1, false); + is(editor.innerHTML, "<table><tbody>" + + "<tr><td>cell1-1</td><td>cell1-2</td></tr>" + + '<tr><td valign="top"><br></td><td valign="top"><br></td></tr>' + + '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>' + + "<tr><td>cell3-1</td><td>cell3-2</td></tr>" + + "</tbody></table>", + "nsITableEditor.insertTableRow(1, false) should insert a row above the second row"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when selection is collapsed in a cell in second row (before)'); + checkInputEvent(beforeInputEvents[0], "when selection is collapsed in a cell in second row (before)"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when selection is collapsed in a cell in second row (before)'); + checkInputEvent(inputEvents[0], "when selection is collapsed in a cell in second row (before)"); + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + "<tr><td>cell1-1</td><td>cell1-2</td></tr>" + + '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>' + + "<tr><td>cell3-1</td><td>cell3-2</td></tr>" + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + selection.setBaseAndExtent(document.getElementById("select").firstChild, 0, + document.getElementById("select").firstChild, 0); + getTableEditor().insertTableRow(1, true); + is(editor.innerHTML, "<table><tbody>" + + "<tr><td>cell1-1</td><td>cell1-2</td></tr>" + + '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>' + + '<tr><td valign="top"><br></td><td valign="top"><br></td></tr>' + + "<tr><td>cell3-1</td><td>cell3-2</td></tr>" + + "</tbody></table>", + "nsITableEditor.insertTableRow(1, true) should insert a row below the second row"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when selection is collapsed in a cell in second row (after)'); + checkInputEvent(beforeInputEvents[0], "when selection is collapsed in a cell in second row (after)"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when selection is collapsed in a cell in second row (after)'); + checkInputEvent(inputEvents[0], "when selection is collapsed in a cell in second row (after)"); + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td>cell1-1</td><td rowspan="2">cell1-2</td></tr>' + + '<tr><td id="select">cell2-1</td></tr>' + + "<tr><td>cell3-1</td><td>cell3-2</td></tr>" + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + selection.setBaseAndExtent(document.getElementById("select").firstChild, 0, + document.getElementById("select").firstChild, 0); + getTableEditor().insertTableRow(1, false); + is(editor.innerHTML, "<table><tbody>" + + '<tr><td>cell1-1</td><td rowspan="3">cell1-2</td></tr>' + + '<tr><td valign="top"><br></td></tr>' + + '<tr><td id="select">cell2-1</td></tr>' + + "<tr><td>cell3-1</td><td>cell3-2</td></tr>" + + "</tbody></table>", + "nsITableEditor.insertTableRow(1, false) should insert a row above the second row and rowspan in the first row should be increased"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when selection is collapsed in a cell in second row which has row-spanned cell (before)'); + checkInputEvent(beforeInputEvents[0], "when selection is collapsed in a cell in second row which has row-spanned cell (before)"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when selection is collapsed in a cell in second row which has row-spanned cell (before)'); + checkInputEvent(inputEvents[0], "when selection is collapsed in a cell in second row which has row-spanned cell (before)"); + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td>cell1-1</td><td rowspan="3">cell1-2</td></tr>' + + '<tr><td id="select">cell2-1</td></tr>' + + "<tr><td>cell3-1</td></tr>" + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + selection.setBaseAndExtent(document.getElementById("select").firstChild, 0, + document.getElementById("select").firstChild, 0); + getTableEditor().insertTableRow(1, true); + is(editor.innerHTML, "<table><tbody>" + + '<tr><td>cell1-1</td><td rowspan="4">cell1-2</td></tr>' + + '<tr><td id="select">cell2-1</td></tr>' + + '<tr><td valign="top"><br></td></tr>' + + "<tr><td>cell3-1</td></tr>" + + "</tbody></table>", + "nsITableEditor.insertTableRow(1, true) should insert a row below the second row and rowspan in the first row should be increased"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when selection is collapsed in a cell in second row which has row-spanned cell (after)'); + checkInputEvent(beforeInputEvents[0], "when selection is collapsed in a cell in second row which has row-spanned cell (after)"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when selection is collapsed in a cell in second row which has row-spanned cell (after)'); + checkInputEvent(inputEvents[0], "when selection is collapsed in a cell in second row which has row-spanned cell (after)"); + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td>cell1-1</td><td id="select" rowspan="2">cell1-2</td></tr>' + + "<tr><td>cell2-1</td></tr>" + + "<tr><td>cell3-1</td><td>cell3-2</td></tr>" + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + selection.setBaseAndExtent(document.getElementById("select").firstChild, 0, + document.getElementById("select").firstChild, 1); + getTableEditor().insertTableRow(2, false); + is(editor.innerHTML, "<table><tbody>" + + '<tr><td valign="top"><br></td><td valign="top"><br></td></tr>' + + '<tr><td valign="top"><br></td><td valign="top"><br></td></tr>' + + '<tr><td>cell1-1</td><td id="select" rowspan="2">cell1-2</td></tr>' + + "<tr><td>cell2-1</td></tr>" + + "<tr><td>cell3-1</td><td>cell3-2</td></tr>" + + "</tbody></table>", + "nsITableEditor.insertTableRow(2, false) should insert 2 rows above the first row"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when selection is collapsed in a cell which is row-spanning (before)'); + checkInputEvent(beforeInputEvents[0], "when selection is collapsed in a cell which is row-spanning (before)"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when selection is collapsed in a cell which is row-spanning (before)'); + checkInputEvent(inputEvents[0], "when selection is collapsed in a cell which is row-spanning (before)"); + + selection.removeAllRanges(); + editor.innerHTML = "<table>" + + '<tr><td>cell1-1</td><td id="select" rowspan="2">cell1-2</td></tr>' + + "<tr><td>cell2-1</td></tr>" + + "<tr><td>cell3-1</td><td>cell3-2</td></tr>" + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + selection.setBaseAndExtent(document.getElementById("select").firstChild, 0, + document.getElementById("select").firstChild, 1); + getTableEditor().insertTableRow(2, true); + is(editor.innerHTML, "<table><tbody>" + + '<tr><td>cell1-1</td><td id="select" rowspan="2">cell1-2</td></tr>' + + "<tr><td>cell2-1</td></tr>" + + '<tr><td valign="top"><br></td><td valign="top"><br></td></tr>' + + '<tr><td valign="top"><br></td><td valign="top"><br></td></tr>' + + "<tr><td>cell3-1</td><td>cell3-2</td></tr>" + + "</tbody></table>", + "nsITableEditor.insertTableRow(2, false) should insert 2 rows below the second row (i.e., below the bottom row of the row-spanning cell"); + is(beforeInputEvents.length, 1, + 'Only one "beforeinput" event should be fired when selection is collapsed in a cell which is row-spanning (after)'); + checkInputEvent(beforeInputEvents[0], "when selection is collapsed in a cell which is row-spanning (after)"); + is(inputEvents.length, 1, + 'Only one "input" event should be fired when selection is collapsed in a cell which is row-spanning (after)'); + checkInputEvent(inputEvents[0], "when selection is collapsed in a cell which is row-spanning (after)"); + + (function testInsertBeforeRowFollowingTextNode() { + selection.removeAllRanges(); + editor.innerHTML = + "<table>" + + "<tr><td>cell1-1</td><td>cell1-2</td></tr>\n" + + '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>\n' + + "<tr><td>cell3-1</td><td>cell3-2</td></tr>" + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + selection.setBaseAndExtent( + document.getElementById("select").firstChild, + 0, + document.getElementById("select").firstChild, + 0 + ); + getTableEditor().insertTableRow(1, false); + is( + editor.innerHTML, + "<table><tbody>" + + "<tr><td>cell1-1</td><td>cell1-2</td></tr>\n" + + '<tr><td valign="top"><br></td><td valign="top"><br></td></tr>' + + '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>\n' + + "<tr><td>cell3-1</td><td>cell3-2</td></tr>" + + "</tbody></table>", + "testInsertBeforeRowFollowingTextNode: nsITableEditor.insertTableRow(1, false) should insert a row above the second row"); + is( + beforeInputEvents.length, + 1, + 'testInsertBeforeRowFollowingTextNode: Only one "beforeinput" event should be fired' + ); + checkInputEvent( + beforeInputEvents[0], + "when selection is collapsed in a cell whose row follows a text node (testInsertBeforeRowFollowingTextNode)" + ); + is( + inputEvents.length, + 1, + 'testInsertBeforeRowFollowingTextNode: Only one "input" event should be fired' + ); + checkInputEvent( + inputEvents[0], + "when selection is collapsed in a cell whose row follows a text node (testInsertBeforeRowFollowingTextNode)" + ); + })(); + + (function testInsertAfterRowFollowedTextNode() { + selection.removeAllRanges(); + editor.innerHTML = + "<table>" + + "<tr><td>cell1-1</td><td>cell1-2</td></tr>\n" + + '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>\n' + + "<tr><td>cell3-1</td><td>cell3-2</td></tr>" + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + selection.setBaseAndExtent( + document.getElementById("select").firstChild, + 0, + document.getElementById("select").firstChild, + 0 + ); + getTableEditor().insertTableRow(1, true); + is( + editor.innerHTML, + "<table><tbody>" + + "<tr><td>cell1-1</td><td>cell1-2</td></tr>\n" + + '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>' + + '<tr><td valign="top"><br></td><td valign="top"><br></td></tr>\n' + + "<tr><td>cell3-1</td><td>cell3-2</td></tr>" + + "</tbody></table>", + "testInsertAfterRowFollowedTextNode: nsITableEditor.insertTableRow(1, true) should insert a row above the second row"); + is( + beforeInputEvents.length, + 1, + 'testInsertAfterRowFollowedTextNode: Only one "beforeinput" event should be fired' + ); + checkInputEvent( + beforeInputEvents[0], + "when selection is collapsed in a cell whose row follows a text node (testInsertAfterRowFollowedTextNode)" + ); + is( + inputEvents.length, + 1, + 'testInsertAfterRowFollowedTextNode: Only one "input" event should be fired' + ); + checkInputEvent( + inputEvents[0], + "when selection is collapsed in a cell whose row follows a text node (testInsertAfterRowFollowedTextNode)" + ); + })(); + + (function testInsertAfterLastRow() { + selection.removeAllRanges(); + editor.innerHTML = + "<table>" + + "<tr><td>cell1-1</td><td>cell1-2</td></tr>" + + '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>' + + "</table>"; + editor.focus(); + beforeInputEvents = []; + inputEvents = []; + selection.collapse(document.getElementById("select").firstChild, 0); + getTableEditor().insertTableRow(1, true); + is( + editor.innerHTML, + "<table><tbody>" + + "<tr><td>cell1-1</td><td>cell1-2</td></tr>" + + '<tr><td id="select">cell2-1</td><td>cell2-2</td></tr>' + + '<tr><td valign="top"><br></td><td valign="top"><br></td></tr>' + + "</tbody></table>", + "testInsertAfterLastRow: nsITableEditor.insertTableRow(1, true) should insert a row after the last row" + ); + is( + beforeInputEvents.length, + 1, + 'testInsertAfterLastRow: Only one "beforeinput" event should be fired' + ); + checkInputEvent( + beforeInputEvents[0], + "when selection is collapsed in a cell whose row follows a text node (testInsertAfterLastRow)" + ); + is( + inputEvents.length, + 1, + 'testInsertAfterLastRow: Only one "input" event should be fired' + ); + checkInputEvent( + inputEvents[0], + "when selection is collapsed in a cell whose row follows a text node (testInsertAfterLastRow)" + ); + })(); + + editor.removeEventListener("beforeinput", onBeforeInput); + editor.removeEventListener("input", onInput); + + SimpleTest.finish(); +}); + +function getTableEditor() { + var editingSession = SpecialPowers.wrap(window).docShell.editingSession; + return editingSession.getEditorForWindow(window).QueryInterface(SpecialPowers.Ci.nsITableEditor); +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_password_input_with_unmasked_range.html b/editor/libeditor/tests/test_password_input_with_unmasked_range.html new file mode 100644 index 0000000000..205884219b --- /dev/null +++ b/editor/libeditor/tests/test_password_input_with_unmasked_range.html @@ -0,0 +1,417 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for inputting new text while there is unmasked range</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<input type="password" value="012345"> + +<script> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(async () => { + let input = document.getElementsByTagName("input")[0]; + let editor = SpecialPowers.wrap(input).editor; + + async function doTest(aDelay) { + await SpecialPowers.pushPrefEnv({ + set: [["editor.password.mask_delay", aDelay]], + }); + input.focus(); + input.value = "012345"; + + // Setting value + editor.unmask(3, 4); + input.value = "abcdef"; + is(editor.unmaskedStart, 0, + "delay=" + aDelay + ": Setting value should mask all characters (start)"); + is(editor.unmaskedEnd, 6, + "delay=" + aDelay + ": Setting value should mask all characters (end)"); + ok(!editor.autoMaskingEnabled, + "delay=" + aDelay + ": auto masking shouldn't be enabled after setting value"); + editor.unmask(3, 4); + input.value = "abcdef"; + is(editor.unmaskedStart, 3, + "delay=" + aDelay + ": Setting same value shouldn't change unmasked range (start)"); + is(editor.unmaskedEnd, 4, + "delay=" + aDelay + ": Setting same value shouldn't change unmasked range (end)"); + ok(!editor.autoMaskingEnabled, + "delay=" + aDelay + ": auto masking shouldn't be enabled after setting same value"); + input.value = ""; + is(editor.unmaskedStart, 0, + "delay=" + aDelay + ": Setting empty value should collapse unmasked range (start)"); + is(editor.unmaskedEnd, 0, + "delay=" + aDelay + ": Setting empty value should collapse unmasked range (end)"); + ok(!editor.autoMaskingEnabled, + "delay=" + aDelay + ": auto masking shouldn't be enabled after setting empty value"); + input.value = "abcdef"; + is(editor.unmaskedStart, 0, + "delay=" + aDelay + ": Setting empty field to new value should unmask all (start)"); + is(editor.unmaskedEnd, 6, + "delay=" + aDelay + ": Setting empty field to new value should unmask all (end)"); + ok(!editor.autoMaskingEnabled, + "delay=" + aDelay + ": auto masking shouldn't be enabled after setting empty field to new value"); + + // Simply typing a character + editor.unmask(3, 4); + input.setSelectionRange(3, 3); + synthesizeKey("1"); + is(editor.unmaskedStart, 3, + "delay=" + aDelay + ": Inserting character at start of unmasked range should expand the range (start)"); + is(editor.unmaskedEnd, 5, + "delay=" + aDelay + ": Inserting character at start of unmasked range should expand the range (end)"); + is(input.value, "abc1def", + "delay=" + aDelay + ": Inserting character at start of unmasked range"); + ok(!editor.autoMaskingEnabled, + "delay=" + aDelay + ": auto masking shouldn't be enabled after inserting character at start of unmasked range"); + + editor.unmask(3, 5); + synthesizeKey("2"); + is(editor.unmaskedStart, 3, + "delay=" + aDelay + ": Inserting character in the unmasked range should expand the range (start)"); + is(editor.unmaskedEnd, 6, + "delay=" + aDelay + ": Inserting character in the unmasked range should expand the range (end)"); + is(input.value, "abc12def", + "delay=" + aDelay + ": Inserting character in the unmasked range"); + ok(!editor.autoMaskingEnabled, + "delay=" + aDelay + ": auto masking shouldn't be enabled after inserting character in the unmasked range"); + + editor.unmask(3, 6); + input.setSelectionRange(6, 6); + synthesizeKey("3"); + is(editor.unmaskedStart, 3, + "delay=" + aDelay + ": Inserting character at end of unmasked range should expand the range (start)"); + is(editor.unmaskedEnd, 7, + "delay=" + aDelay + ": Inserting character at end of unmasked range should expand the range (end)"); + is(input.value, "abc12d3ef", + "delay=" + aDelay + ": Inserting character at end of unmasked range"); + ok(!editor.autoMaskingEnabled, + "delay=" + aDelay + ": auto masking shouldn't be enabled after inserting character at end of unmasked range"); + + editor.unmask(3, 4); + input.setSelectionRange(2, 2); + synthesizeKey("4"); + is(editor.unmaskedStart, 2, + "delay=" + aDelay + ": Inserting character before the unmasked range should expand the range (start)"); + is(editor.unmaskedEnd, 5, + "delay=" + aDelay + ": Inserting character before the unmasked range should expand the range (end)"); + is(input.value, "ab4c12d3ef", + "delay=" + aDelay + ": Inserting character before the unmasked range"); + ok(!editor.autoMaskingEnabled, + "delay=" + aDelay + ": auto masking shouldn't be enabled after inserting character before the unmasked range"); + + editor.unmask(2, 5); + input.setSelectionRange(6, 6); + synthesizeKey("5"); + is(editor.unmaskedStart, 2, + "delay=" + aDelay + ": Inserting character after the unmasked range should expand the range (start)"); + is(editor.unmaskedEnd, 7, + "delay=" + aDelay + ": Inserting character after the unmasked range should expand the range (end)"); + is(input.value, "ab4c125d3ef", + "delay=" + aDelay + ": Inserting character after the unmasked range"); + ok(!editor.autoMaskingEnabled, + "delay=" + aDelay + ": auto masking shouldn't be enabled after inserting character after the unmasked range"); + + // Simply removing characters + input.value = "abcdefgh"; + editor.unmask(3, 6); + input.setSelectionRange(3, 3); + editor.deleteSelection(editor.eNext, editor.eStrip); + is(editor.unmaskedStart, 3, + "delay=" + aDelay + ": Removing first character of unmasked range should shrink the range (start)"); + is(editor.unmaskedEnd, 5, + "delay=" + aDelay + ": Removing first character of unmasked range should shrink the range (end)"); + is(input.value, "abcefgh", + "delay=" + aDelay + ": Removing first character of unmasked range"); + ok(!editor.autoMaskingEnabled, + "delay=" + aDelay + ": auto masking shouldn't be enabled after removing first character of unmasked range"); + + editor.unmask(3, 5); + editor.deleteSelection(editor.ePrevious, editor.eStrip); + is(editor.unmaskedStart, 2, + "delay=" + aDelay + ": Removing previous character of unmasked range should move the range (start)"); + is(editor.unmaskedEnd, 4, + "delay=" + aDelay + ": Removing previous character of unmasked range should move the range (end)"); + is(input.value, "abefgh", + "delay=" + aDelay + ": Removing previous character of unmasked range"); + ok(!editor.autoMaskingEnabled, + "delay=" + aDelay + ": auto masking shouldn't be enabled after removing previous character of unmasked range"); + + input.value = "abcdefgh"; + editor.unmask(3, 6); + input.setSelectionRange(6, 6); + editor.deleteSelection(editor.ePrevious, editor.eStrip); + is(editor.unmaskedStart, 3, + "delay=" + aDelay + ": Removing last character of unmasked range should shrink the range (start)"); + is(editor.unmaskedEnd, 5, + "delay=" + aDelay + ": Removing last character of unmasked range should shrink the range (end)"); + ok(!editor.autoMaskingEnabled, + "delay=" + aDelay + ": auto masking shouldn't be enabled after removing last character of unmasked range should shrink the range"); + + editor.unmask(3, 5); + editor.deleteSelection(editor.eNext, editor.eStrip); + is(editor.unmaskedStart, 3, + "delay=" + aDelay + ": Removing next character of unmasked range shouldn't change the range (start)"); + is(editor.unmaskedEnd, 5, + "delay=" + aDelay + ": Removing next character of unmasked range shouldn't change the range (end)"); + ok(!editor.autoMaskingEnabled, + "delay=" + aDelay + ": auto masking shouldn't be enabled after removing next character of unmasked range shouldn't change the range"); + + input.value = "abcdef"; + editor.unmask(3, 6); + input.setSelectionRange(4, 4); + editor.deleteSelection(editor.eNext, editor.eStrip); + is(editor.unmaskedStart, 3, + "delay=" + aDelay + ": Removing middle character of unmasked range should shrink the range (start)"); + is(editor.unmaskedEnd, 5, + "delay=" + aDelay + ": Removing middle character of unmasked range should shrink the range (end)"); + ok(!editor.autoMaskingEnabled, + "delay=" + aDelay + ": auto masking shouldn't be enabled after removing middle character of unmasked range should shrink the range"); + + // Removing selection + input.value = "abcdefgh"; + editor.unmask(3, 6); + input.setSelectionRange(3, 4); + editor.deleteSelection(editor.eNone, editor.eStrip); + is(editor.unmaskedStart, 3, + "delay=" + aDelay + ": Removing selected first character of unmasked range should shrink the range (start)"); + is(editor.unmaskedEnd, 5, + "delay=" + aDelay + ": Removing selected first character of unmasked range should shrink the range (end)"); + is(input.value, "abcefgh", + "delay=" + aDelay + ": Removing selected first character of unmasked range"); + ok(!editor.autoMaskingEnabled, + "delay=" + aDelay + ": auto masking shouldn't be enabled after removing selected first character of unmasked range"); + + input.value = "abcdefgh"; + editor.unmask(3, 6); + input.setSelectionRange(2, 3); + editor.deleteSelection(editor.eNone, editor.eStrip); + is(editor.unmaskedStart, 2, + "delay=" + aDelay + ": Removing selected previous character of unmasked range should move the range (start)"); + is(editor.unmaskedEnd, 5, + "delay=" + aDelay + ": Removing selected previous character of unmasked range should move the range (end)"); + is(input.value, "abdefgh", + "delay=" + aDelay + ": Removing selected previous character of unmasked range"); + ok(!editor.autoMaskingEnabled, + "delay=" + aDelay + ": auto masking shouldn't be enabled after removing selected previous character of unmasked range"); + + input.value = "abcdefgh"; + editor.unmask(3, 6); + input.setSelectionRange(5, 6); + editor.deleteSelection(editor.eNone, editor.eStrip); + is(editor.unmaskedStart, 3, + "delay=" + aDelay + ": Removing selected last character of unmasked range should shrink the range (start)"); + is(editor.unmaskedEnd, 5, + "delay=" + aDelay + ": Removing selected last character of unmasked range should shrink the range (end)"); + is(input.value, "abcdegh", + "delay=" + aDelay + ": Removing selected last character of unmasked range"); + ok(!editor.autoMaskingEnabled, + "delay=" + aDelay + ": auto masking shouldn't be enabled after removing selected last character of unmasked range"); + + input.value = "abcdefgh"; + editor.unmask(3, 6); + input.setSelectionRange(6, 7); + editor.deleteSelection(editor.eNone, editor.eStrip); + is(editor.unmaskedStart, 3, + "delay=" + aDelay + ": Removing selected next character of unmasked range should keep the range (start)"); + is(editor.unmaskedEnd, 6, + "delay=" + aDelay + ": Removing selected next character of unmasked range should keep the range (end)"); + is(input.value, "abcdefh", + "delay=" + aDelay + ": Removing selected next character of unmasked range"); + ok(!editor.autoMaskingEnabled, + "delay=" + aDelay + ": auto masking shouldn't be enabled after removing selected next character of unmasked range"); + + input.value = "abcdefgh"; + editor.unmask(3, 6); + input.setSelectionRange(4, 5); + editor.deleteSelection(editor.eNone, editor.eStrip); + is(editor.unmaskedStart, 3, + "delay=" + aDelay + ": Removing selected middle character of unmasked range should shrink the range (start)"); + is(editor.unmaskedEnd, 5, + "delay=" + aDelay + ": Removing selected middle character of unmasked range should shrink the range (end)"); + is(input.value, "abcdfgh", + "delay=" + aDelay + ": Removing selected middle character of unmasked range"); + ok(!editor.autoMaskingEnabled, + "delay=" + aDelay + ": auto masking shouldn't be enabled after removing selected middle character of unmasked range"); + + // Replacing a character + input.value = "abcdefgh"; + editor.unmask(3, 6); + input.setSelectionRange(3, 4); + synthesizeKey("0"); + is(editor.unmaskedStart, 3, + "delay=" + aDelay + ": Replacing first character of unmasked range should keep the range (start)"); + is(editor.unmaskedEnd, 6, + "delay=" + aDelay + ": Replacing first character of unmasked range should keep the range (end)"); + is(input.value, "abc0efgh", + "delay=" + aDelay + ": Replacing first character of unmasked range"); + ok(!editor.autoMaskingEnabled, + "delay=" + aDelay + ": auto masking shouldn't be enabled after replacing first character of unmasked range"); + + input.value = "abcdefgh"; + editor.unmask(3, 6); + input.setSelectionRange(2, 3); + synthesizeKey("0"); + is(editor.unmaskedStart, 2, + "delay=" + aDelay + ": Replacing previous character of unmasked range should expand the range (start)"); + is(editor.unmaskedEnd, 6, + "delay=" + aDelay + ": Replacing previous character of unmasked range should expand the range (end)"); + is(input.value, "ab0defgh", + "delay=" + aDelay + ": Replacing previous character of unmasked range"); + ok(!editor.autoMaskingEnabled, + "delay=" + aDelay + ": auto masking shouldn't be enabled after replacing previous character of unmasked range"); + + input.value = "abcdefgh"; + editor.unmask(3, 6); + input.setSelectionRange(5, 6); + synthesizeKey("0"); + is(editor.unmaskedStart, 3, + "delay=" + aDelay + ": Replacing last character of unmasked range should keep the range (start)"); + is(editor.unmaskedEnd, 6, + "delay=" + aDelay + ": Replacing last character of unmasked range should keep the range (end)"); + is(input.value, "abcde0gh", + "delay=" + aDelay + ": Replacing last character of unmasked range"); + ok(!editor.autoMaskingEnabled, + "delay=" + aDelay + ": auto masking shouldn't be enabled after replacing last character of unmasked range"); + + input.value = "abcdefgh"; + editor.unmask(3, 6); + input.setSelectionRange(6, 7); + synthesizeKey("0"); + is(editor.unmaskedStart, 3, + "delay=" + aDelay + ": Replacing next character of unmasked range should expand the range (start)"); + is(editor.unmaskedEnd, 7, + "delay=" + aDelay + ": Replacing next character of unmasked range should expand the range (end)"); + is(input.value, "abcdef0h", + "delay=" + aDelay + ": Replacing next character of unmasked range"); + ok(!editor.autoMaskingEnabled, + "delay=" + aDelay + ": auto masking shouldn't be enabled after replacing next character of unmasked range"); + + input.value = "abcdefgh"; + editor.unmask(3, 6); + input.setSelectionRange(4, 5); + synthesizeKey("0"); + is(editor.unmaskedStart, 3, + "delay=" + aDelay + ": Replacing middle character of unmasked range should keep the range (start)"); + is(editor.unmaskedEnd, 6, + "delay=" + aDelay + ": Replacing middle character of unmasked range should keep the range (end)"); + is(input.value, "abcd0fgh", + "delay=" + aDelay + ": Replacing middle character of unmasked range"); + ok(!editor.autoMaskingEnabled, + "delay=" + aDelay + ": auto masking shouldn't be enabled after replacing middle character of unmasked range"); + + // Replace part of the range + input.value = "abcdefgh"; + editor.unmask(3, 6); + input.setSelectionRange(1, 4); + synthesizeKey("0"); + is(editor.unmaskedStart, 1, + "delay=" + aDelay + ": Replacing start edge of unmasked range should move and shrink the range (start) #1"); + is(editor.unmaskedEnd, 4, + "delay=" + aDelay + ": Replacing start edge of unmasked range should move and shrink the range (end) #1"); + is(input.value, "a0efgh", + "delay=" + aDelay + ": Replacing start edge of unmasked range #1"); + ok(!editor.autoMaskingEnabled, + "delay=" + aDelay + ": auto masking shouldn't be enabled after replacing start edge of unmasked range #1"); + + input.value = "abcdefgh"; + editor.unmask(3, 6); + input.setSelectionRange(2, 5); + synthesizeKey("0"); + is(editor.unmaskedStart, 2, + "delay=" + aDelay + ": Replacing start edge of unmasked range should move and shrink the range (start) #2"); + is(editor.unmaskedEnd, 4, + "delay=" + aDelay + ": Replacing start edge of unmasked range should move and shrink the range (end) #2"); + is(input.value, "ab0fgh", + "delay=" + aDelay + ": Replacing start edge of unmasked range #2"); + ok(!editor.autoMaskingEnabled, + "delay=" + aDelay + ": auto masking shouldn't be enabled after replacing start edge of unmasked range #2"); + + input.value = "abcdefgh"; + editor.unmask(3, 6); + input.setSelectionRange(4, 7); + synthesizeKey("0"); + is(editor.unmaskedStart, 3, + "delay=" + aDelay + ": Replacing end edge of unmasked range should shrink the range (start) #1"); + is(editor.unmaskedEnd, 5, + "delay=" + aDelay + ": Replacing end edge of unmasked range should shrink the range (end) #1"); + is(input.value, "abcd0h", + "delay=" + aDelay + ": Replacing end edge of unmasked range #1"); + ok(!editor.autoMaskingEnabled, + "delay=" + aDelay + ": auto masking shouldn't be enabled after replacing end edge of unmasked range #1"); + + input.value = "abcdefghi"; + editor.unmask(3, 6); + input.setSelectionRange(5, 8); + synthesizeKey("0"); + is(editor.unmaskedStart, 3, + "delay=" + aDelay + ": Replacing end edge of unmasked range should shrink the range (start) #2"); + is(editor.unmaskedEnd, 6, + "delay=" + aDelay + ": Replacing end edge of unmasked range should shrink the range (end) #2"); + is(input.value, "abcde0i", + "delay=" + aDelay + ": Replacing end edge of unmasked range #2"); + ok(!editor.autoMaskingEnabled, + "delay=" + aDelay + ": auto masking shouldn't be enabled after replacing end edge of unmasked range #2"); + + // Replaceing all of the range + input.value = "abcdefgh"; + editor.unmask(3, 6); + input.setSelectionRange(3, 6); + synthesizeKey("0"); + is(editor.unmaskedStart, 3, + "delay=" + aDelay + ": Replacing all of unmasked range should shrink the range (start) #1"); + is(editor.unmaskedEnd, 4, + "delay=" + aDelay + ": Replacing all of unmasked range should shrink the range (end) #1"); + is(input.value, "abc0gh", + "delay=" + aDelay + ": Replacing all of unmasked range #1"); + ok(!editor.autoMaskingEnabled, + "delay=" + aDelay + ": auto masking shouldn't be enabled after replacing all of unmasked range #1"); + + input.value = "abcdefgh"; + editor.unmask(3, 6); + input.setSelectionRange(2, 7); + synthesizeKey("0"); + is(editor.unmaskedStart, 2, + "delay=" + aDelay + ": Replacing all of unmasked range should shrink the range (start) #2"); + is(editor.unmaskedEnd, 3, + "delay=" + aDelay + ": Replacing all of unmasked range should shrink the range (end) #2"); + is(input.value, "ab0h", + "delay=" + aDelay + ": Replacing all of unmasked range #2"); + ok(!editor.autoMaskingEnabled, + "delay=" + aDelay + ": auto masking shouldn't be enabled after replacing all of unmasked range #2"); + + // Removing all characters and type new character + input.value = "abcdefgh"; + editor.unmask(3, 6); + input.setSelectionRange(0, 8); + editor.deleteSelection(editor.eNone, editor.eStrip); + is(editor.unmaskedStart, 0, + "delay=" + aDelay + ": Removing all characters should shrink the range (start)"); + is(editor.unmaskedEnd, 0, + "delay=" + aDelay + ": Removing all characters should shrink the range (end)"); + is(input.value, "", + "delay=" + aDelay + ": Removing all characters"); + ok(!editor.autoMaskingEnabled, + "delay=" + aDelay + ": auto masking shouldn't be enabled after removing all characters"); + synthesizeKey("0"); + is(editor.unmaskedStart, 0, + "delay=" + aDelay + ": Typing first character should expand unmasked range (start)"); + is(editor.unmaskedEnd, 1, + "delay=" + aDelay + ": Typing first character should expand unmasked range (end)"); + is(input.value, "0", + "delay=" + aDelay + ": Typing first character"); + ok(!editor.autoMaskingEnabled, + "delay=" + aDelay + ": auto masking shouldn't be enabled after typing first character"); + } + + await doTest(0); + await doTest(600); + + SimpleTest.finish(); +}); +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_password_paste.html b/editor/libeditor/tests/test_password_paste.html new file mode 100644 index 0000000000..817de9b277 --- /dev/null +++ b/editor/libeditor/tests/test_password_paste.html @@ -0,0 +1,65 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for masking password</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/WindowSnapshot.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none;"> + +</div> + +<input type="password" id="input1" value="abcdef"> + +<pre id="test"> +<script class="testbody" type="application/javascript"> +function getEditor() { + return SpecialPowers.wrap(document.getElementById("input1")).editor; +} + +function getLoadContext() { + return SpecialPowers.wrap(window).docShell.QueryInterface( + SpecialPowers.Ci.nsILoadContext); +} + +function pasteText(str) { + const Cc = SpecialPowers.Cc; + const Ci = SpecialPowers.Ci; + let trans = Cc["@mozilla.org/widget/transferable;1"]. + createInstance(Ci.nsITransferable); + trans.init(getLoadContext()); + let s = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + s.data = str; + trans.setTransferData("text/plain", s); + let inputEvent = null; + window.addEventListener("input", aEvent => { inputEvent = aEvent; }, {once: true}); + getEditor().pasteTransferable(trans); + is(inputEvent.type, "input", "input event should be fired"); + is(inputEvent.inputType, "insertFromPaste", "inputType should be insertFromPaste"); + is(inputEvent.data, str, `data should be "${str}"`); + is(inputEvent.dataTransfer, null, "dataTransfer should be null on password field"); +} + +SimpleTest.waitForFocus(async () => { + let input1 = document.getElementById("input1"); + input1.focus(); + let reference = snapshotWindow(window, false); + + // Bug 1501376 - Password should be masked immediately when pasting text + input1.value = ""; + pasteText("abcdef"); + assertSnapshots(reference, snapshotWindow(window), true, null, + "Password should be masked immediately when pasting text", + "reference is masked"); + SimpleTest.finish(); +}); + +SimpleTest.waitForExplicitFinish(); +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_password_per_word_operation.html b/editor/libeditor/tests/test_password_per_word_operation.html new file mode 100644 index 0000000000..847f2e9854 --- /dev/null +++ b/editor/libeditor/tests/test_password_per_word_operation.html @@ -0,0 +1,148 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for operations in a password field</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<input type="password" value="abcdef ghijk" size="50"> +<script> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(async () => { + await SpecialPowers.pushPrefEnv({ + set: [["layout.word_select.eat_space_to_next_word", false]], + }); + // Double click on the anonymous text node + let input = document.getElementsByTagName("input")[0]; + let editor = SpecialPowers.wrap(input).editor; + let anonymousDiv = editor.rootElement; + input.select(); + const kTextNodeRectInAnonymousDiv = { + left: editor.selection.getRangeAt(0).getBoundingClientRect().left - anonymousDiv.getBoundingClientRect().left, + top: editor.selection.getRangeAt(0).getBoundingClientRect().top - anonymousDiv.getBoundingClientRect().top, + width: editor.selection.getRangeAt(0).getBoundingClientRect().width, + height: editor.selection.getRangeAt(0).getBoundingClientRect().height, + }; + kTextNodeRectInAnonymousDiv.right = kTextNodeRectInAnonymousDiv.left + kTextNodeRectInAnonymousDiv.width; + kTextNodeRectInAnonymousDiv.bottom = kTextNodeRectInAnonymousDiv.top + kTextNodeRectInAnonymousDiv.height; + input.setSelectionRange(0, 0); + const kHalfHeightOfAnonymousDiv = anonymousDiv.getBoundingClientRect().height / 2; + synthesizeMouse(anonymousDiv, kTextNodeRectInAnonymousDiv.left + 5, kHalfHeightOfAnonymousDiv, {button: 0, clickCount: 1}); + synthesizeMouse(anonymousDiv, kTextNodeRectInAnonymousDiv.left + 5, kHalfHeightOfAnonymousDiv, {button: 0, clickCount: 2}); + is(input.selectionStart, 0, + "Double clicking on the anonymous text node in a password field should select all"); + is(input.selectionEnd, input.value.length, + "Double clicking on the anonymous text node in a password field should select all"); + + // Double click on the anonymous div element + synthesizeMouse(anonymousDiv, kTextNodeRectInAnonymousDiv.right + 3, kHalfHeightOfAnonymousDiv, {button: 0, clickCount: 1}); + synthesizeMouse(anonymousDiv, kTextNodeRectInAnonymousDiv.right + 3, kHalfHeightOfAnonymousDiv, {button: 0, clickCount: 2}); + is(input.selectionStart, 0, + "Double clicking on the anonymous div element in a password field should select all"); + is(input.selectionEnd, input.value.length, + "Double clicking on the anonymous div element in a password field should select all"); + + // Move caret per word + let selectionController = editor.selectionController; + input.focus(); + input.setSelectionRange(12, 12); + selectionController.wordMove(false, false); + is(input.selectionStart, 0, + "Moving caret one word from the end should move caret to the start"); + input.setSelectionRange(0, 0); + selectionController.wordMove(true, false); + is(input.selectionStart, 12, + "Moving caret one word from the start should move caret to the end"); + + // Expand selection per word + input.setSelectionRange(12, 12); + selectionController.wordMove(false, true); + is(input.selectionStart, 0, + "Selecting one word from the end should move selection start to the start"); + input.setSelectionRange(0, 0); + selectionController.wordMove(true, true); + is(input.selectionEnd, 12, + "Selecting one word from the start should move selection end to the end"); + + // Delete one word + input.setSelectionRange(12, 12); + editor.deleteSelection(editor.ePreviousWord, editor.eStrip); + is(input.value, "", + "Deleting one word from the end should delete all characters"); + input.value = "abcdef ghijk"; + document.documentElement.scrollTop; // Flush frames for setting the value. + input.setSelectionRange(0, 0); + editor.deleteSelection(editor.eNextWord, editor.eStrip); + is(input.value, "", + "Deleting one word from the start should delete all characters"); + input.value = "abcdef ghijk"; + document.documentElement.scrollTop; // Flush frames for setting the value. + + // Test same things when the space is unmasked. + + // Double click on the anonymous text node + editor.unmask(6, 7); + synthesizeMouse(anonymousDiv, kTextNodeRectInAnonymousDiv.left + 3, kHalfHeightOfAnonymousDiv, {button: 0, clickCount: 1}); + synthesizeMouse(anonymousDiv, kTextNodeRectInAnonymousDiv.left + 3, kHalfHeightOfAnonymousDiv, {button: 0, clickCount: 2}); + is(input.selectionStart, 0, + "Double clicking on the first word should select it"); + is(input.selectionEnd, 6, + "Double clicking on the first word should select it"); + + // Double click on the anonymous div element + editor.unmask(6, 7); + synthesizeMouse(anonymousDiv, kTextNodeRectInAnonymousDiv.right + 3, kHalfHeightOfAnonymousDiv, {button: 0, clickCount: 1}); + synthesizeMouse(anonymousDiv, kTextNodeRectInAnonymousDiv.right + 3, kHalfHeightOfAnonymousDiv, {button: 0, clickCount: 2}); + is(input.selectionStart, 7, + "Double clicking on the anonymous div element in a password field should select the last word"); + is(input.selectionEnd, input.value.length, + "Double clicking on the anonymous div element in a password field should select the last word"); + + // Move caret per word + input.focus(); + input.setSelectionRange(12, 12); + editor.unmask(6, 7); + selectionController.wordMove(false, false); + is(input.selectionStart, 6, + "Moving caret one word from the end should move caret to end of the first word"); + input.setSelectionRange(0, 0); + editor.unmask(6, 7); + selectionController.wordMove(true, false); + is(input.selectionStart, 7, + "Moving caret one word from the start should move caret to start of the last word"); + + // Expand selection per word + input.setSelectionRange(12, 12); + editor.unmask(6, 7); + selectionController.wordMove(false, true); + is(input.selectionStart, 6, + "Selecting one word from the end should move selection start to end of the first word"); + input.setSelectionRange(0, 0); + editor.unmask(6, 7); + selectionController.wordMove(true, true); + is(input.selectionEnd, 7, + "Selecting one word from the start should move selection end to start of the last word"); + + // Delete one word + input.setSelectionRange(12, 12); + editor.unmask(6, 7); + editor.deleteSelection(editor.ePreviousWord, editor.eStrip); + is(input.value, "abcdef", + "Deleting one word from the end should delete the last word"); + input.value = "abcdef ghijk"; + document.documentElement.scrollTop; // Flush frames for setting the value. + input.setSelectionRange(0, 0); + editor.unmask(6, 7); + editor.deleteSelection(editor.eNextWord, editor.eStrip); + is(input.value, "ghijk", + "Deleting one word from the start should delete the first word"); + input.value = "abcdef ghijk"; + document.documentElement.scrollTop; // Flush frames for setting the value. + + SimpleTest.finish(); +}); +</script> +</body> +</html> diff --git a/editor/libeditor/tests/test_password_unmask_API.html b/editor/libeditor/tests/test_password_unmask_API.html new file mode 100644 index 0000000000..0fc7ef8194 --- /dev/null +++ b/editor/libeditor/tests/test_password_unmask_API.html @@ -0,0 +1,318 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for unmasking password API</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<input type="text"> +<input type="password"> +<script class="testbody" type="application/javascript"> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + let input = document.getElementsByTagName("input")[0]; + let password = document.getElementsByTagName("input")[1]; + + let editor, passwordEditor; + function updateEditors() { + editor = SpecialPowers.wrap(input).editor; + passwordEditor = SpecialPowers.wrap(password).editor; + } + + try { + updateEditors(); + editor.mask(); + ok(false, + `nsIEditor.mask() should throw exception when called for <input type="text"> before nsIEditor.unmask()`); + } catch (e) { + ok(true, + `nsIEditor.mask() should throw exception when called for <input type="text"> before nsIEditor.unmask() ${e}`); + } + + try { + updateEditors(); + editor.unmask(); + ok(false, + `nsIEditor.unmask() should throw exception when called for <input type="text">`); + } catch (e) { + ok(true, + `nsIEditor.unmask() should throw exception when called for <input type="text"> ${e}`); + } + + try { + updateEditors(); + editor.unmask(0); + ok(false, + `nsIEditor.unmask(0) should throw exception when called for <input type="text">`); + } catch (e) { + ok(true, + `nsIEditor.unmask(0) should throw exception when called for <input type="text"> ${e}`); + } + + input.value = "abcdef"; + try { + updateEditors(); + editor.unmask(); + ok(false, + `nsIEditor.unmask() should throw exception when called for <input type="text" value="abcdef">`); + } catch (e) { + ok(true, + `nsIEditor.unmask() should throw exception when called for <input type="text" value="abcdef"> ${e}`); + } + + try { + updateEditors(); + editor.mask(); + ok(false, + `nsIEditor.mask() should throw exception when called for <input type="text" value="abcdef"> after nsIEditor.unmask()`); + } catch (e) { + ok(true, + `nsIEditor.mask() should throw exception when called for <input type="text" value="abcdef"> after nsIEditor.unmask() ${e}`); + } + + try { + updateEditors(); + passwordEditor.mask(); + ok(true, + `nsIEditor.mask() shouldn't throw exception when called for <input type="password"> before nsIEditor.unmask()`); + ok(passwordEditor.autoMaskingEnabled, + `nsIEditor.autoMaskingEnabled should be true after nsIEditor.mask() for <input type="password"> before nsIEditor.unmask()`); + } catch (e) { + ok(false, + `nsIEditor.mask() shouldn't throw exception when called for <input type="password"> before nsIEditor.unmask() ${e}`); + } + + try { + updateEditors(); + editor.unmask(5); + ok(false, + `nsIEditor.unmask(5) should throw exception when called for <input type="password" value="">`); + } catch (e) { + ok(true, + `nsIEditor.unmask(5) should throw exception when called for <input type="password" value=""> ${e}`); + ok(passwordEditor.autoMaskingEnabled, + `nsIEditor.autoMaskingEnabled should keep true (<input type="password">)`); + } + + try { + updateEditors(); + passwordEditor.unmask(); + ok(true, + `nsIEditor.unmask() shouldn't throw exception when called for <input type="password">`); + ok(!passwordEditor.autoMaskingEnabled, + `nsIEditor.autoMaskingEnabled should be false after nsIEditor.unmask() for <input type="password">)`); + is(passwordEditor.unmaskedStart, 0, + `nsIEditor.unmaskedStart should be 0 after nsIEditor.unmask() for <input type="password">`); + is(passwordEditor.unmaskedEnd, 0, + `nsIEditor.unmaskedEnd should be 0 after nsIEditor.unmask() for <input type="password">`); + } catch (e) { + ok(false, + `nsIEditor.unmask() shouldn't throw exception when called for <input type="password"> ${e}`); + } + + password.value = "abcdef"; + try { + updateEditors(); + passwordEditor.unmask(); + ok(true, + `nsIEditor.unmask() shouldn't throw exception when called for <input type="password" value="abcdef">)`); + ok(!passwordEditor.autoMaskingEnabled, + `nsIEditor.autoMaskingEnabled should be false after nsIEditor.unmask() for <input type="password" value="abcdef">`); + is(passwordEditor.unmaskedStart, 0, + `nsIEditor.unmaskedStart should be 0 after nsIEditor.unmask() for <input type="password" value="abcdef">`); + is(passwordEditor.unmaskedEnd, 6, + `nsIEditor.unmaskedEnd should be 0 after nsIEditor.unmask() for <input type="password" value="abcdef">`); + } catch (e) { + ok(false, + `nsIEditor.unmask() shouldn't throw exception when called for <input type="password" value="abcdef"> ${e}`); + } + + try { + updateEditors(); + passwordEditor.mask(); + ok(true, + `nsIEditor.mask() shouldn't throw exception when called for <input type="password" value="abcdef">`); + ok(passwordEditor.autoMaskingEnabled, + `nsIEditor.autoMaskingEnabled should be true after nsIEditor.mask() for <input type="password" value="abcdef">`); + } catch (e) { + ok(false, + `nsIEditor.mask() shouldn't throw exception when called for <input type="password" value="abcdef"> ${e}`); + } + + try { + updateEditors(); + passwordEditor.unmask(0, 100, 1000); + ok(true, + `nsIEditor.unmask(0, 100, 1000) shouldn't throw exception when called for <input type="password" value="abcdef">`); + ok(passwordEditor.autoMaskingEnabled, + `nsIEditor.autoMaskingEnabled should be true after nsIEditor.unmask(0, 100, 1000) for <input type="password" value="abcdef">`); + is(passwordEditor.unmaskedStart, 0, + `nsIEditor.unmaskedStart should be 0 after nsIEditor.unmask(0, 100, 1000) for <input type="password" value="abcdef">`); + is(passwordEditor.unmaskedEnd, 6, + `nsIEditor.unmaskedEnd should be 6 after nsIEditor.unmask(0, 100, 1000) for <input type="password" value="abcdef">`); + } catch (e) { + ok(false, + `nsIEditor.unmask(0, 100, 1000) shouldn't throw exception when called for <input type="password" value="abcdef"> ${e}`); + } + + try { + updateEditors(); + passwordEditor.unmask(3); + ok(true, + `nsIEditor.unmask(3) shouldn't throw exception when called for <input type="password" value="abcdef">`); + ok(!passwordEditor.autoMaskingEnabled, + `nsIEditor.autoMaskingEnabled should be false after nsIEditor.unmask(3) for <input type="password" value="abcdef">`); + is(passwordEditor.unmaskedStart, 3, + `nsIEditor.unmaskedStart should be 3 after nsIEditor.unmask(3) for <input type="password" value="abcdef">`); + is(passwordEditor.unmaskedEnd, 6, + `nsIEditor.unmaskedEnd should be 6 after nsIEditor.unmask(3) for <input type="password" value="abcdef">`); + } catch (e) { + ok(false, + `nsIEditor.unmask(3) shouldn't throw exception when called for <input type="password" value="abcdef"> ${e}`); + } + + try { + updateEditors(); + passwordEditor.unmask(0); + password.style.fontSize = "32px"; // reframe the `<input>` element + password.getBoundingClientRect(); // flush pending reflow if there is + // Then, new `TextEditor` should keep unmasked range. + passwordEditor = SpecialPowers.wrap(password).editor; + ok(!passwordEditor.autoMaskingEnabled, + `nsIEditor.autoMaskingEnabled should be false after the password field reframed`); + is(passwordEditor.unmaskedStart, 0, + `nsIEditor.unmaskedStart should be 0 after the password field reframed`); + is(passwordEditor.unmaskedEnd, 6, + `nsIEditor.unmaskedEnd should be 6 after the password field reframed`); + } catch (e) { + ok(false, `Shouldn't throw exception while testing unmasked range after reframing ${e}`); + } finally { + password.style.fontSize = ""; + password.getBoundingClientRect(); + } + + try { + updateEditors(); + passwordEditor.unmask(0); + password.style.display = "none"; // Hide the password field temporarily + password.getBoundingClientRect(); + password.style.display = "block"; // And show it again + password.getBoundingClientRect(); + updateEditors(); + // Then, new `TextEditor` should keep unmasked range. + ok(!passwordEditor.autoMaskingEnabled, + `nsIEditor.autoMaskingEnabled should be false after the password field was temporarily hidden`); + is(passwordEditor.unmaskedStart, 0, + `nsIEditor.unmaskedStart should be 0 after the password field was temporarily hidden`); + is(passwordEditor.unmaskedEnd, 6, + `nsIEditor.unmaskedEnd should be 6 after the password field was temporarily hidden`); + } catch (e) { + ok(false, `Shouldn't throw exception while testing unmasked range after temporarily hiding the password field ${e}`); + } finally { + password.style.display = ""; + password.getBoundingClientRect(); + passwordEditor = SpecialPowers.wrap(password).editor; + } + + try { + updateEditors(); + passwordEditor.unmask(0); + password.style.display = "none"; // Hide the password field temporarily + password.getBoundingClientRect(); + password.value = "ghijkl"; // And modify the value + password.style.display = "block"; // And show it again + password.getBoundingClientRect(); + // Then, new `TextEditor` shouldn't keep unmasked range due to the value change. + updateEditors(); + ok(passwordEditor.autoMaskingEnabled, + `nsIEditor.autoMaskingEnabled should be true after the password field was temporarily hidden and changed its value`); + is(passwordEditor.unmaskedStart, 0, + `nsIEditor.unmaskedStart should be 0 after the password field was temporarily hidden and changed its value`); + is(passwordEditor.unmaskedEnd, 0, + `nsIEditor.unmaskedEnd should be 0 after the password field was temporarily hidden and changed its value`); + } catch (e) { + ok(false, `Shouldn't throw exception while testing unmasked range after temporarily hiding the password field and changing the value ${e}`); + } finally { + password.style.display = ""; + password.getBoundingClientRect(); + password.value = "abcdef"; + passwordEditor = SpecialPowers.wrap(password).editor; + } + + try { + updateEditors(); + passwordEditor.unmask(0); + password.style.display = "none"; // Hide the password field temporarily + password.getBoundingClientRect(); + password.value = "abcdef"; // And overwrite the value with same value + password.style.display = "block"; // And show it again + password.getBoundingClientRect(); + // Then, new `TextEditor` shouldn't keep unmasked range due to setting the value. + updateEditors(); + ok(passwordEditor.autoMaskingEnabled, + `nsIEditor.autoMaskingEnabled should be true after the password field was temporarily hidden and changed its value`); + is(passwordEditor.unmaskedStart, 0, + `nsIEditor.unmaskedStart should be 0 after the password field was temporarily hidden and changed its value`); + is(passwordEditor.unmaskedEnd, 0, + `nsIEditor.unmaskedEnd should be 0 after the password field was temporarily hidden and changed its value`); + } catch (e) { + ok(false, `Shouldn't throw exception while testing unmasked range after temporarily hiding the password field and changing the value ${e}`); + } finally { + password.style.display = ""; + password.getBoundingClientRect(); + password.value = "abcdef"; + passwordEditor = SpecialPowers.wrap(password).editor; + } + + try { + updateEditors(); + passwordEditor.unmask(0, 6, 10000); + password.style.display = "none"; // Hide the password field temporarily + password.getBoundingClientRect(); + password.style.display = "block"; // And show it again + password.getBoundingClientRect(); + updateEditors(); + // Then, new `TextEditor` should mask all characters since nobody can mask it with the timer. + ok(passwordEditor.autoMaskingEnabled, + `nsIEditor.autoMaskingEnabled should be true after the password field was temporarily hidden (if auto-masking timer was set)`); + is(passwordEditor.unmaskedStart, 0, + `nsIEditor.unmaskedStart should be 0 after the password field was temporarily hidden (if auto-masking timer was set)`); + is(passwordEditor.unmaskedEnd, 0, + `nsIEditor.unmaskedEnd should be 0 after the password field was temporarily hidden (if auto-masking timer was set)`); + } catch (e) { + ok(false, `Shouldn't throw exception while testing unmasked range after temporarily hiding the password field whose auto-masking timer was set ${e}`); + } finally { + password.style.display = ""; + password.getBoundingClientRect(); + passwordEditor = SpecialPowers.wrap(password).editor; + } + + try { + updateEditors(); + passwordEditor.unmask(0); + password.type = "text"; + password.getBoundingClientRect(); + password.type = "password"; + password.getBoundingClientRect(); + updateEditors(); + // Then, new `TextEditor` should mask all characters after `type` attribute was changed. + ok(passwordEditor.autoMaskingEnabled, + `nsIEditor.autoMaskingEnabled should be true after "type" attribute of the password field was changed`); + is(passwordEditor.unmaskedStart, 0, + `nsIEditor.unmaskedStart should be 0 after "type" attribute of the password field was changed`); + is(passwordEditor.unmaskedEnd, 0, + `nsIEditor.unmaskedEnd should be 0 after "type" attribute of the password field was changed`); + } catch (e) { + ok(false, `Shouldn't throw exception while testing unmasked range after "type" attribute of the password field was changed ${e}`); + } finally { + password.type = "password"; + password.getBoundingClientRect(); + passwordEditor = SpecialPowers.wrap(password).editor; + } + + SimpleTest.finish(); +}); +</script> +</body> +</html> diff --git a/editor/libeditor/tests/test_pasteImgFromTransferable.html b/editor/libeditor/tests/test_pasteImgFromTransferable.html new file mode 100644 index 0000000000..a45bb5b574 --- /dev/null +++ b/editor/libeditor/tests/test_pasteImgFromTransferable.html @@ -0,0 +1,78 @@ +<!DOCTYPE html> +<html> +<head> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<body> +<div id="edit" contenteditable></div> + +<script> +const Cc = SpecialPowers.Cc; +const Ci = SpecialPowers.Ci; + +function getHTMLEditor(aWindow) { + let editingSession = SpecialPowers.wrap(aWindow).docShell.editingSession; + if (!editingSession) { + return null; + } + let editor = editingSession.getEditorForWindow(aWindow); + if (!editor) { + return null; + } + return editor.QueryInterface(Ci.nsIHTMLEditor); +} + +const TESTS = [ + { + mimeType: "image/gif", + base64: "R0lGODdhAQACAPABAAD/AP///ywAAAAAAQACAAACAkQKADs=" + }, + { + mimeType: "image/jpeg", + base64: "/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAP//////////////////////////////////////////////////////////////////////////////////////wgALCAABAAEBAREA/8QAFBABAAAAAAAAAAAAAAAAAAAAAP/aAAgBAQABPxA=" + }, + { + mimeType: "image/png", + base64: "iVBORw0KGgoAAAANSUhEUgAAAAYAAAAGCAIAAABvrngfAAAAFklEQVQImWMwjWhCQwxECoW3oCHihAB0LyYv5/oAHwAAAABJRU5ErkJggg==" + }, +]; + +add_task(async function() { + await new Promise(resolve => SimpleTest.waitForFocus(resolve, window)); + + let edit = document.getElementById("edit"); + edit.focus(); + + await new Promise(resolve => SimpleTest.executeSoon(resolve)); + + for (const test of TESTS) { + let bin = window.atob(test.base64); + let stringStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + stringStream.setData(bin, bin.length); + + let trans = Cc["@mozilla.org/widget/transferable;1"].createInstance( + Ci.nsITransferable + ); + trans.init(null); + trans.setTransferData(test.mimeType, stringStream); + + let evt = new Promise(resolve => + edit.addEventListener("input", resolve, {once: true})); + + getHTMLEditor(window).pasteTransferable(trans); + + await evt; + + is(edit.innerHTML, + "<img src=\"data:" + test.mimeType + ";base64," + test.base64 + "\" alt=\"\">", + "pastedTransferable pastes image as data URL"); + edit.innerHTML = ""; + } +}); +</script> +</body> +</html> diff --git a/editor/libeditor/tests/test_pasteImgTextarea.html b/editor/libeditor/tests/test_pasteImgTextarea.html new file mode 100644 index 0000000000..c19417e4c0 --- /dev/null +++ b/editor/libeditor/tests/test_pasteImgTextarea.html @@ -0,0 +1,19 @@ +<!doctype html> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<img id="i" src="green.png"> +<textarea id="t"></textarea> + +<script> +let loaded = new Promise(resolve => addLoadEvent(resolve)); + add_task(async function() { + await loaded; + SpecialPowers.setCommandNode(window, document.getElementById("i")); + SpecialPowers.doCommand(window, "cmd_copyImageContents"); + let input = document.getElementById("t"); + input.focus(); + var controller = + SpecialPowers.wrap(input).controllers.getControllerForCommand("cmd_paste"); + is(controller.isCommandEnabled("cmd_paste"), true, + "paste should be enabled in html textareas when an image is on the clipboard"); + }); +</script> diff --git a/editor/libeditor/tests/test_pasteImgTextarea.xhtml b/editor/libeditor/tests/test_pasteImgTextarea.xhtml new file mode 100644 index 0000000000..2dbf0b6427 --- /dev/null +++ b/editor/libeditor/tests/test_pasteImgTextarea.xhtml @@ -0,0 +1,26 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<window xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <html:img id="i" src="green.png" /> + <html:textarea id="t"></html:textarea> + </body> + <script type="text/javascript"><![CDATA[ + let loaded = new Promise(resolve => addLoadEvent(resolve)); + add_task(async function() { + await loaded; + SpecialPowers.setCommandNode(window, document.getElementById("i")); + SpecialPowers.doCommand(window, "cmd_copyImageContents"); + let input = document.getElementById("t"); + input.focus(); + var controller = + SpecialPowers.wrap(input).controllers.getControllerForCommand("cmd_paste"); + is(controller.isCommandEnabled("cmd_paste"), false, + "paste should not be enabled in xul textareas when an image is on the clipboard"); + }); + ]]></script> +</window> diff --git a/editor/libeditor/tests/test_paste_as_quote_in_text_control.html b/editor/libeditor/tests/test_paste_as_quote_in_text_control.html new file mode 100644 index 0000000000..443ee00eaa --- /dev/null +++ b/editor/libeditor/tests/test_paste_as_quote_in_text_control.html @@ -0,0 +1,48 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<title>Testing "paste" event dispatching for cmd_pasteQuote command</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<script> +"use strict"; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(async () => { + info("Waiting for initializing clipboard..."); + await SimpleTest.promiseClipboardChange( + "plain text", + () => SpecialPowers.clipboardCopyString("plain text") + ); + + for (let selector of ["input", "textarea"]) { + const textControl = document.querySelector(selector); + textControl.focus(); + textControl.addEventListener( + "paste", + event => event.preventDefault(), + {once: true} + ); + SpecialPowers.doCommand(window, "cmd_pasteQuote"); + is( + textControl.value, + "", + `<${selector}> should not have pasted text because "paste" event should've been canceled` + ); + SpecialPowers.doCommand(window, "cmd_pasteQuote"); + is( + textControl.value.replace(/\n/g, ""), + "> plain text", + `<${selector}> should have pasted text with a ">"` + ); + } + + SimpleTest.finish(); +}); +</script> +</head> +<body> +<input><textarea></textarea> +</body> +</html> diff --git a/editor/libeditor/tests/test_paste_no_formatting.html b/editor/libeditor/tests/test_paste_no_formatting.html new file mode 100644 index 0000000000..384510e405 --- /dev/null +++ b/editor/libeditor/tests/test_paste_no_formatting.html @@ -0,0 +1,214 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test pasting formatted test into various fields</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<input id="input"> +<textarea id="textarea"></textarea> +<div id="editable" contenteditable="true"></div> +<div id="noneditable">Text</div> + +<div id="source">Some <b>Bold</b> Text</div> +<script> + +const expectedText = "Some Bold Text"; +const expectedHTML = "<div id=\"source\">Some <b>Bold</b> Text</div>"; + +const htmlPrefix = navigator.platform.includes("Win") + ? "<html><body>\n<!--StartFragment-->" + : ""; +const htmlPostfix = navigator.platform.includes("Win") + ? "<!--EndFragment-->\n</body>\n</html>" + : ""; + +add_task(async function test_paste_formatted() { + window.getSelection().selectAllChildren(document.getElementById("source")); + synthesizeKey("c", { accelKey: true }); + + function doKey(element, withShiftKey) + { + let inputEventPromise = new Promise(resolve => { + element.addEventListener("input", event => { + is(event.inputType, "insertFromPaste", "correct inputType"); + resolve(); + }, { once: true }); + }); + synthesizeKey("v", { accelKey: true, shiftKey: withShiftKey }); + return inputEventPromise; + } + + function cancelEvent(event) { + event.preventDefault(); + } + + // Paste into input and textarea + for (let fieldid of ["input", "textarea"]) { + const field = document.getElementById(fieldid); + field.focus(); + + field.addEventListener("paste", cancelEvent); + doKey(field, false); + is( + field.value, + "", + `Nothing should be pasted into <${field.tagName.toLowerCase()}> when paste event is canceled (shift key is not pressed)` + ); + + doKey(field, true); + is( + field.value, + "", + `Nothing should be pasted into <${field.tagName.toLowerCase()}> when paste event is canceled (shift key is pressed)` + ); + field.removeEventListener("paste", cancelEvent); + + doKey(field, false); + is(field.value, expectedText, "paste into " + fieldid); + + doKey(field, true); + is(field.value, expectedText + expectedText, "paste unformatted into " + field); + } + + const selection = window.getSelection(); + + const editable = document.getElementById("editable"); + const innerHTMLBeforeTest = editable.innerHTML; + + (function test_pasteWithFormatIntoEditableArea() { + selection.selectAllChildren(editable); + selection.collapseToStart(); + editable.addEventListener("paste", cancelEvent); + doKey(editable, false); + is( + editable.innerHTML, + "", + "test_pasteWithFormatIntoEditableArea: Nothing should be pasted when paste event is canceled" + ); + editable.removeEventListener("paste", cancelEvent); + + doKey(editable, false); + is( + editable.innerHTML, + expectedHTML, + "test_pasteWithFormatIntoEditableArea: Pasting with format should work as expected" + ); + editable.innerHTML = innerHTMLBeforeTest; + }()); + + (function test_pasteWithoutFormatIntoEditableArea() { + selection.selectAllChildren(editable); + selection.collapseToEnd(); + doKey(editable, true); + is( + editable.innerHTML, + expectedText, + "test_pasteWithoutFormatIntoEditableArea: Pasting without format should work as expected", + ); + editable.innerHTML = innerHTMLBeforeTest; + })(); + + (function test_pasteWithFormatIntoEditableAreaWhenHTMLEditorIsInReadonlyMode() { + getEditor().flags |= SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask; + selection.selectAllChildren(editable); + selection.collapseToStart(); + let beforeInputEvents = []; + function onBeforeInput(aEvent) { + beforeInputEvents.push(aEvent); + } + editable.addEventListener("beforeinput", onBeforeInput); + doKey(editable, false); + const description = "test_pasteWithFormatIntoEditableAreaWhenHTMLEditorIsInReadonlyMode"; + is( + editable.innerHTML, + innerHTMLBeforeTest, + `${description}: Pasting with format should not work` + ); + is( + beforeInputEvents.length, + 0, + `${description}: Pasting with format should not cause "beforeinput", but fired "${ + beforeInputEvents[0]?.inputType + }"` + ); + editable.removeEventListener("beforeinput", onBeforeInput); + editable.innerHTML = innerHTMLBeforeTest; + getEditor().flags &= ~SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask; + })(); + + (function test_pasteWithoutFormatIntoEditableAreaWhenHTMLEditorIsInReadonlyMode() { + getEditor().flags |= SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask; + selection.selectAllChildren(editable); + selection.collapseToStart(); + let beforeInputEvents = []; + function onBeforeInput(aEvent) { + beforeInputEvents.push(aEvent); + } + editable.addEventListener("beforeinput", onBeforeInput); + doKey(editable, false); + const description = "test_pasteWithoutFormatIntoEditableAreaWhenHTMLEditorIsInReadonlyMode"; + is( + editable.innerHTML, + innerHTMLBeforeTest, + `${description}: Pasting with format should not work` + ); + is( + beforeInputEvents.length, + 0, + `${description}: Pasting with format should not cause "beforeinput", but fired "${ + beforeInputEvents[0]?.inputType + }"` + ); + editable.removeEventListener("beforeinput", onBeforeInput); + editable.innerHTML = innerHTMLBeforeTest; + getEditor().flags &= ~SpecialPowers.Ci.nsIEditor.eEditorReadonlyMask; + })(); + + let noneditable = document.getElementById("noneditable"); + selection.selectAllChildren(noneditable); + selection.collapseToStart(); + + function getPasteResult() { + return new Promise(resolve => { + noneditable.addEventListener("paste", event => { + resolve({ + text: event.clipboardData.getData("text/plain"), + html: event.clipboardData.getData("text/html"), + }); + }, { once: true}); + }); + } + + // Normal paste into non-editable area + let pastePromise = getPasteResult(); + doKey(noneditable, false); + is(noneditable.innerHTML, "Text", "paste into non-editable"); + + let result = await pastePromise; + is(result.text, expectedText, "paste text into non-editable"); + is(result.html, + htmlPrefix + expectedHTML + htmlPostfix, + "paste html into non-editable"); + + // Unformatted paste into non-editable area + pastePromise = getPasteResult(); + doKey(noneditable, true); + is(noneditable.innerHTML, "Text", "paste unformatted into non-editable"); + + result = await pastePromise; + is(result.text, expectedText, "paste unformatted text into non-editable"); + // Formatted HTML text should not exist when pasting unformatted. + is(result.html, "", "paste unformatted html into non-editable"); +}); + +function getEditor() { + const editingSession = SpecialPowers.wrap(window).docShell.editingSession; + return editingSession.getEditorForWindow(window); +} +</script> +</body> diff --git a/editor/libeditor/tests/test_paste_redirect_focus_in_paste_event_listener.html b/editor/libeditor/tests/test_paste_redirect_focus_in_paste_event_listener.html new file mode 100644 index 0000000000..b82938158e --- /dev/null +++ b/editor/libeditor/tests/test_paste_redirect_focus_in_paste_event_listener.html @@ -0,0 +1,143 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<title>Testing handling "paste" command when a "paste" event listener moves focus</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<script> +"use strict"; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(async () => { + info("Waiting for initializing clipboard..."); + await SimpleTest.promiseClipboardChange( + "plain text", + () => SpecialPowers.clipboardCopyString("plain text") + ); + + const transferable = + SpecialPowers.Cc["@mozilla.org/widget/transferable;1"].createInstance(SpecialPowers.Ci.nsITransferable); + transferable.init( + SpecialPowers.wrap(window).docShell.QueryInterface(SpecialPowers.Ci.nsILoadContext) + ); + const supportString = + SpecialPowers.Cc["@mozilla.org/supports-string;1"].createInstance(SpecialPowers.Ci.nsISupportsString); + supportString.data = "plain text"; + transferable.setTransferData("text/plain", supportString); + + function getValue(aElement) { + if (aElement.tagName.toLowerCase() == "input" || + aElement.tagName.toLowerCase() == "textarea") { + return aElement.value; + } + return aElement.textContent; + } + function setValue(aElement, aValue) { + if (aElement.tagName.toLowerCase() == "input" || + aElement.tagName.toLowerCase() == "textarea") { + aElement.value = aValue; + return; + } + aElement.innerHTML = aValue === "" ? "<br>" : aValue; + } + + for (const command of [ + "cmd_paste", + "cmd_pasteNoFormatting", + "cmd_pasteQuote", + "cmd_pasteTransferable" + ]) { + for (const editableSelector of [ + "#src > input", + "#src > textarea", + "#src > div[contenteditable]" + ]) { + const editableElement = document.querySelector(editableSelector); + const editableElementDesc = `<${ + editableElement.tagName.toLocaleLowerCase() + }${editableElement.hasAttribute("contenteditable") ? " contenteditable" : ""}>`; + (test_from_editableElement_to_input => { + const input = document.querySelector("#dest > input"); + editableElement.focus(); + editableElement.addEventListener( + "paste", + () => input.focus(), + {once: true} + ); + SpecialPowers.doCommand(window, command, transferable); + is( + getValue(editableElement).replace(/\n/g, ""), + "", + `${command}: ${ + editableElementDesc + } should not have the pasted text because focus is redirected to <input> in a "paste" event listener` + ); + is( + input.value.replace("> ", ""), + "plain text", + `${command}: new focused <input> (moved from ${ + editableElementDesc + }) should have the pasted text` + ); + setValue(editableElement, ""); + input.value = ""; + })(); + + (test_from_editableElement_to_contenteditable => { + const contentEditable = document.querySelector("#dest > div[contenteditable]"); + editableElement.focus(); + editableElement.addEventListener( + "paste", + () => contentEditable.focus(), + {once: true} + ); + SpecialPowers.doCommand(window, command, transferable); + is( + getValue(editableElement).replace(/\n/g, ""), + "", + `${command}: ${ + editableElementDesc + } should not have the pasted text because focus is redirected to <div contenteditable> in a "paste" event listener` + ); + is( + contentEditable.textContent.replace(/\n/g, "").replace("> ", ""), + "plain text", + `${command}: new focused <div contenteditable> (moved from ${ + editableElementDesc + }) should have the pasted text` + ); + setValue(editableElement, ""); + contentEditable.innerHTML = "<br>"; + })(); + + (test_from_editableElement_to_non_editable => { + const button = document.querySelector("#dest > button"); + editableElement.focus(); + editableElement.addEventListener( + "paste", + () => button.focus(), + {once: true} + ); + SpecialPowers.doCommand(window, command, transferable); + is( + getValue(editableElement).replace(/\n/g, ""), + "", + `${command}: ${ + editableElementDesc + } should not have the pasted text because focus is redirected to <button> in a "paste" event listener` + ); + setValue(editableElement, ""); + })(); + } + } + + SimpleTest.finish(); +}); +</script> +</head> +<body> +<div id="src"><input><textarea></textarea><div contenteditable><br></div></div> +<div id="dest"><input><div contenteditable><br></div><button>button</button></div> +</body> +</html> diff --git a/editor/libeditor/tests/test_pasting_in_root_element.xhtml b/editor/libeditor/tests/test_pasting_in_root_element.xhtml new file mode 100644 index 0000000000..d647899ca7 --- /dev/null +++ b/editor/libeditor/tests/test_pasting_in_root_element.xhtml @@ -0,0 +1,74 @@ +<!DOCTYPE HTML> +<html contenteditable="" xmlns="http://www.w3.org/1999/xhtml"><head> + <!-- https://bugzilla.mozilla.org/show_bug.cgi?id=1719387 --> + <meta charset="utf-8"/> + <title>Test to paste plaintext in the clipboard into the html element which does not have body element</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head><span>Text outside body</span><script><![CDATA[ +"use strict"; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(async () => { + info("Waiting for initializing clipboard..."); + await SimpleTest.promiseClipboardChange( + "plain text", + () => { + SpecialPowers.clipboardCopyString("plain text"); + } + ); + + focus(); + + function getInnerHTMLOfBody() { + return document.documentElement.innerHTML.replace(/\n/g, "") + .replace(/ xmlns="http:\/\/www.w3.org\/1999\/xhtml"/g, "") + .replace(/<head.+[\/]head>/g, "") + .replace(/<script.+[\/]script>/g, ""); + } + + try { + getSelection().collapse(document.documentElement, 1); // collapse to before the <span> element. + document.execCommand("insertText", false, "plain text"); + todo_is( + getInnerHTMLOfBody(), + "plain text<span>Text outside body</span>", // Chrome's result: "<span>plain textText outside body</span>" + "Typing text should insert the text before the <span> element" + ); + } catch (ex) { + ok(false, `Failed to typing text due to ${ex}`); + } finally { + SpecialPowers.doCommand(window, "cmd_undo"); + } + + try { + getSelection().collapse(document.documentElement, 1); // collapse to before the <span> element. + SpecialPowers.doCommand(window, "cmd_paste"); + is( + getInnerHTMLOfBody(), + "plain text<span>Text outside body</span>", // Chrome's result: "<span>plain textText outside body</span>" + "\"cmd_paste\" should insert text in the clipboard before the <span> element" + ); + } catch (ex) { + todo(false, `Failed to typing text due to ${ex}`); + } finally { + SpecialPowers.doCommand(window, "cmd_undo"); + } + + try { + getSelection().collapse(document.documentElement, 1); // collapse to before the <span> element. + SpecialPowers.doCommand(window, "cmd_pasteQuote"); + is( + getInnerHTMLOfBody(), + "<blockquote type=\"cite\">plain text</blockquote><span>Text outside body</span>", + "\"cmd_pasteQuote\" should insert the text wrapping with <blockquote> element before the <span> element" + ); + } catch (ex) { + ok(false, `Failed to typing text due to ${ex}`); + } finally { + SpecialPowers.doCommand(window, "cmd_undo"); + } + + SimpleTest.finish(); +}); +]]></script></html> diff --git a/editor/libeditor/tests/test_pasting_in_temporarily_created_div_outside_body.html b/editor/libeditor/tests/test_pasting_in_temporarily_created_div_outside_body.html new file mode 100644 index 0000000000..05250be049 --- /dev/null +++ b/editor/libeditor/tests/test_pasting_in_temporarily_created_div_outside_body.html @@ -0,0 +1,42 @@ +<!doctype html> +<html> +<head> +<title>Test for paste in temporarily created div element outside the body element</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<script> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(async () => { + const editor = document.querySelector("div[contenteditable]"); + const heading = document.querySelector("h1"); + getSelection().setBaseAndExtent(heading.firstChild, "So".length, + heading.firstChild, "Some te".length); + try { + await SimpleTest.promiseClipboardChange( + "me te", () => synthesizeKey("c", {accelKey: true})); + } catch (ex) { + ok(false, `Failed to copy selected text: ${ex}`); + SimpleTest.finish(); + } + editor.focus(); + editor.addEventListener("paste", () => { + const anotherEditor = document.createElement("div"); + anotherEditor.setAttribute("contenteditable", "true"); + document.documentElement.appendChild(anotherEditor); + anotherEditor.focus(); + }, {once: true}); + synthesizeKey("v", {accelKey: true}); + const tempEditor = document.documentElement.lastChild; + is(tempEditor.nodeName.toLocaleLowerCase(), "div", + "Paste event handler should've inserted another editor"); + is(tempEditor.textContent.trim(), "me te"); + SimpleTest.finish(); +}); +</script> +</head> +<body> + <h1>Some text</h1> + <div contenteditable></div> +</body> +</html> diff --git a/editor/libeditor/tests/test_pasting_table_rows.html b/editor/libeditor/tests/test_pasting_table_rows.html new file mode 100644 index 0000000000..328fb0b297 --- /dev/null +++ b/editor/libeditor/tests/test_pasting_table_rows.html @@ -0,0 +1,554 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test pasting table rows</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <style> + /** + * A small font-size, so that the loaded document fits on the screens of all + * test devices. + */ + * { font-size: 8px; } + + /** + * Helps fitting the tables on the screens of all test devices. + */ + div[class="tableContainer"] { + display: inline-block; + } + </style> + <script> + const kEditabilityModeContenteditable = "contenteditable"; + const kEditabilityModeDesignMode = "designMode"; + + // All column names of the test-tables used below. + const kColumns = ["c1", "c2", "c3"]; + + // Ctrl+click on table cells to select them. + const kSelectionModeClickSelection = "click-selection"; + // Click and drag from the first given row to the end of the last given row. + const kSelectionModeDragSelection = "drag-selection"; + + const kTableTagName = "TABLE"; + const kTbodyTagName = "TBODY"; + const kTheadTagName = "THEAD"; + const kTfootTagName = "TFOOT"; + + const kInputEventType = "input"; + const kInputEventInputTypeInsertFromPaste = "insertFromPaste"; + + // Where a table is pasted to in the test. + const kTargetElementId = "targetElement"; + + /** + * @param aTableName see Test::constructor::aTableName. + * @param aRowsInTable see Test::constructor::aRowsInTable. + * @return an array of elements of aRowsInTable. + */ + function FilterRowsWithParentTag(aTableName, aRowsInTable, aTagName) { + return aRowsInTable.filter(rowName => document.getElementById(aTableName + + rowName).parentElement.tagName == aTagName); + } + + /** + * Tables used with this class are required to: + * - have ids of the following form for each table cell: + <tableName><rowName><column>. Where <column> has to be one of + `kColumns`. + - have exactly `kColumns.length` columns per row. + - have an id of the form <tableName><rowName> for each table row. + */ + class Test { + /** + * @param aTableName indicates which table to operate on. + * @param aRowsInTable an array of row names. Ordered from top to bottom. + * @param aEditabilityMode `kEditabilityModeContenteditable` or + * `kEditabilityModeDesignMode`. + * @param aSelectionMode `kSelectionModeClickSelection` or + * `kSelectionModeDragSelection`. + */ + constructor(aTableName, aRowsInTable, aEditabilityMode, aSelectionMode) { + ok(aEditabilityMode == kEditabilityModeContenteditable || + aEditabilityMode == kEditabilityModeDesignMode, + "Editablity mode is valid."); + + ok(aSelectionMode == kSelectionModeClickSelection || + aSelectionMode == kSelectionModeDragSelection, + "Selection mode is valid."); + + this._tableName = aTableName; + this._rowsInTable = aRowsInTable; + this._editabilityMode = aEditabilityMode; + this._selectionMode = aSelectionMode; + this._innerHTMLOfTargetBeforeTestRun = + document.getElementById(kTargetElementId).innerHTML; + + if (this._editabilityMode == kEditabilityModeDesignMode) { + this._removeContenteditableAttributeOfTarget(); + document.designMode = "on"; + } + + SimpleTest.info("Constructed the test (" + this._toString() + ")."); + } + + /** + * Call `_restoreStateOfDocumentBeforeRun` afterwards. + */ + async _run() { + // Generate the expected pasted HTML before pasting the clipboard's + // content, because that may duplicate ids, hence leading to creating + // a wrong expectation string. + const expectedPastedHTML = this._createExpectedOuterHTMLOfTable(); + + if (this._selectionMode == kSelectionModeDragSelection) { + this._dragSelectAllCellsInRowsOfTable(); + } else { + this._clickSelectAllCellsInRowsOfTable(); + } + + await this._copyToClipboard(expectedPastedHTML); + this._pasteToTargetElement(); + + const targetElement = document.getElementById(kTargetElementId); + is(targetElement.children.length, 1, + "Target element has exactly one child."); + is(targetElement.children[0]?.tagName, kTableTagName, + "Target element has a table child."); + + // Linebreaks and whitespace after tags are irrelevant, hence stripping + // them. + is(SimpleTest.stripLinebreaksAndWhitespaceAfterTags( + targetElement.children[0]?.outerHTML), expectedPastedHTML, + "Pasted table (" + this._toString() + ") has expected outerHTML."); + } + + _restoreStateOfDocumentBeforeRun() { + if (this._editabilityMode == kEditabilityModeDesignMode) { + document.designMode = "off"; + this._setContenteditableAttributeOfTarget(); + } + + const targetElement = document.getElementById(kTargetElementId); + targetElement.innerHTML = this._innerHTMLOfTargetBeforeTestRun; + targetElement.getBoundingClientRect(); + + SimpleTest.info( + "Restored the state of the document before the test run."); + } + + _toString() { + return "table: " + this._tableName + "; row(s): " + + this._rowsInTable.toString() + "; editability-mode: " + + this._editabilityMode + "; selection-mode: " + this._selectionMode; + } + + _removeContenteditableAttributeOfTarget() { + const targetElement = document.getElementById(kTargetElementId); + SimpleTest.info("Removing target's 'contenteditable' attribute."); + targetElement.removeAttribute("contenteditable"); + } + + _setContenteditableAttributeOfTarget() { + const targetElement = document.getElementById(kTargetElementId); + SimpleTest.info("Setting 'contenteditable' attribute of target."); + targetElement.setAttribute("contenteditable", ""); + } + + _getOuterHTMLAndStripLinebreaksAndWhitespaceAfterTags(aElementId) { + const outerHTML = document.getElementById(aElementId).outerHTML; + return SimpleTest.stripLinebreaksAndWhitespaceAfterTags(outerHTML); + } + + _createExpectedOuterHTMLOfTable() { + const rowsInTableHead = FilterRowsWithParentTag(this._tableName, + this._rowsInTable, kTheadTagName); + + const rowsInTableBody = FilterRowsWithParentTag(this._tableName, + this._rowsInTable, kTbodyTagName); + + const rowsInTableFoot = FilterRowsWithParentTag(this._tableName, + this._rowsInTable, kTfootTagName); + + let expectedTableOuterHTML = '\ +<table>'; + + if (rowsInTableHead.length) { + expectedTableOuterHTML += '\ +<thead>'; + rowsInTableHead.forEach(rowName => + expectedTableOuterHTML += + this._getOuterHTMLAndStripLinebreaksAndWhitespaceAfterTags( + this._tableName + rowName)); + expectedTableOuterHTML +='\ +</thead>'; + } + + if (rowsInTableBody.length) { + expectedTableOuterHTML += '\ +<tbody>'; + + rowsInTableBody.forEach(rowName => + expectedTableOuterHTML += + this._getOuterHTMLAndStripLinebreaksAndWhitespaceAfterTags( + this._tableName + rowName)); + + expectedTableOuterHTML +='\ +</tbody>'; + } + + if (rowsInTableFoot.length) { + expectedTableOuterHTML += '\ +<tfoot>'; + rowsInTableFoot.forEach(rowName => + expectedTableOuterHTML += + this._getOuterHTMLAndStripLinebreaksAndWhitespaceAfterTags(this._tableName + + rowName)); + expectedTableOuterHTML += '\ +</tfoot>'; + } + + expectedTableOuterHTML += '\ +</table>'; + + return expectedTableOuterHTML; + } + + _clickSelectAllCellsInRowsOfTable() { + function synthesizeAccelKeyAndClickAt(aElementId) { + const element = document.getElementById(aElementId); + synthesizeMouseAtCenter(element, { accelKey: true }); + } + + this._rowsInTable.forEach(rowName => kColumns.forEach(column => + synthesizeAccelKeyAndClickAt(this._tableName + rowName + column))); + } + + _dragSelectAllCellsInRowsOfTable() { + const firstColumnOfFirstRow = document.getElementById(this._tableName + + this._rowsInTable[0] + kColumns[0]); + const lastColumnOfLastRow = document.getElementById(this._tableName + + this._rowsInTable.slice(-1)[0] + kColumns.slice(-1)[0]); + + synthesizeMouse(firstColumnOfFirstRow, 0 /* aOffsetX */, + 0 /* aOffsetY */, { type: "mousedown" } /* aEvent */); + + const rectOfLastColumnOfLastRow = + lastColumnOfLastRow.getBoundingClientRect(); + + synthesizeMouse(lastColumnOfLastRow, rectOfLastColumnOfLastRow.width + /* aOffsetX */, rectOfLastColumnOfLastRow.height /* aOffsetY */, + { type: "mousemove" } /* aEvent */); + + synthesizeMouse(lastColumnOfLastRow, rectOfLastColumnOfLastRow.width + /* aOffsetX */, rectOfLastColumnOfLastRow.height /* aOffsetY */, + { type: "mouseup" } /* aEvent */); + } + + /** + * @return a promise. + */ + async _copyToClipboard(aExpectedPastedHTML) { + const flavor = "text/html"; + + const expectedPastedHTML = (() => { + if (navigator.platform.includes(kPlatformWindows)) { + // TODO: ideally, this should be factored out, see bug 1669963. + + // Windows wraps the pasted HTML, see + // https://searchfox.org/mozilla-central/rev/8f7b017a31326515cb467e69eef1f6c965b4f00e/widget/windows/nsDataObj.cpp#1798-1805,1839-1840,1842. + return kTextHtmlPrefixClipboardDataWindows + + aExpectedPastedHTML + kTextHtmlSuffixClipboardDataWindows; + } + return aExpectedPastedHTML; + })(); + + function validatorFn(aData) { + // The data's format doesn't specify whether there should be line + // breaks or whitspace between tags. Hence, remove them. + if (SimpleTest.stripLinebreaksAndWhitespaceAfterTags(aData) == + SimpleTest.stripLinebreaksAndWhitespaceAfterTags(expectedPastedHTML)) { + return true; + } + info(`Waiting clipboard data: expected:\n"${ + SimpleTest.stripLinebreaksAndWhitespaceAfterTags(expectedPastedHTML) + }"\n, but got:\n"${ + SimpleTest.stripLinebreaksAndWhitespaceAfterTags(aData) + }"`); + return false; + } + + return SimpleTest.promiseClipboardChange(validatorFn, + () => synthesizeKey("c", { accelKey: true } /* aEvent*/), flavor); + } + + _pasteToTargetElement() { + const editingHost = (this._editabilityMode == + kEditabilityModeContenteditable) ? + document.getElementById(kTargetElementId) : + document; + + let inputEvent; + function handleInputEvent(aEvent) { + if (aEvent.inputType == kInputEventInputTypeInsertFromPaste) { + editingHost.removeEventListener(kInputEventType, handleInputEvent); + SimpleTest.info( + 'Listened to an "' + kInputEventInputTypeInsertFromPaste + '" "' + + kInputEventType + ' event.'); + inputEvent = aEvent; + } + } + editingHost.addEventListener(kInputEventType, handleInputEvent); + + const targetElement = document.getElementById(kTargetElementId); + synthesizeMouseAtCenter(targetElement, {}); + synthesizeKey("v", { accelKey: true } /* aEvent */); + + ok( + inputEvent != undefined, + `An ${kInputEventType} whose "inputType" is ${ + kInputEventInputTypeInsertFromPaste + } should've been fired on ${editingHost.localName}` + ); + } + } + + function ContainsRowWithParentTag(aTableName, aRowsInTable, aTagName) { + return !!FilterRowsWithParentTag(aTableName, aRowsInTable, + aTagName).length; + } + + function DoesContainRowInTheadAndTbody(aTableName, aRowsInTable) { + return ContainsRowWithParentTag(aTableName, aRowsInTable, kTheadTagName) && + ContainsRowWithParentTag(aTableName, aRowsInTable, kTbodyTagName); + } + + function DoesContainRowInTbodyAndTfoot(aTableName, aRowsInTable) { + return ContainsRowWithParentTag(aTableName, aRowsInTable, kTbodyTagName) + && ContainsRowWithParentTag(aTableName, aRowsInTable, kTfootTagName); + } + + async function runTests() { + const kClickSelectionTests = { + selectionMode : kSelectionModeClickSelection, + tablesToTest : ["t1", "t2", "t3", "t4", "t5"], + rowsToSelect : [ + ["r1", "r2", "r3", "r4"], + ["r1"], + ["r2", "r3"], + ["r1", "r3"], + ["r3", "r4"], + ["r4"], + ], + }; + + const kDragSelectionTests = { + selectionMode : kSelectionModeDragSelection, + tablesToTest : ["t1", "t2", "t3", "t4", "t5"], + // Only consecutive rows when drag-selecting. + rowsToSelect : [ + ["r1", "r2", "r3", "r4"], + ["r1"], + ["r2", "r3"], + ["r3", "r4"], + ["r4"], + ], + }; + + const kTestGroups = [kClickSelectionTests, kDragSelectionTests]; + + const kEditabilityModes = [ + kEditabilityModeContenteditable, + kEditabilityModeDesignMode, + ]; + + for (const editabilityMode of kEditabilityModes) { + for (const testGroup of kTestGroups) { + for (const tableName of testGroup.tablesToTest) { + for (const rowsToSelect of testGroup.rowsToSelect) { + if (DoesContainRowInTheadAndTbody(tableName, rowsToSelect) || + DoesContainRowInTbodyAndTfoot(tableName, rowsToSelect)) { + todo(false, + 'Rows to select (' + rowsToSelect.toString() + ') contains ' + + ' row in <tbody> and <thead> or <tfoot> of table "' + + tableName + '", see bug 1667786.'); + continue; + } + + const test = new Test(tableName, rowsToSelect, editabilityMode, + testGroup.selectionMode); + try { + await test._run(); + } catch (ex) { + ok(false, `Aborting the following tests due to unexpected error: ${ex.message}`); + SimpleTest.finish(); + return; + } + test._restoreStateOfDocumentBeforeRun(); + } + } + } + } + + SimpleTest.finish(); + } + + function onLoad() { + SimpleTest.waitForExplicitFinish(); + SimpleTest.waitForFocus(runTests); + } + </script> +</head> +<body onload="onLoad()"> +<p id="display"></p> + <h4>Test for <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1639972">bug 1639972</a></h4> + <div id="content"> + <div class="tableContainer">Table with <code>tbody</code> and <code>td</code>: + <table> + <tbody> + <tr id="t1r1"> + <td id="t1r1c1">r1c1</td> + <td id="t1r1c2">r1c2</td> + <td id="t1r1c3">r1c3</td> + </tr> + <tr id="t1r2"> + <td id="t1r2c1">r2c1</td> + <td id="t1r2c2">r2c2</td> + <td id="t1r2c3">r2c3</td> + </tr> + <tr id="t1r3"> + <td id="t1r3c1">r3c1</td> + <td id="t1r3c2">r3c2</td> + <td id="t1r3c3">r3c3</td> + </tr> + <tr id="t1r4"> + <td id="t1r4c1">r4c1</td> + <td id="t1r4c2">r4c2</td> + <td id="t1r4c3">r4c3</td> + </tr> + </tbody> + </table> + </div> + + <div class="tableContainer">Table with <code>tbody</code>, <code>td</code> and <code>th</code>: + <table> + <tbody> + <tr id="t2r1"> + <th id="t2r1c1">r1c1</th> + <th id="t2r1c2">r1c2</th> + <th id="t2r1c3">r1c3</th> + </tr> + <tr id="t2r2"> + <td id="t2r2c1">r2c1</td> + <td id="t2r2c2">r2c2</td> + <td id="t2r2c3">r2c3</td> + </tr> + <tr id="t2r3"> + <td id="t2r3c1">r3c1</td> + <td id="t2r3c2">r3c2</td> + <td id="t2r3c3">r3c3</td> + </tr> + <tr id="t2r4"> + <td id="t2r4c1">r4c1</td> + <td id="t2r4c2">r4c2</td> + <td id="t2r4c3">r4c3</td> + </tr> + </tbody> + </table> + </div> + + <div class="tableContainer">Table with <code>thead</code>, <code>tbody</code>, <code>td</code>: + <table> + <thead> + <tr id="t3r1"> + <td id="t3r1c1">r1c1</td> + <td id="t3r1c2">r1c2</td> + <td id="t3r1c3">r1c3</td> + </tr> + </thead> + <tbody> + <tr id="t3r2"> + <td id="t3r2c1">r2c1</td> + <td id="t3r2c2">r2c2</td> + <td id="t3r2c3">r2c3</td> + </tr> + <tr id="t3r3"> + <td id="t3r3c1">r3c1</td> + <td id="t3r3c2">r3c2</td> + <td id="t3r3c3">r3c3</td> + </tr> + <tr id="t3r4"> + <td id="t3r4c1">r4c1</td> + <td id="t3r4c2">r4c2</td> + <td id="t3r4c3">r4c3</td> + </tr> + </tbody> + </table> + </div> + + <div class="tableContainer">Table with <code>thead</code>, <code>tbody</code>, <code>td</code> and <code>th</code>: + <table> + <thead> + <tr id="t4r1"> + <th id="t4r1c1">r1c1</th> + <th id="t4r1c2">r1c2</th> + <th id="t4r1c3">r1c3</th> + </tr> + </thead> + <tbody> + <tr id="t4r2"> + <td id="t4r2c1">r2c1</td> + <td id="t4r2c2">r2c2</td> + <td id="t4r2c3">r2c3</td> + </tr> + <tr id="t4r3"> + <td id="t4r3c1">r3c1</td> + <td id="t4r3c2">r3c2</td> + <td id="t4r3c3">r3c3</td> + </tr> + <tr id="t4r4"> + <td id="t4r4c1">r4c1</td> + <td id="t4r4c2">r4c2</td> + <td id="t4r4c3">r4c3</td> + </tr> + </tbody> + </table> + </div> + <div class="tableContainer">Table with <code>thead</code>, + <code>tbody</code>, <code>tfoot</code>, and <code>td</code>: + <table> + <thead> + <tr id="t5r1"> + <td id="t5r1c1">r1c1</td> + <td id="t5r1c2">r1c2</td> + <td id="t5r1c3">r1c3</td> + </tr> + </thead> + <tbody> + <tr id="t5r2"> + <td id="t5r2c1">r2c1</td> + <td id="t5r2c2">r2c2</td> + <td id="t5r2c3">r2c3</td> + </tr> + <tr id="t5r3"> + <td id="t5r3c1">r3c1</td> + <td id="t5r3c2">r3c2</td> + <td id="t5r3c3">r3c3</td> + </tr> + </tbody> + <tfoot> + <tr id="t5r4"> + <td id="t5r4c1">r4c1</td> + <td id="t5r4c2">r4c2</td> + <td id="t5r4c3">r4c3</td> + </tr> + </tfoot> + </table> + </div> + <p>Target for pasting: + <div id="targetElement" contenteditable><!-- Some content so that it can be clicked on. -->X</div> + </p> + </div> +</html> diff --git a/editor/libeditor/tests/test_pasting_text_longer_than_maxlength.html b/editor/libeditor/tests/test_pasting_text_longer_than_maxlength.html new file mode 100644 index 0000000000..d25875431e --- /dev/null +++ b/editor/libeditor/tests/test_pasting_text_longer_than_maxlength.html @@ -0,0 +1,53 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1320229 + +Checks if a user can paste a password longer than `maxlength` and if the field +is then marked as `tooLong`. +--> +<head> + <title>Test pasting text longer than maxlength</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1320229">Mozilla Bug 1320229</a> +<p id="display"></p> +<div id="content"> + <div id="src">aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</div> + <input id="form-password" type="password" maxlength="20"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 1320229 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(async function() { + await SpecialPowers.pushPrefEnv({"set": [["editor.truncate_user_pastes", false]]}); + var src = document.getElementById("src"); + var pwd = document.getElementById("form-password"); + SimpleTest.waitForClipboard(src.textContent, + function() { + getSelection().selectAllChildren(src); + synthesizeKey("C", {accelKey: true}); + }, + function() { + pwd.focus(); + synthesizeKey("V", {accelKey: true}); + is(pwd.value, src.textContent, + "Pasting should paste the clipboard contents regardless of maxlength"); + is(pwd.validity.tooLong, true, "Pasting over maxlength should set the tooLong flag") + SimpleTest.finish(); + }, + function() { + SimpleTest.finish(); + } + ); +}); + +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_resizers_appearance.html b/editor/libeditor/tests/test_resizers_appearance.html new file mode 100644 index 0000000000..e09c80f530 --- /dev/null +++ b/editor/libeditor/tests/test_resizers_appearance.html @@ -0,0 +1,109 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test for resizers appearance</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none;"> + +</div> + +<div id="editor" contenteditable></div> +<div id="clickaway" style="width: 3px; height: 3px;"></div> +<img src="green.png"><!-- for ensuring to load the image at first test of <img> case --> +<pre id="test"> + +<script class="testbody" type="application/javascript"> +"use strict"; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(async function() { + async function waitForSelectionChange() { + return new Promise(resolve => { + document.addEventListener("selectionchange", () => { + resolve(); + }, {once: true}); + }); + } + + let editor = document.getElementById("editor"); + let outOfEditor = document.getElementById("clickaway"); + + const kTests = [ + { description: "<img>", + innerHTML: "<img id=\"target\" src=\"green.png\" width=\"100\" height=\"100\">", + resizable: true, + }, + { description: "<table>", + innerHTML: "<table id=\"target\" border><tr><td>1-1</td><td>1-2</td></tr><tr><td>2-1</td><td>2-2</td></tr></table>", + resizable: true, + }, + { description: "absolute positioned <div>", + innerHTML: "<div id=\"target\" style=\"position: absolute; top: 50px; left: 50px;\">positioned</div>", + resizable() { return document.queryCommandState("enableAbsolutePositionEditing"); }, + }, + { description: "fixed positioned <div>", + innerHTML: "<div id=\"target\" style=\"position: fixed; top: 50px; left: 50px;\">positioned</div>", + resizable: false, + }, + { description: "relative positioned <div>", + innerHTML: "<div id=\"target\" style=\"position: relative; top: 50px; left: 50px;\">positioned</div>", + resizable: false, + }, + ]; + + for (let kEnableAbsolutePositionEditor of [true, false]) { + document.execCommand("enableAbsolutePositionEditing", false, kEnableAbsolutePositionEditor); + for (const kTest of kTests) { + const kDescription = kTest.description + + (kEnableAbsolutePositionEditor ? " (enabled absolute position editor)" : "") + ": "; + editor.innerHTML = kTest.innerHTML; + let target = document.getElementById("target"); + + document.execCommand("enableObjectResizing", false, false); + ok(!document.queryCommandState("enableObjectResizing"), + kDescription + "Object resizer should be disabled by the call of execCommand"); + + synthesizeMouseAtCenter(outOfEditor, {}); + let promiseSelectionChangeEvent1 = waitForSelectionChange(); + synthesizeMouseAtCenter(target, {}); + await promiseSelectionChangeEvent1; + + ok(!target.hasAttribute("_moz_resizing"), + kDescription + ": While enableObjectResizing is disabled, resizers shouldn't appear"); + + document.execCommand("enableObjectResizing", false, true); + ok(document.queryCommandState("enableObjectResizing"), + kDescription + "Object resizer should be enabled by the call of execCommand"); + + synthesizeMouseAtCenter(outOfEditor, {}); + let promiseSelectionChangeEvent2 = waitForSelectionChange(); + synthesizeMouseAtCenter(target, {}); + await promiseSelectionChangeEvent2; + + const kResizable = typeof kTest.resizable === "function" ? kTest.resizable() : kTest.resizable; + is(target.hasAttribute("_moz_resizing"), kResizable, + kDescription + (kResizable ? "While enableObjectResizing is enabled, resizers should appear" : + "Even while enableObjectResizing is enabled, resizers shouldn't appear")); + + document.execCommand("enableObjectResizing", false, false); + ok(!target.hasAttribute("_moz_resizing"), + kDescription + "enableObjectResizing is disabled even while resizers are visible, resizers should disappear"); + + document.execCommand("enableObjectResizing", false, true); + is(target.hasAttribute("_moz_resizing"), kResizable, + kDescription + (kResizable ? "enableObjectResizing is enabled when resizable object is selected, resizers should appear" : + "Even if enableObjectResizing is enabled when non-resizable object is selected, resizers shouldn't appear")); + } + } + + SimpleTest.finish(); +}); +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_resizers_resizing_elements.html b/editor/libeditor/tests/test_resizers_resizing_elements.html new file mode 100644 index 0000000000..f30b6bb269 --- /dev/null +++ b/editor/libeditor/tests/test_resizers_resizing_elements.html @@ -0,0 +1,299 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for resizers of some elements</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style> + #target { + background-color: green; + } + </style> +</head> +<body> +<p id="display"></p> +<div id="content" contenteditable style="width: 200px; height: 200px;"></div> +<div id="clickaway" style="width: 10px; height: 10px"></div> +<img src="green.png"><!-- for ensuring to load the image at first test of <img> case --> +<pre id="test"> +<script type="application/javascript"> +"use strict"; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(async () => { + document.execCommand("enableObjectResizing", false, true); + ok(document.queryCommandState("enableObjectResizing"), + "Object resizer should be enabled by the call of execCommand"); + // Disable inline-table-editing UI for this test. + document.execCommand("enableInlineTableEditing", false, false); + + let outOfEditor = document.getElementById("clickaway"); + + function cancel(e) { e.stopPropagation(); } + let content = document.getElementById("content"); + content.addEventListener("mousedown", cancel); + content.addEventListener("mousemove", cancel); + content.addEventListener("mouseup", cancel); + + async function waitForSelectionChange() { + return new Promise(resolve => { + document.addEventListener("selectionchange", () => { + resolve(); + }, {once: true}); + }); + } + + async function doTest(aDescription, aPreserveRatio, aInnerHTML) { + let description = aDescription; + if (document.queryCommandState("enableAbsolutePositionEditing")) { + description += " (absolute position editor is enabled)"; + } + description += ": "; + content.innerHTML = aInnerHTML; + let target = document.getElementById("target"); + + /** + * This function is a generic resizer test. + * We have 8 resizers that we'd like to test, and each can be moved in 8 different directions. + * In specifying baseX, W can be considered to be the width of the image, and for baseY, H + * can be considered to be the height of the image. deltaX and deltaY are regular pixel values + * which can be positive or negative. + * TODO: Should test canceling "beforeinput" events case. + */ + const W = 1; + const H = 1; + async function testResizer(baseX, baseY, deltaX, deltaY, expectedDeltaX, expectedDeltaY) { + ok(true, description + "testResizer(" + [baseX, baseY, deltaX, deltaY, expectedDeltaX, expectedDeltaY].join(", ") + ")"); + + // Reset the dimensions of the target. + target.style.width = "150px"; + target.style.height = "150px"; + let rect = target.getBoundingClientRect(); + is(rect.width, 150, description + "Sanity check the width"); + is(rect.height, 150, description + "Sanity check the height"); + + // Click on the target to show the resizers + ok(true, "waiting selectionchange to select the target element"); + let promiseSelectionChangeEvent = waitForSelectionChange(); + synthesizeMouseAtCenter(target, {}); + await promiseSelectionChangeEvent; + + // Determine which resizer we're dealing with. + let basePosX = rect.width * baseX; + let basePosY = rect.height * baseY; + + let inputEventExpected = true; + function onInput(aEvent) { + if (!inputEventExpected) { + ok(false, `"${aEvent.type}" event shouldn't be fired after stopping resizing`); + return; + } + ok(aEvent instanceof InputEvent, + `"${aEvent.type}" event should be dispatched with InputEvent interface`); + is(aEvent.cancelable, false, + `"${aEvent.type}" event should be never cancelable`); + is(aEvent.bubbles, true, + `"${aEvent.type}" event should always bubble`); + is(aEvent.inputType, "", + `inputType of "${aEvent.type}" event should be empty string when an element is resized`); + is(aEvent.data, null, + `data of "${aEvent.type}" event should be null ${aDescription}`); + is(aEvent.dataTransfer, null, + `data of "${aEvent.type}" event should be null ${aDescription}`); + let targetRanges = aEvent.getTargetRanges(); + if (aEvent.type === "beforeinput") { + let selection = document.getSelection(); + is(targetRanges.length, selection.rangeCount, + `getTargetRanges() of "beforeinput" event for position changing of absolute position should return selection ranges ${aDescription}`); + if (targetRanges.length === selection.rangeCount) { + for (let i = 0; i < selection.rangeCount; i++) { + let range = selection.getRangeAt(i); + is(targetRanges[i].startContainer, range.startContainer, + `startContainer of getTargetRanges()[${i}] of "beforeinput" event for position changing of absolute position does not match ${aDescription}`); + is(targetRanges[i].startOffset, range.startOffset, + `startOffset of getTargetRanges()[${i}] of "beforeinput" event for position changing of absolute position does not match ${aDescription}`); + is(targetRanges[i].endContainer, range.endContainer, + `endContainer of getTargetRanges()[${i}] of "beforeinput" event for position changing of absolute position does not match ${aDescription}`); + is(targetRanges[i].endOffset, range.endOffset, + `endOffset of getTargetRanges()[${i}] of "beforeinput" event for position changing of absolute position does not match ${aDescription}`); + } + } + } else { + is(targetRanges.length, 0, + `getTargetRanges() of "${aEvent.type}" event for position changing of absolute position should return empty array ${aDescription}`); + } + } + + content.addEventListener("beforeinput", onInput); + content.addEventListener("input", onInput); + + // Click on the correct resizer + synthesizeMouse(target, basePosX, basePosY, {type: "mousedown"}); + // Drag it delta pixels to the right and bottom (or maybe left and top!) + synthesizeMouse(target, basePosX + deltaX, basePosY + deltaY, {type: "mousemove"}); + // Release the mouse button + synthesizeMouse(target, basePosX + deltaX, basePosY + deltaY, {type: "mouseup"}); + + inputEventExpected = false; + + // Move the mouse delta more pixels to the same direction to make sure that the + // resize operation has stopped. + synthesizeMouse(target, basePosX + deltaX * 2, basePosY + deltaY * 2, {type: "mousemove"}); + + // Click outside of the editor to hide the resizers + ok(true, "waiting selectionchange to select outside the target element"); + let promiseSelectionExitEvent = waitForSelectionChange(); + synthesizeMouseAtCenter(outOfEditor, {}); + await promiseSelectionExitEvent; + + // Get the new dimensions for the target + // XXX I don't know why we need 2px margin to check this on Android. + // Fortunately, this test checks whether objects are resizable + // actually. So, bigger difference is okay. + let newRect = target.getBoundingClientRect(); + isfuzzy(newRect.width, rect.width + expectedDeltaX, 2, description + "The width should be increased by " + expectedDeltaX + " pixels"); + isfuzzy(newRect.height, rect.height + expectedDeltaY, 2, description + "The height should be increased by " + expectedDeltaY + "pixels"); + + content.removeEventListener("beforeinput", onInput); + content.removeEventListener("input", onInput); + } + + // Account for changes in the resizing behavior when we're trying to preserve + // the aspect ration of image. + // ignoredGrowth means we don't change the size of a dimension because otherwise + // the aspect ratio would change undesirably. + // needlessGrowth means that we change the size of a dimension perpendecular to + // the mouse movement axis in order to preserve the aspect ratio. + // reversedGrowth means that we change the size of a dimension in the opposite + // direction to the mouse movement in order to maintain the aspect ratio. + const ignoredGrowth = aPreserveRatio ? 0 : 1; + const needlessGrowth = aPreserveRatio ? 1 : 0; + const reversedGrowth = aPreserveRatio ? -1 : 1; + + /* eslint-disable no-multi-spaces */ + + // top resizer + await testResizer(W / 2, 0, -10, -10, 0, 10); + await testResizer(W / 2, 0, -10, 0, 0, 0); + await testResizer(W / 2, 0, -10, 10, 0, -10); + await testResizer(W / 2, 0, 0, -10, 0, 10); + await testResizer(W / 2, 0, 0, 0, 0, 0); + await testResizer(W / 2, 0, 0, 10, 0, -10); + await testResizer(W / 2, 0, 10, -10, 0, 10); + await testResizer(W / 2, 0, 10, 0, 0, 0); + await testResizer(W / 2, 0, 10, 10, 0, -10); + + // top right resizer + await testResizer( W, 0, -10, -10, -10 * reversedGrowth, 10); + await testResizer( W, 0, -10, 0, -10 * ignoredGrowth, 0); + await testResizer( W, 0, -10, 10, -10, -10); + await testResizer( W, 0, 0, -10, 10 * needlessGrowth, 10); + await testResizer( W, 0, 0, 0, 0, 0); + await testResizer( W, 0, 0, 10, 0, -10 * ignoredGrowth); + await testResizer( W, 0, 10, -10, 10, 10); + await testResizer( W, 0, 10, 0, 10, 10 * needlessGrowth); + await testResizer( W, 0, 10, 10, 10, -10 * reversedGrowth); + + // right resizer + await testResizer( W, H / 2, -10, -10, -10, 0); + await testResizer( W, H / 2, -10, 0, -10, 0); + await testResizer( W, H / 2, -10, 10, -10, 0); + await testResizer( W, H / 2, 0, -10, 0, 0); + await testResizer( W, H / 2, 0, 0, 0, 0); + await testResizer( W, H / 2, 0, 10, 0, 0); + await testResizer( W, H / 2, 10, -10, 10, 0); + await testResizer( W, H / 2, 10, 0, 10, 0); + await testResizer( W, H / 2, 10, 10, 10, 0); + + // bottom right resizer + await testResizer( W, H, -10, -10, -10, -10); + await testResizer( W, H, -10, 0, -10 * ignoredGrowth, 0); + await testResizer( W, H, -10, 10, -10 * reversedGrowth, 10); + await testResizer( W, H, 0, -10, 0, -10 * ignoredGrowth); + await testResizer( W, H, 0, 0, 0, 0); + await testResizer( W, H, 0, 10, 10 * needlessGrowth, 10); + await testResizer( W, H, 10, -10, 10, -10 * reversedGrowth); + await testResizer( W, H, 10, 0, 10, 10 * needlessGrowth); + await testResizer( W, H, 10, 10, 10, 10); + + // bottom resizer + await testResizer(W / 2, H, -10, -10, 0, -10); + await testResizer(W / 2, H, -10, 0, 0, 0); + await testResizer(W / 2, H, -10, 10, 0, 10); + await testResizer(W / 2, H, 0, -10, 0, -10); + await testResizer(W / 2, H, 0, 0, 0, 0); + await testResizer(W / 2, H, 0, 10, 0, 10); + await testResizer(W / 2, H, 10, -10, 0, -10); + await testResizer(W / 2, H, 10, 0, 0, 0); + await testResizer(W / 2, H, 10, 10, 0, 10); + + // bottom left resizer + await testResizer( 0, H, -10, -10, 10, -10 * reversedGrowth); + await testResizer( 0, H, -10, 0, 10, 10 * needlessGrowth); + await testResizer( 0, H, -10, 10, 10, 10); + await testResizer( 0, H, 0, -10, 0, -10 * ignoredGrowth); + await testResizer( 0, H, 0, 0, 0, 0); + await testResizer( 0, H, 0, 10, 10 * needlessGrowth, 10); + await testResizer( 0, H, 10, -10, -10, -10); + await testResizer( 0, H, 10, 0, -10 * ignoredGrowth, 0); + await testResizer( 0, H, 10, 10, -10 * reversedGrowth, 10); + + // left resizer + await testResizer( 0, H / 2, -10, -10, 10, 0); + await testResizer( 0, H / 2, -10, 0, 10, 0); + await testResizer( 0, H / 2, -10, 10, 10, 0); + await testResizer( 0, H / 2, 0, -10, 0, 0); + await testResizer( 0, H / 2, 0, 0, 0, 0); + await testResizer( 0, H / 2, 0, 10, 0, 0); + await testResizer( 0, H / 2, 10, -10, -10, 0); + await testResizer( 0, H / 2, 10, 0, -10, 0); + await testResizer( 0, H / 2, 10, 10, -10, 0); + + // top left resizer + await testResizer( 0, 0, -10, -10, 10, 10); + await testResizer( 0, 0, -10, 0, 10, 10 * needlessGrowth); + await testResizer( 0, 0, -10, 10, 10, -10 * reversedGrowth); + await testResizer( 0, 0, 0, -10, 10 * needlessGrowth, 10); + await testResizer( 0, 0, 0, 0, 0, 0); + await testResizer( 0, 0, 0, 10, 0, -10 * ignoredGrowth); + await testResizer( 0, 0, 10, -10, -10 * reversedGrowth, 10); + await testResizer( 0, 0, 10, 0, -10 * ignoredGrowth, 0); + await testResizer( 0, 0, 10, 10, -10, -10); + + /* eslint-enable no-multi-spaces */ + } + + const kTests = [ + { description: "Resizers for <img>", + innerHTML: "<img id=\"target\" src=\"green.png\">", + mayPreserveRatio: true, + isAbsolutePosition: false, + }, + { description: "Resizers for <table>", + innerHTML: "<table id=\"target\" border><tr><td>cell</td><td>cell</td></tr></table>", + mayPreserveRatio: false, + isAbsolutePosition: false, + }, + { description: "Resizers for absolute positioned <div>", + innerHTML: "<div id=\"target\" style=\"position: absolute; top: 50px; left: 50px;\">positioned</div>", + mayPreserveRatio: false, + isAbsolutePosition: true, + }, + ]; + + // Resizers for absolute positioned element and table element are available + // only when enableAbsolutePositionEditing or enableInlineTableEditing is + // enabled for each. So, let's enable them during testing resizers for + // absolute positioned elements or table elements. + for (const kTest of kTests) { + document.execCommand("enableAbsolutePositionEditing", false, kTest.isAbsolutePosition); + await doTest(kTest.description, kTest.mayPreserveRatio, kTest.innerHTML); + } + content.innerHTML = ""; + SimpleTest.finish(); +}); +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_root_element_replacement.html b/editor/libeditor/tests/test_root_element_replacement.html new file mode 100644 index 0000000000..45765d9ab2 --- /dev/null +++ b/editor/libeditor/tests/test_root_element_replacement.html @@ -0,0 +1,140 @@ +<html> +<head> + <title>Test for root element replacement</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" + href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTest); + +function runDesignModeTest(aDoc, aFocus, aNewSource) { + aDoc.designMode = "on"; + + if (aFocus) { + aDoc.documentElement.focus(); + } + + aDoc.open(); + aDoc.write(aNewSource); + aDoc.close(); + aDoc.documentElement.focus(); +} + +function runContentEditableTest(aDoc, aFocus, aNewSource) { + if (aFocus) { + aDoc.body.setAttribute("contenteditable", "true"); + aDoc.body.focus(); + } + + aDoc.open(); + aDoc.write(aNewSource); + aDoc.close(); + aDoc.getElementById("focus").focus(); +} + +var gTestIndex = 0; + +const kTests = [ + { description: "Replace to '<body></body>', designMode", + initializer: runDesignModeTest, + args: [ "<body></body>" ] }, + { description: "Replace to '<html><body></body></html>', designMode", + initializer: runDesignModeTest, + args: [ "<html><body></body></html>" ] }, + { description: "Replace to '<html> <body></body></html>', designMode", + initializer: runDesignModeTest, + args: [ "<html> <body></body></html>" ] }, + { description: "Replace to ' <html> <body></body></html>', designMode", + initializer: runDesignModeTest, + args: [ " <html> <body></body></html>" ] }, + + { description: "Replace to '<html contenteditable='true'><body></body></html>", + initializer: runContentEditableTest, + args: [ "<html contenteditable='true' id='focus'><body></body></html>" ] }, + { description: "Replace to '<html><body contenteditable='true'></body></html>", + initializer: runContentEditableTest, + args: [ "<html><body contenteditable='true' id='focus'></body></html>" ] }, + { description: "Replace to '<body contenteditable='true'></body>", + initializer: runContentEditableTest, + args: [ "<body contenteditable='true' id='focus'></body>" ] }, +]; + +var gIFrame; +var gSetFocusToIFrame = false; + +function onLoadIFrame() { + var frameDoc = gIFrame.contentWindow.document; + + var selCon = SpecialPowers.wrap(gIFrame).contentWindow. + docShell. + QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor). + getInterface(SpecialPowers.Ci.nsISelectionDisplay). + QueryInterface(SpecialPowers.Ci.nsISelectionController); + var utils = SpecialPowers.getDOMWindowUtils(window); + + // move focus to the HTML editor + const kTest = kTests[gTestIndex]; + ok(true, "Running " + kTest.description); + if (kTest.args.length == 1) { + kTest.initializer(frameDoc, gSetFocusToIFrame, kTest.args[0]); + ok(selCon.caretVisible, "caret isn't visible -- " + kTest.description); + } else { + ok(false, "kTests is broken at index=" + gTestIndex); + } + + is(utils.IMEStatus, utils.IME_STATUS_ENABLED, + "IME isn't enabled -- " + kTest.description); + synthesizeKey("A", { }, gIFrame.contentWindow); + synthesizeKey("B", { }, gIFrame.contentWindow); + synthesizeKey("C", { }, gIFrame.contentWindow); + var content = frameDoc.body.firstChild; + ok(content, "body doesn't have contents -- " + kTest.description); + if (content) { + is(content.nodeType, Node.TEXT_NODE, + "the content of body isn't text node -- " + kTest.description); + if (content.nodeType == Node.TEXT_NODE) { + is(content.data, "ABC", + "the content of body text isn't 'ABC' -- " + kTest.description); + is(frameDoc.body.innerHTML, "ABC", + "the innerHTML of body isn't 'ABC' -- " + kTest.description); + } + } + + document.getElementById("display").removeChild(gIFrame); + + // Do next test or finish the tests. + if (++gTestIndex < kTests.length) { + setTimeout(runTest, 0); + } else if (!gSetFocusToIFrame) { + gSetFocusToIFrame = true; + gTestIndex = 0; + setTimeout(runTest, 0); + } else { + SimpleTest.finish(); + } +} + +function runTest() { + gIFrame = document.createElement("iframe"); + document.getElementById("display").appendChild(gIFrame); + gIFrame.src = "about:blank"; + gIFrame.onload = onLoadIFrame; +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_sanitizer_on_paste.html b/editor/libeditor/tests/test_sanitizer_on_paste.html new file mode 100644 index 0000000000..09c6ff4fb2 --- /dev/null +++ b/editor/libeditor/tests/test_sanitizer_on_paste.html @@ -0,0 +1,48 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test pasting table rows</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<textarea></textarea> +<div contenteditable="true">Paste target</div> +<script> + SimpleTest.waitForExplicitFinish(); + function fail() { + ok(false, "Should not run event handlers."); + } + document.addEventListener('copy', ev => { + dump("IN LISTENER\n"); + const payload = `<svg><style><image href=file_sanitizer_on_paste.sjs onerror=fail() onload=fail()>` + + ev.preventDefault(); + ev.clipboardData.setData('text/html', payload); + ev.clipboardData.setData('text/plain', payload); + }); + + document.getElementsByTagName("textarea")[0].focus(); + synthesizeKey("c", { accelKey: true } /* aEvent*/); + + let div = document.getElementsByTagName("div")[0]; + div.focus(); + synthesizeKey("v", { accelKey: true } /* aEvent*/); + + let svg = div.firstChild; + is(svg.nodeName, "svg", "Node name should be svg"); + + let style = svg.firstChild; + if (style) { + is(style.firstChild, null, "Style should not have child nodes."); + } else { + ok(false, "Should have gotten a node."); + } + + var s = document.createElement("script"); + s.src = "file_sanitizer_on_paste.sjs?report=1"; + document.body.appendChild(s); +</script> +</body>
\ No newline at end of file diff --git a/editor/libeditor/tests/test_select_all_without_body.html b/editor/libeditor/tests/test_select_all_without_body.html new file mode 100644 index 0000000000..0e8dca9f3d --- /dev/null +++ b/editor/libeditor/tests/test_select_all_without_body.html @@ -0,0 +1,26 @@ +<html> +<head> + <title>Test select all in HTML editor without body element</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" + href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +window.open("file_select_all_without_body.html", "_blank", + "width=600,height=600"); + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_selection_move_commands.html b/editor/libeditor/tests/test_selection_move_commands.html new file mode 100644 index 0000000000..cb2f769a2a --- /dev/null +++ b/editor/libeditor/tests/test_selection_move_commands.html @@ -0,0 +1,225 @@ +<!doctype html> +<title>Test for nsSelectionMoveCommands</title> +<link rel=stylesheet href="/tests/SimpleTest/test.css"> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script type="text/javascript" src="/tests/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=454004">Mozilla Bug 454004</a> + +<iframe id="edit" width="200" height="100" src="about:blank"></iframe> + +<script> +SimpleTest.waitForExplicitFinish(); + +async function setup() { + await SpecialPowers.pushPrefEnv({set: [["general.smoothScroll", false]]}); +} + +async function* runTests() { + var e = document.getElementById("edit"); + var doc = e.contentDocument; + var win = e.contentWindow; + var root = doc.documentElement; + var body = doc.body; + + var sel = win.getSelection(); + + function testScrollCommand(cmd, expectTop) { + const { top } = root.getBoundingClientRect(); + + // XXX(krosylight): Android scrolls slightly more inside + // geckoview-test-verify-e10s CI job + const isAndroid = SpecialPowers.Services.appinfo.widgetToolkit == "android"; + const delta = isAndroid ? .150 : 0; + isfuzzy(top, -expectTop, delta, cmd); + } + + function testMoveCommand(cmd, expectNode, expectOffset) { + SpecialPowers.doCommand(window, cmd); + is(sel.isCollapsed, true, "collapsed after " + cmd); + is(sel.anchorNode, expectNode, "node after " + cmd); + is(sel.anchorOffset, expectOffset, "offset after " + cmd); + } + + function findChildNum(element, child) { + var i = 0; + var n = element.firstChild; + while (n && n != child) { + n = n.nextSibling; + ++i; + } + if (!n) + return -1; + return i; + } + + function testPageMoveCommand(cmd, expectOffset) { + SpecialPowers.doCommand(window, cmd); + is(sel.isCollapsed, true, "collapsed after " + cmd); + is(sel.anchorOffset, expectOffset, "offset after " + cmd); + return findChildNum(body, sel.anchorNode); + } + + function testSelectCommand(cmd, expectNode, expectOffset) { + var anchorNode = sel.anchorNode; + var anchorOffset = sel.anchorOffset; + SpecialPowers.doCommand(window, cmd); + is(sel.isCollapsed, false, "not collapsed after " + cmd); + is(sel.anchorNode, anchorNode, "anchor not moved after " + cmd); + is(sel.anchorOffset, anchorOffset, "anchor not moved after " + cmd); + is(sel.focusNode, expectNode, "node after " + cmd); + is(sel.focusOffset, expectOffset, "offset after " + cmd); + } + + function testPageSelectCommand(cmd, expectOffset) { + var anchorNode = sel.anchorNode; + var anchorOffset = sel.anchorOffset; + SpecialPowers.doCommand(window, cmd); + is(sel.isCollapsed, false, "not collapsed after " + cmd); + is(sel.anchorNode, anchorNode, "anchor not moved after " + cmd); + is(sel.anchorOffset, anchorOffset, "anchor not moved after " + cmd); + is(sel.focusOffset, expectOffset, "offset after " + cmd); + return findChildNum(body, sel.focusNode); + } + + function node(i) { + var n = body.firstChild; + while (i > 0) { + n = n.nextSibling; + --i; + } + return n; + } + + SpecialPowers.doCommand(window, "cmd_scrollBottom"); + yield; + testScrollCommand("cmd_scrollBottom", root.scrollHeight - 100); + SpecialPowers.doCommand(window, "cmd_scrollTop"); + yield; + testScrollCommand("cmd_scrollTop", 0); + + SpecialPowers.doCommand(window, "cmd_scrollPageDown"); + yield; + var pageHeight = -root.getBoundingClientRect().top; + ok(pageHeight > 0, "cmd_scrollPageDown works"); + ok(pageHeight <= 100, "cmd_scrollPageDown doesn't scroll too much"); + SpecialPowers.doCommand(window, "cmd_scrollBottom"); + yield; + SpecialPowers.doCommand(window, "cmd_scrollPageUp"); + yield; + testScrollCommand("cmd_scrollPageUp", root.scrollHeight - 100 - pageHeight); + + SpecialPowers.doCommand(window, "cmd_scrollTop"); + yield; + SpecialPowers.doCommand(window, "cmd_scrollLineDown"); + yield; + var lineHeight = -root.getBoundingClientRect().top; + ok(lineHeight > 0, "Can scroll by lines"); + SpecialPowers.doCommand(window, "cmd_scrollBottom"); + yield; + SpecialPowers.doCommand(window, "cmd_scrollLineUp"); + yield; + testScrollCommand("cmd_scrollLineUp", root.scrollHeight - 100 - lineHeight); + + var runSelectionTests = function() { + testMoveCommand("cmd_moveBottom", body, 23); + testMoveCommand("cmd_moveTop", node(0), 0); + testSelectCommand("cmd_selectBottom", body, 23); + SpecialPowers.doCommand(window, "cmd_moveBottom"); + testSelectCommand("cmd_selectTop", node(0), 0); + + SpecialPowers.doCommand(window, "cmd_moveTop"); + testMoveCommand("cmd_lineNext", node(2), 0); + testMoveCommand("cmd_linePrevious", node(0), 0); + testSelectCommand("cmd_selectLineNext", node(2), 0); + SpecialPowers.doCommand(window, "cmd_moveBottom"); + testSelectCommand("cmd_selectLinePrevious", node(20), 2); + + SpecialPowers.doCommand(window, "cmd_moveBottom"); + testMoveCommand("cmd_charPrevious", node(22), 1); + testMoveCommand("cmd_charNext", node(22), 2); + testSelectCommand("cmd_selectCharPrevious", node(22), 1); + SpecialPowers.doCommand(window, "cmd_moveTop"); + testSelectCommand("cmd_selectCharNext", node(0), 1); + + SpecialPowers.doCommand(window, "cmd_moveTop"); + testMoveCommand("cmd_endLine", node(0), 1); + testMoveCommand("cmd_beginLine", node(0), 0); + testSelectCommand("cmd_selectEndLine", node(0), 1); + SpecialPowers.doCommand(window, "cmd_moveBottom"); + testSelectCommand("cmd_selectBeginLine", node(22), 0); + + SpecialPowers.doCommand(window, "cmd_moveBottom"); + testMoveCommand("cmd_wordPrevious", node(22), 0); + testMoveCommand("cmd_wordNext", body, 23); + testSelectCommand("cmd_selectWordPrevious", node(22), 0); + SpecialPowers.doCommand(window, "cmd_moveTop"); + testSelectCommand("cmd_selectWordNext", body, 1); + + SpecialPowers.doCommand(window, "cmd_moveTop"); + var lineNum = testPageMoveCommand("cmd_movePageDown", 0); + ok(lineNum > 0, "cmd_movePageDown works"); + SpecialPowers.doCommand(window, "cmd_moveBottom"); + SpecialPowers.doCommand(window, "cmd_beginLine"); + is(testPageMoveCommand("cmd_movePageUp", 0), 22 - lineNum, "cmd_movePageUp"); + + SpecialPowers.doCommand(window, "cmd_moveTop"); + is(testPageSelectCommand("cmd_selectPageDown", 0), lineNum, "cmd_selectPageDown"); + SpecialPowers.doCommand(window, "cmd_moveBottom"); + SpecialPowers.doCommand(window, "cmd_beginLine"); + is(testPageSelectCommand("cmd_selectPageUp", 0), 22 - lineNum, "cmd_selectPageUp"); + }; + + await SpecialPowers.pushPrefEnv({set: [["layout.word_select.eat_space_to_next_word", false]]}); + runSelectionTests(); + await SpecialPowers.pushPrefEnv({set: [["layout.word_select.eat_space_to_next_word", true]]}); + runSelectionTests(); +} + +function cleanup() { + SimpleTest.finish(); +} + +async function testRunner() { + var e = document.getElementById("edit"); + var doc = e.contentDocument; + var win = e.contentWindow; + var body = doc.body; + + body.style.fontSize = "16px"; + body.style.lineHeight = "16px"; + body.style.height = "400px"; + body.style.padding = "0px"; + body.style.margin = "0px"; + body.style.borderWidth = "0px"; + + doc.designMode = "on"; + body.innerHTML = "1<br>2<br>3<br>4<br>5<br>6<br>7<br>8<br>9<br>10<br>11<br>12<br>"; + win.focus(); + // Flush out layout to make sure that the subdocument will be the size we + // expect by the time we try to scroll it. + is(body.getBoundingClientRect().height, 400, + "Body height should be what we set it to"); + + await waitToClearOutAnyPotentialScrolls(win); + + let curTest = runTests(); + while (true) { + let promise = new Promise(resolve => { win.addEventListener("scroll", () => { SimpleTest.executeSoon(resolve); }, {once: true, capture: true}); }); + if ((await curTest.next()).done) { + break; + } + // wait for the scroll + await promise; + // clear out any other pending scrolls + await waitToClearOutAnyPotentialScrolls(win); + } +} + +SimpleTest.waitForFocus(function() { + setup() + .then(() => testRunner()) + .then(() => cleanup()) + .catch(err => ok(false, err)); +}, window); + +</script> diff --git a/editor/libeditor/tests/test_setting_value_longer_than_maxlength_with_setUserInput.html b/editor/libeditor/tests/test_setting_value_longer_than_maxlength_with_setUserInput.html new file mode 100644 index 0000000000..5c2290c094 --- /dev/null +++ b/editor/libeditor/tests/test_setting_value_longer_than_maxlength_with_setUserInput.html @@ -0,0 +1,69 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1320229 + +Checks if `eReplaceText` has consistent behavior regardless of whether the +field has an associated editor---this test works by calling `setUserInput()` +before the element gets focus.) + +Inspired by `dom/html/test/forms/test_MozEditableElement_setUserInput.html`. +--> + +<head> + <title>Test setting value longer than maxlength with setUserInput</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> + +<body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1320229">Mozilla Bug 1320229</a> + <div id="display"> + </div> + <div id="content"></div> + <pre id="test"> +</pre> + + <script class="testbody" type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + // eslint-disable-next-line complexity + SimpleTest.waitForFocus(() => { + let content = document.getElementById("content"); + for (let test of [ + { + element: "input", + type: "password", + maxlength: "9", + input: { before: "aaaaaaaa", after: "bbbbbbbb" }, + result: { before: "aaaaaaaa", after: "bbbbbbbb"} + }, + { + element: "input", + type: "password", + maxlength: "4", + input: { before: "aaaaaaaa", after: "bbbbbbbb" }, + result: { before: "aaaaaaaa", after: "bbbbbbbb"} + }, + ]) { + let tag = `<${test.element} type="${test.type}" maxlength="${test.maxlength}">` + content.innerHTML = `${tag}`; + content.scrollTop; // Flush pending layout. + let target = content.firstChild; + + // Before setting focus, editor of the element may have not been created yet. + SpecialPowers.wrap(target).setUserInput(test.input.before); + is(target.value, test.result.before, `setUserInput("${test.input.before}") before ${tag} gets focus should set its value to "${test.result.before}"`); + + // Now we do the same after setting focus. + target.focus(); + SpecialPowers.wrap(target).setUserInput(test.input.after); + is(target.value, test.result.after, `setUserInput("${test.input.after}") after ${tag} gets focus should set its value to "${test.result.after}"`); + } + + SimpleTest.finish(); + }); + </script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_spellcheck_pref.html b/editor/libeditor/tests/test_spellcheck_pref.html new file mode 100644 index 0000000000..8b0ea7f303 --- /dev/null +++ b/editor/libeditor/tests/test_spellcheck_pref.html @@ -0,0 +1,22 @@ +<html> +<head> + <title>Test if spellcheck is turned on</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" + href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + + is(SpecialPowers.getIntPref("layout.spellcheckDefault"), 1, "Check if the layout.spellcheckDefault pref is turned on"); + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_state_change_on_reframe.html b/editor/libeditor/tests/test_state_change_on_reframe.html new file mode 100644 index 0000000000..c74bf46ea7 --- /dev/null +++ b/editor/libeditor/tests/test_state_change_on_reframe.html @@ -0,0 +1,29 @@ +<!doctype html> +<title>Test for state change not bogusly changing during reframe (bug 1528644)</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<style> + .reframe { display: table } + input:invalid { color: red; } + input:valid { color: green; } +</style> +<form> + <input type="text" required minlength="4" onkeypress="this.classList.toggle('reframe')"> +</form> +<script> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + let input = document.querySelector("input"); + input.focus(); + requestAnimationFrame(() => { + synthesizeKey("a"); + requestAnimationFrame(() => { + requestAnimationFrame(() => { + ok(!input.validity.valid); + is(getComputedStyle(input).display, "table"); + SimpleTest.finish(); + }); + }); + }); +}); +</script> diff --git a/editor/libeditor/tests/test_textarea_value_not_include_cr.html b/editor/libeditor/tests/test_textarea_value_not_include_cr.html new file mode 100644 index 0000000000..f687792f8b --- /dev/null +++ b/editor/libeditor/tests/test_textarea_value_not_include_cr.html @@ -0,0 +1,95 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test for HTMLTextAreaElement.value not returning value including CR</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none;"> + +</div> + +<textarea></textarea> + +<script> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(async () => { + /** + * This test should check only the cases with emulating complicated + * key events and using XPCOM methods. If you need to add simple textcases, + * use testing/web-platform/tests/html/semantics/forms/the-textarea-element/value-defaultValue-textContent.html + * instead + */ + let textarea = document.querySelector("textarea"); + textarea.focus(); + // This shouldn't occur because widget handles control characters if they + // receive native key event, but for backward compatibility as XUL platform, + // let's check this. + synthesizeKey("\r"); + is(textarea.value, "\n", "Inputting \\r from keyboard event should be converted to \\n"); + + textarea.value = ""; + await new Promise((resolve, reject) => { + SimpleTest.waitForClipboard( + "ab\ncd\nef", + () => { SpecialPowers.clipboardCopyString("ab\r\ncd\ref"); }, + resolve, + () => { + ok(false, "Clipboard copy failed"); + reject(); + } + ); + }); + synthesizeKey("v", {accelKey: true}); + is(textarea.value, "ab\ncd\nef", "Pasting \\r from clipboard should be converted to \\n"); + + textarea.value = ""; + SpecialPowers.wrap(textarea).editor.insertText("ab\r\ncd\ref"); + is(textarea.value, "ab\ncd\nef", "Inserting \\r with nsIEditor.insertText() should be converted to \\n"); + + textarea.value = ""; + synthesizeCompositionChange( + { "composition": + { "string": "ab\r\ncd\ref", + "clauses": + [ + { "length": 9, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 9, "length": 0 }, + }); + is(textarea.value, "ab\ncd\nef", "Inserting \\r with composition should be converted to \\n"); + + synthesizeComposition({type: "compositioncommitasis"}); + is(textarea.value, "ab\ncd\nef", "Inserting \\r with committing composition should be converted to \\n"); + + // We don't need to test spellchecker features on Android because of unsupported. + if (!navigator.appVersion.includes("Android")) { + ok(true, "Waiting to run spellchecker..."); + let inlineSpellchecker = SpecialPowers.wrap(textarea).editor.getInlineSpellChecker(true); + textarea.value = "abx "; + await new Promise(resolve => { + const { onSpellCheck } = SpecialPowers.ChromeUtils.importESModule( + "resource://testing-common/AsyncSpellCheckTestHelper.sys.mjs" + ); + onSpellCheck(textarea, () => { + SimpleTest.executeSoon(resolve); + }); + }); + let anonymousDivElement = SpecialPowers.wrap(textarea).editor.rootElement; + let misspelledWord = inlineSpellchecker.getMisspelledWord(anonymousDivElement.firstChild, 0); + is(misspelledWord.startOffset, 0, "Misspelled word start should be 0"); + is(misspelledWord.endOffset, 3, "Misspelled word end should be 3"); + inlineSpellchecker.replaceWord(anonymousDivElement.firstChild, 0, "ab\r\ncd\ref"); + is(textarea.value, "ab\ncd\nef ", "Inserting \\r from spellchecker should be converted to \\n"); + } + + SimpleTest.finish(); +}); +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_texteditor_keyevent_handling.html b/editor/libeditor/tests/test_texteditor_keyevent_handling.html new file mode 100644 index 0000000000..2c80181b3c --- /dev/null +++ b/editor/libeditor/tests/test_texteditor_keyevent_handling.html @@ -0,0 +1,471 @@ +<html> +<head> + <title>Test for key event handler of text editor</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> +</head> +<body> +<div id="display"> + <input type="text" id="inputField"> + <input type="password" id="passwordField"> + <textarea id="textarea"></textarea> +</div> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests, window); + +var inputField = document.getElementById("inputField"); +var passwordField = document.getElementById("passwordField"); +var textarea = document.getElementById("textarea"); + +const kIsMac = navigator.platform.includes("Mac"); +const kIsWin = navigator.platform.includes("Win"); +const kIsLinux = navigator.platform.includes("Linux"); + +async function runTests() { + var fm = SpecialPowers.Services.focus; + + var capturingPhase = { fired: false, prevented: false }; + var bubblingPhase = { fired: false, prevented: false }; + + var listener = { + handleEvent: function _hv(aEvent) { + is(aEvent.type, "keypress", "unexpected event is handled"); + switch (aEvent.eventPhase) { + case aEvent.CAPTURING_PHASE: + capturingPhase.fired = true; + capturingPhase.prevented = aEvent.defaultPrevented; + break; + case aEvent.BUBBLING_PHASE: + bubblingPhase.fired = true; + bubblingPhase.prevented = aEvent.defaultPrevented; + aEvent.preventDefault(); // prevent the browser default behavior + break; + default: + ok(false, "event is handled in unexpected phase"); + } + }, + }; + + function check(aDescription, + aFiredOnCapture, aFiredOnBubbling, aPreventedOnBubbling) { + function getDesciption(aExpected) { + return aDescription + (aExpected ? " wasn't " : " was "); + } + + is(capturingPhase.fired, aFiredOnCapture, + getDesciption(aFiredOnCapture) + "fired on capture phase"); + is(bubblingPhase.fired, aFiredOnBubbling, + getDesciption(aFiredOnBubbling) + "fired on bubbling phase"); + + // If the event is fired on bubbling phase and it was already prevented + // on capture phase, it must be prevented on bubbling phase too. + if (capturingPhase.prevented) { + todo(false, aDescription + + " was consumed already, so, we cannot test the editor behavior actually"); + aPreventedOnBubbling = true; + } + + is(bubblingPhase.prevented, aPreventedOnBubbling, + getDesciption(aPreventedOnBubbling) + "prevented on bubbling phase"); + } + + var parentElement = document.getElementById("display"); + SpecialPowers.addSystemEventListener(parentElement, "keypress", listener, + true); + SpecialPowers.addSystemEventListener(parentElement, "keypress", listener, + false); + + async function doTest(aElement, aDescription, aIsSingleLine, aIsReadonly) { + function reset(aText) { + capturingPhase.fired = false; + capturingPhase.prevented = false; + bubblingPhase.fired = false; + bubblingPhase.prevented = false; + aElement.value = aText; + } + + if (document.activeElement) { + document.activeElement.blur(); + } + + aDescription += ": "; + + aElement.focus(); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, aDescription + "failed to move focus"); + + // Backspace key: + // If native key bindings map the key combination to something, it's consumed. + // If editor is readonly, it doesn't consume. + // If editor is editable, it consumes backspace and shift+backspace. + // Otherwise, editor doesn't consume the event but the native key + // bindings on nsTextControlFrame may consume it. + reset(""); + synthesizeKey("KEY_Backspace"); + check(aDescription + "Backspace", true, true, true); + + reset(""); + synthesizeKey("KEY_Backspace", {shiftKey: true}); + check(aDescription + "Shift+Backspace", true, true, true); + + reset(""); + synthesizeKey("KEY_Backspace", {ctrlKey: true}); + // Win: cmd_deleteWordBackward + check(aDescription + "Ctrl+Backspace", + true, true, aIsReadonly || kIsWin); + + reset(""); + synthesizeKey("KEY_Backspace", {altKey: true}); + // Win: cmd_undo + // Mac: cmd_deleteWordBackward + check(aDescription + "Alt+Backspace", + true, true, aIsReadonly || kIsWin || kIsMac); + + reset(""); + synthesizeKey("KEY_Backspace", {metaKey: true}); + check(aDescription + "Meta+Backspace", true, true, aIsReadonly || kIsMac); + + // Delete key: + // If native key bindings map the key combination to something, it's consumed. + // If editor is readonly, it doesn't consume. + // If editor is editable, delete is consumed. + // Otherwise, editor doesn't consume the event but the native key + // bindings on nsTextControlFrame may consume it. + reset(""); + synthesizeKey("KEY_Delete"); + // Linux: native handler + // Mac: cmd_deleteCharForward + check(aDescription + "Delete", + true, true, !aIsReadonly || kIsLinux || kIsMac); + + reset(""); + // Win: cmd_cutOrDelete + // Linux: cmd_cut + // Mac: cmd_deleteCharForward + synthesizeKey("KEY_Delete", {shiftKey: true}); + check(aDescription + "Shift+Delete", + true, true, true); + + reset(""); + synthesizeKey("KEY_Delete", {ctrlKey: true}); + // Win: cmd_deleteWordForward + // Linux: cmd_copy + check(aDescription + "Ctrl+Delete", + true, true, kIsWin || kIsLinux); + + reset(""); + synthesizeKey("KEY_Delete", {altKey: true}); + // Mac: cmd_deleteWordForward + check(aDescription + "Alt+Delete", + true, true, kIsMac); + + reset(""); + synthesizeKey("KEY_Delete", {metaKey: true}); + // Linux: native handler consumed. + check(aDescription + "Meta+Delete", + true, true, kIsLinux); + + // XXX input.value returns "\n" when it's empty, so, we should use dummy + // value ("a") for the following tests. + + // Return key: + // If editor is readonly, it doesn't consume. + // If editor is editable and not single line editor, it consumes Return + // and Shift+Return. + // Otherwise, editor doesn't consume the event. + reset("a"); + synthesizeKey("KEY_Enter"); + check(aDescription + "Return", + true, true, !aIsSingleLine && !aIsReadonly); + is(aElement.value, !aIsSingleLine && !aIsReadonly ? "a\n" : "a", + aDescription + "Return"); + + reset("a"); + synthesizeKey("KEY_Enter", {shiftKey: true}); + check(aDescription + "Shift+Return", + true, true, !aIsSingleLine && !aIsReadonly); + is(aElement.value, !aIsSingleLine && !aIsReadonly ? "a\n" : "a", + aDescription + "Shift+Return"); + + reset("a"); + synthesizeKey("KEY_Enter", {ctrlKey: true}); + check(aDescription + "Ctrl+Return", true, true, false); + is(aElement.value, "a", aDescription + "Ctrl+Return"); + + reset("a"); + synthesizeKey("KEY_Enter", {altKey: true}); + check(aDescription + "Alt+Return", true, true, false); + is(aElement.value, "a", aDescription + "Alt+Return"); + + reset("a"); + synthesizeKey("KEY_Enter", {metaKey: true}); + check(aDescription + "Meta+Return", true, true, false); + is(aElement.value, "a", aDescription + "Meta+Return"); + + // Tab key: + // Editor consumes tab key event unless any modifier keys are pressed. + reset("a"); + synthesizeKey("KEY_Tab"); + check(aDescription + "Tab", true, true, false); + is(aElement.value, "a", aDescription + "Tab"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Tab)"); + aElement.focus(); + + reset("a"); + synthesizeKey("KEY_Tab", {shiftKey: true}); + check(aDescription + "Shift+Tab", true, true, false); + is(aElement.value, "a", aDescription + "Shift+Tab"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Shift+Tab)"); + + // Ctrl+Tab should be consumed by tabbrowser at keydown, so, keypress + // event should never be fired. + reset("a"); + synthesizeKey("KEY_Tab", {ctrlKey: true}); + check(aDescription + "Ctrl+Tab", false, false, false); + is(aElement.value, "a", aDescription + "Ctrl+Tab"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Ctrl+Tab)"); + + reset("a"); + synthesizeKey("KEY_Tab", {altKey: true}); + check(aDescription + "Alt+Tab", true, true, false); + is(aElement.value, "a", aDescription + "Alt+Tab"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Alt+Tab)"); + + reset("a"); + synthesizeKey("KEY_Tab", {metaKey: true}); + check(aDescription + "Meta+Tab", true, true, false); + is(aElement.value, "a", aDescription + "Meta+Tab"); + is(SpecialPowers.unwrap(fm.focusedElement), aElement, + aDescription + "focus moved unexpectedly (Meta+Tab)"); + + // Esc key: + // In all cases, esc key events are not consumed + reset("abc"); + synthesizeKey("KEY_Escape"); + check(aDescription + "Esc", true, true, false); + + reset("abc"); + synthesizeKey("KEY_Escape", {shiftKey: true}); + check(aDescription + "Shift+Esc", true, true, false); + + reset("abc"); + synthesizeKey("KEY_Escape", {ctrlKey: true}); + check(aDescription + "Ctrl+Esc", true, true, false); + + reset("abc"); + synthesizeKey("KEY_Escape", {altKey: true}); + check(aDescription + "Alt+Esc", true, true, false); + + reset("abc"); + synthesizeKey("KEY_Escape", {metaKey: true}); + check(aDescription + "Meta+Esc", true, true, false); + + // typical typing tests: + reset(""); + sendString("M"); + check(aDescription + "M", true, true, !aIsReadonly); + sendString("o"); + check(aDescription + "o", true, true, !aIsReadonly); + sendString("z"); + check(aDescription + "z", true, true, !aIsReadonly); + sendString("i"); + check(aDescription + "i", true, true, !aIsReadonly); + sendString("l"); + check(aDescription + "l", true, true, !aIsReadonly); + sendString("l"); + check(aDescription + "l", true, true, !aIsReadonly); + sendString("a"); + check(aDescription + "a", true, true, !aIsReadonly); + sendString(" "); + check(aDescription + "' '", true, true, !aIsReadonly); + is(aElement.value, !aIsReadonly ? "Mozilla " : "", + aDescription + "typed \"Mozilla \""); + + // typing non-BMP character: + async function test_typing_surrogate_pair( + aTestPerSurrogateKeyPress, + aTestIllFormedUTF16KeyValue = false + ) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.event.keypress.dispatch_once_per_surrogate_pair", !aTestPerSurrogateKeyPress], + ["dom.event.keypress.key.allow_lone_surrogate", aTestIllFormedUTF16KeyValue], + ], + }); + reset(""); + let events = []; + function pushIntoEvents(aEvent) { + events.push(aEvent); + } + function getEventData(aKeyboardEventOrInputEvent) { + if (!aKeyboardEventOrInputEvent) { + return "{}"; + } + switch (aKeyboardEventOrInputEvent.type) { + case "keydown": + case "keypress": + case "keyup": + return `{ type: "${aKeyboardEventOrInputEvent.type}", key="${ + aKeyboardEventOrInputEvent.key + }", charCode=0x${ + aKeyboardEventOrInputEvent.charCode.toString(16).toUpperCase() + } }`; + default: + return `{ type: "${aKeyboardEventOrInputEvent.type}", inputType="${ + aKeyboardEventOrInputEvent.inputType + }", data="${aKeyboardEventOrInputEvent.data}" }`; + } + } + function getEventArrayData(aEvents) { + if (!aEvents.length) { + return "[]"; + } + let result = "[\n"; + for (const e of aEvents) { + result += ` ${getEventData(e)}\n`; + } + return result + "]"; + } + aElement.addEventListener("keydown", pushIntoEvents); + aElement.addEventListener("keypress", pushIntoEvents); + aElement.addEventListener("keyup", pushIntoEvents); + aElement.addEventListener("beforeinput", pushIntoEvents); + aElement.addEventListener("input", pushIntoEvents); + synthesizeKey("\uD842\uDFB7"); + aElement.removeEventListener("keydown", pushIntoEvents); + aElement.removeEventListener("keypress", pushIntoEvents); + aElement.removeEventListener("keyup", pushIntoEvents); + aElement.removeEventListener("beforeinput", pushIntoEvents); + aElement.removeEventListener("input", pushIntoEvents); + const settingDescription = + `aTestPerSurrogateKeyPress=${ + aTestPerSurrogateKeyPress + }, aTestIllFormedUTF16KeyValue=${aTestIllFormedUTF16KeyValue}`; + const allowIllFormedUTF16 = + aTestPerSurrogateKeyPress && aTestIllFormedUTF16KeyValue; + + check(`${aDescription}, ${settingDescription}a surrogate pair`, true, true, !aIsReadonly); + is( + aElement.value, + !aIsReadonly ? "\uD842\uDFB7" : "", + `${aDescription}, ${ + settingDescription + }, The typed surrogate pair should've been inserted` + ); + if (aIsReadonly) { + is( + getEventArrayData(events), + getEventArrayData( + // eslint-disable-next-line no-nested-ternary + aTestPerSurrogateKeyPress + ? ( + allowIllFormedUTF16 + ? [ + { type: "keydown", key: "\uD842\uDFB7", charCode: 0 }, + { type: "keypress", key: "\uD842", charCode: 0xD842 }, + { type: "keypress", key: "\uDFB7", charCode: 0xDFB7 }, + { type: "keyup", key: "\uD842\uDFB7", charCode: 0 }, + ] + : [ + { type: "keydown", key: "\uD842\uDFB7", charCode: 0 }, + { type: "keypress", key: "\uD842\uDFB7", charCode: 0xD842 }, + { type: "keypress", key: "", charCode: 0xDFB7 }, + { type: "keyup", key: "\uD842\uDFB7", charCode: 0 }, + ] + ) + : [ + { type: "keydown", key: "\uD842\uDFB7", charCode: 0 }, + { type: "keypress", key: "\uD842\uDFB7", charCode: 0x20BB7 }, + { type: "keyup", key: "\uD842\uDFB7", charCode: 0 }, + ] + ), + `${aDescription}, ${ + settingDescription + }, Typing a surrogate pair in readonly editor should not cause input events` + ); + } else { + is( + getEventArrayData(events), + getEventArrayData( + // eslint-disable-next-line no-nested-ternary + aTestPerSurrogateKeyPress + ? ( + allowIllFormedUTF16 + ? [ + { type: "keydown", key: "\uD842\uDFB7", charCode: 0 }, + { type: "keypress", key: "\uD842", charCode: 0xD842 }, + { type: "beforeinput", data: "\uD842", inputType: "insertText" }, + { type: "input", data: "\uD842", inputType: "insertText" }, + { type: "keypress", key: "\uDFB7", charCode: 0xDFB7 }, + { type: "beforeinput", data: "\uDFB7", inputType: "insertText" }, + { type: "input", data: "\uDFB7", inputType: "insertText" }, + { type: "keyup", key: "\uD842\uDFB7", charCode: 0 }, + ] + : [ + { type: "keydown", key: "\uD842\uDFB7", charCode: 0 }, + { type: "keypress", key: "\uD842\uDFB7", charCode: 0xD842 }, + { type: "beforeinput", data: "\uD842\uDFB7", inputType: "insertText" }, + { type: "input", data: "\uD842\uDFB7", inputType: "insertText" }, + { type: "keypress", key: "", charCode: 0xDFB7 }, + { type: "keyup", key: "\uD842\uDFB7", charCode: 0 }, + ] + ) + : [ + { type: "keydown", key: "\uD842\uDFB7", charCode: 0 }, + { type: "keypress", key: "\uD842\uDFB7", charCode: 0x20BB7 }, + { type: "beforeinput", data: "\uD842\uDFB7", inputType: "insertText" }, + { type: "input", data: "\uD842\uDFB7", inputType: "insertText" }, + { type: "keyup", key: "\uD842\uDFB7", charCode: 0 }, + ] + ), + `${aDescription}, ${ + settingDescription + }, Typing a surrogate pair in editor should cause input events` + ); + } + } + await test_typing_surrogate_pair(true, true); + await test_typing_surrogate_pair(true, false); + await test_typing_surrogate_pair(false); + } + + await doTest(inputField, "<input type=\"text\">", true, false); + + inputField.setAttribute("readonly", "readonly"); + await doTest(inputField, "<input type=\"text\" readonly>", true, true); + + await doTest(passwordField, "<input type=\"password\">", true, false); + + passwordField.setAttribute("readonly", "readonly"); + await doTest(passwordField, "<input type=\"password\" readonly>", true, true); + + await doTest(textarea, "<textarea>", false, false); + + textarea.setAttribute("readonly", "readonly"); + await doTest(textarea, "<textarea readonly>", false, true); + + SpecialPowers.removeSystemEventListener(parentElement, "keypress", listener, + true); + SpecialPowers.removeSystemEventListener(parentElement, "keypress", listener, + false); + + SimpleTest.finish(); +} + +</script> +</body> + +</html> diff --git a/editor/libeditor/tests/test_texteditor_textnode.html b/editor/libeditor/tests/test_texteditor_textnode.html new file mode 100644 index 0000000000..d93f714649 --- /dev/null +++ b/editor/libeditor/tests/test_texteditor_textnode.html @@ -0,0 +1,52 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Test for Bug 1713334</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +<input id="input"> +<textarea id="textarea"></textarea> +<script> +"use strict"; + +function assertChild(div, content) { + const name = div.parentElement.localName; + is(div.firstChild.nodeType, Node.TEXT_NODE, `<${name}>: The first node of the root element must be a text node`); + is(div.firstChild.textContent, content, `<${name}>: The content of the text node is wrong`); +} + +function test(element) { + element.focus(); + + const { rootElement } = SpecialPowers.wrap(element).editor; + assertChild(rootElement, ""); + + element.value = ""; + assertChild(rootElement, ""); + + element.value = "foo" + assertChild(rootElement, "foo"); + + element.value = ""; + assertChild(rootElement, ""); + + element.value = "foo"; + const selection = + SpecialPowers.wrap(element). + editor. + selectionController.getSelection( + SpecialPowers.Ci.nsISelectionController.SELECTION_NORMAL + ); + selection.setBaseAndExtent(rootElement, 0, rootElement, 1); + document.execCommand("delete"); + assertChild(rootElement, ""); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + test(document.all.input); + test(document.all.textarea); + + SimpleTest.finish(); +}); +</script> diff --git a/editor/libeditor/tests/test_texteditor_tripleclick_setvalue.html b/editor/libeditor/tests/test_texteditor_tripleclick_setvalue.html new file mode 100644 index 0000000000..65ae2ce7e4 --- /dev/null +++ b/editor/libeditor/tests/test_texteditor_tripleclick_setvalue.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Test for TextEditor triple click and SetValue</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<style> + body { + font: 1em/1 Ahem + } +</style> +<input id="input" value="foo bar baz"> +<script> + SimpleTest.waitForExplicitFinish(); + SimpleTest.waitForFocus(() => { + const { input } = document.all; + input.focus(); + synthesizeMouse(input, 5, 5, { clickCount: 3 }, window); + is(input.selectionStart, 0, "selectionStart should be 0"); + is(input.selectionEnd, input.value.length, "selectionEnd should be the end of the value"); + synthesizeKey("KEY_Backspace"); + is(input.value, "", ".value should be empty"); + + input.value = "hmm"; + is(input.value, "hmm", ".value must be set"); + SimpleTest.finish(); + }); +</script> diff --git a/editor/libeditor/tests/test_texteditor_wrapping_long_line.html b/editor/libeditor/tests/test_texteditor_wrapping_long_line.html new file mode 100644 index 0000000000..14a445bbb0 --- /dev/null +++ b/editor/libeditor/tests/test_texteditor_wrapping_long_line.html @@ -0,0 +1,45 @@ +<!DOCTYPE> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1733878 +--> +<head> + <meta charset="UTF-8" /> + <title>Test for bug 1733878</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script> + /** Test for bug 1733878 **/ + window.addEventListener("DOMContentLoaded", (event) => { + SimpleTest.waitForExplicitFinish(); + SimpleTest.waitForFocus(function() { + document.body.textContent = ""; // It would be \n\n otherwise... + synthesizeMouseAtCenter(document.body, {}); + + var editor = getEditor(); + is(document.body.textContent, "", "Initial body check"); + editor.rewrap(false); + is(document.body.textContent, "", "Initial body check after rewrap"); + + document.body.innerHTML = "> hello world this_is_a_very_long_long_word_which_has_a_length_higher_than_the_max_column"; + editor.rewrap(true); + is(document.body.innerText, "> hello world\n> this_is_a_very_long_long_word_which_has_a_length_higher_than_the_max_column", "Rewrapped"); + + SimpleTest.finish(); + }); + }); + + function getEditor() { + var Ci = SpecialPowers.Ci; + var editingSession = SpecialPowers.wrap(window).docShell.editingSession; + var editor = editingSession.getEditorForWindow(window); + editor.QueryInterface(Ci.nsIHTMLEditor); + editor.QueryInterface(Ci.nsIEditorMailSupport); + editor.flags |= SpecialPowers.Ci.nsIEditor.eEditorPlaintextMask; + return editor; + } + </script> +</head> +<body contenteditable></body> +</html> diff --git a/editor/libeditor/tests/test_typing_at_edge_of_anchor.html b/editor/libeditor/tests/test_typing_at_edge_of_anchor.html new file mode 100644 index 0000000000..3cb1a2de2d --- /dev/null +++ b/editor/libeditor/tests/test_typing_at_edge_of_anchor.html @@ -0,0 +1,476 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for typing after a link</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> +</head> +<body> +<p id="display"></p> +<div id="content"> + <div contenteditable style="padding: 5px;"></div> + <iframe srcdoc="<!doctype html><html><body style='padding: 5px;'></body></html>"></iframe> +</div> + +<pre id="test"> +<script> + +// Currently, `Home` and `End` key press can be tested only on Windows. +// Therefore, we need to check the running platform. +const kCanTestHomeEndKeys = + navigator.platform.indexOf("Win") == 0 || navigator.appVersion.includes("Android"); + +SimpleTest.waitForExplicitFinish(); + +// When some tests newly fail by your change but the new result is expected by +// testing/web-platform/tests/editing/other/typing-around-link-element-at-(non-)collapsed-selection.tentative.html, +// it's okay to delete the test from here. +function doTest(editor, selection, win) { + const kDescription = + editor.getAttribute("contenteditable") === null ? "designMode" : "contenteditable"; + + // At start + editor.innerHTML = "<p>abc<a href=\"about:blank\">def</a></p>"; + selection.collapse(editor.querySelector("a[href]").firstChild, 1); + synthesizeKey("KEY_ArrowLeft"); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, "<p>abc<a href=\"about:blank\">XYdef</a></p>", + `${kDescription}: Typing X and YY at start of the <a> element should insert to the start of it when caret is moved from middle of it`); + + editor.innerHTML = "<p>abc<a href=\"about:blank\">def</a></p>"; + selection.collapse(editor.querySelector("a[href]").previousSibling, 2); + synthesizeKey("KEY_ArrowRight"); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, "<p>abcXY<a href=\"about:blank\">def</a></p>", + `${kDescription}: Typing X and Y at start of the <a> element should insert to the end of the preceding text node when caret is moved from middle of the preceding text node`); + + editor.innerHTML = "<p><b>abc</b><a href=\"about:blank\">def</a></p>"; + selection.collapse(editor.querySelector("a[href]").firstChild, 1); + synthesizeKey("KEY_ArrowLeft"); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, "<p><b>abc</b><a href=\"about:blank\">XYdef</a></p>", + `${kDescription}: Typing X and Y at start of the <a> element should insert to the start of it when caret is moved from middle of it (following <b>)`); + + editor.innerHTML = "<p><b>abc</b><a href=\"about:blank\">def</a></p>"; + selection.collapse(editor.querySelector("b").firstChild, 2); + synthesizeKey("KEY_ArrowRight"); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, "<p><b>abcXY</b><a href=\"about:blank\">def</a></p>", + `${kDescription}: Typing X and Y at start of the <a> element should insert to end of the following <b> when caret is moved from middle of the <b>`); + + editor.innerHTML = "<p>abc</p><p><a href=\"about:blank\">def</a></p>"; + selection.collapse(editor.querySelector("a[href]").firstChild, 1); + synthesizeKey("KEY_ArrowLeft"); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, "<p>abc</p><p><a href=\"about:blank\">XYdef</a></p>", + `${kDescription}: Typing X and Y at start of the <a> element should insert to start of it when caret is moved from middle of it (following <p>)`); + + editor.innerHTML = "<p>abc</p><p><a href=\"about:blank\">def</a></p>"; + selection.collapse(editor.querySelector("p").firstChild, 3); + synthesizeKey("KEY_ArrowRight"); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, "<p>abc</p><p>XY<a href=\"about:blank\">def</a></p>", + `${kDescription}: Typing X and Y at start of the <a> element should insert to start of new text node when caret is moved from the previous paragraph`); + + editor.innerHTML = "<p>abc</p><p><b><a href=\"about:blank\">def</a></b></p>"; + selection.collapse(editor.querySelector("a[href]").firstChild, 1); + synthesizeKey("KEY_ArrowLeft"); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, "<p>abc</p><p><b><a href=\"about:blank\">XYdef</a></b></p>", + `${kDescription}: Typing X and Y at start of the <a> element should insert to start of it when caret is moved from middle of it (following <p> and wrapped by <b>)`); + + editor.innerHTML = "<p>abc</p><p><b><a href=\"about:blank\">def</a></b></p>"; + selection.collapse(editor.querySelector("p").firstChild, 3); + synthesizeKey("KEY_ArrowRight"); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, "<p>abc</p><p><b>XY<a href=\"about:blank\">def</a></b></p>", + `${kDescription}: Typing X and Y at start of the <a> element should insert to start of wrapper <b> when caret is moved from the previous paragraph`); + + editor.innerHTML = "<p>abc</p><p><a href=\"about:blank\">def</a></p>"; + selection.collapse(editor.querySelector("p").firstChild, 3); + synthesizeKey("KEY_Delete"); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, "<p>abcXY<a href=\"about:blank\">def</a></p>", + `${kDescription}: Typing X and Y at start of the <a> element after joining paragraphs with Delete should insert to end of the preceding text node`); + + editor.innerHTML = "<p><b>abc</b></p><p><a href=\"about:blank\">def</a></p>"; + selection.collapse(editor.querySelector("b").firstChild, 3); + synthesizeKey("KEY_Delete"); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, "<p><b>abcXY</b><a href=\"about:blank\">def</a></p>", + `${kDescription}: Typing X and Y at start of the <a> element after joining paragraphs with Delete should insert to end of the preceding <b>`); + + editor.innerHTML = "<p>abc<br></p><p><a href=\"about:blank\">def</a></p>"; + selection.collapse(editor.querySelector("p").firstChild, 3); + synthesizeKey("KEY_Delete"); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, "<p>abcXY<a href=\"about:blank\">def</a></p>", + `${kDescription}: Typing X and Y at start of the <a> element after joining paragraphs with Delete should insert to end of the preceding text node (invisible <br>)`); + + editor.innerHTML = "<p><b>abc</b><br></p><p><a href=\"about:blank\">def</a></p>"; + selection.collapse(editor.querySelector("b").firstChild, 3); + synthesizeKey("KEY_Delete"); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, "<p><b>abcXY</b><a href=\"about:blank\">def</a></p>", + `${kDescription}: Typing X and Y at start of the <a> element after joining paragraphs with Delete should insert to start of the preceding <b> (invisible <br>)`); + + if (kCanTestHomeEndKeys) { + editor.innerHTML = "<p><a href=\"about:blank\">abc</a></p>"; + selection.collapse(editor.querySelector("a[href]").firstChild, 1); + synthesizeKey("KEY_Home"); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, "<p>XY<a href=\"about:blank\">abc</a></p>", + `${kDescription}: Typing X and Y at start of the <a> element after \`Home\` key press should insert it outside the link`); + + editor.innerHTML = "<p><b><a href=\"about:blank\">abc</a></b></p>"; + selection.collapse(editor.querySelector("a[href]").firstChild, 1); + synthesizeKey("KEY_Home"); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, "<p><b>XY<a href=\"about:blank\">abc</a></b></p>", + `${kDescription}: Typing X and Y at start of the <a> element wrapped by <b> after \`Home\` key press should insert it outside the link but in the <b>`); + + editor.innerHTML = "<p><a href=\"about:blank\">abc</a></p>"; + selection.collapse(editor.querySelector("a[href]").firstChild, 0); + synthesizeKey("KEY_Home"); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, "<p>XY<a href=\"about:blank\">abc</a></p>", + `${kDescription}: Typing X and Y at start of the <a> element after \`Home\` key press without selection change should insert it outside the link`); + + editor.innerHTML = "<p><b><a href=\"about:blank\">abc</a></b></p>"; + selection.collapse(editor.querySelector("a[href]").firstChild, 0); + synthesizeKey("KEY_Home"); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, "<p><b>XY<a href=\"about:blank\">abc</a></b></p>", + `${kDescription}: Typing X and Y at start of the <a> element wrapped by <b> after \`Home\` key press without selection change should insert it outside the link but in the <b>`); + } + + editor.innerHTML = "<p><a href=\"about:blank\">abc</a></p>"; + selection.collapse(editor.querySelector("a[href]").firstChild, 0); + synthesizeKey("KEY_ArrowLeft"); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, "<p>XY<a href=\"about:blank\">abc</a></p>", + `${kDescription}: Typing X and Y at start of the <a> element after \`ArrowLeft\` key press without selection change should insert it outside the link`); + + editor.innerHTML = "<p><b><a href=\"about:blank\">abc</a></b></p>"; + selection.collapse(editor.querySelector("a[href]").firstChild, 0); + synthesizeKey("KEY_ArrowLeft"); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, "<p><b>XY<a href=\"about:blank\">abc</a></b></p>", + `${kDescription}: Typing X and Y at start of the <a> element wrapped by <b> after \`ArrowLeft\` key press without selection change should insert it outside the link but in the <b>`); + + for (const startPos of [2, 0]) { + editor.innerHTML = '<p><a href="about:blank">abc</a></p>'; + selection.collapse(editor.querySelector("a").firstChild, startPos); + synthesizeMouse(editor.querySelector("a"), 2, 2, {}, win); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, '<p><a href="about:blank">XYabc</a></p>', + `${kDescription}: Typing X and Y after clicking left half of the first character of the link ${ + startPos === 0 ? "(without caret move)" : "" + } should insert them to start of the link`); + + editor.innerHTML = '<p><a href="about:blank">abc</a></p>'; + selection.collapse(editor.querySelector("a").firstChild, startPos); + synthesizeMouse(editor.querySelector("a"), -2, 2, {}, win); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, '<p>XY<a href="about:blank">abc</a></p>', + `${kDescription}: Typing X and Y after clicking "before" the first character of the link ${ + startPos === 0 ? "(without caret move)" : "" + } should insert them to before the link`); + + editor.innerHTML = '<p><b><a href="about:blank">abc</a></b></p>'; + selection.collapse(editor.querySelector("a").firstChild, startPos); + synthesizeMouse(editor.querySelector("a"), 2, 2, {}, win); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, '<p><b><a href="about:blank">XYabc</a></b></p>', + `${kDescription}: Typing X and Y after clicking left half of the first character of the link in <b> ${ + startPos === 0 ? "(without caret move)" : "" + } should insert them to start of the link`); + + editor.innerHTML = '<p><b><a href="about:blank">abc</a></b></p>'; + selection.collapse(editor.querySelector("a").firstChild, startPos); + synthesizeMouse(editor.querySelector("a"), -2, 2, {}, win); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, '<p><b>XY<a href="about:blank">abc</a></b></p>', + `${kDescription}: Typing X and Y after clicking "before" the first character of the link in <b> ${ + startPos === 0 ? "(without caret move)" : "" + } should insert them to before the link`); + } + + // At end + editor.innerHTML = "<p><a href=\"about:blank\">abc</a>def</p>"; + selection.collapse(editor.querySelector("a[href]").firstChild, 2); + synthesizeKey("KEY_ArrowRight"); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, "<p><a href=\"about:blank\">abcXY</a>def</p>", + `${kDescription}: Typing X and Y at end of the <a> element should insert to the end of it when caret is moved from middle of it`); + + editor.innerHTML = "<p><a href=\"about:blank\">abc</a>def</p>"; + selection.collapse(editor.querySelector("a[href]").nextSibling, 1); + synthesizeKey("KEY_ArrowLeft"); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, "<p><a href=\"about:blank\">abc</a>XYdef</p>", + `${kDescription}: Typing X and Y at end of the <a> element should insert to the start of the following text node when caret is moved from middle of the following text node`); + + editor.innerHTML = "<p><a href=\"about:blank\">abc</a><b>def</b></p>"; + selection.collapse(editor.querySelector("a[href]").firstChild, 2); + synthesizeKey("KEY_ArrowRight"); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, "<p><a href=\"about:blank\">abcXY</a><b>def</b></p>", + `${kDescription}: Typing X and Y at end of the <a> element should insert to the end of it when caret is moved from middle of it (followed by <b>)`); + + editor.innerHTML = "<p><a href=\"about:blank\">abc</a><b>def</b></p>"; + selection.collapse(editor.querySelector("b").firstChild, 1); + synthesizeKey("KEY_ArrowLeft"); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, "<p><a href=\"about:blank\">abc</a><b>XYdef</b></p>", + `${kDescription}: Typing X and Y at end of the <a> element should insert to start of the following <b> when caret is moved from middle of the following <b> element`); + + editor.innerHTML = "<p><a href=\"about:blank\">abc</a></p><p>def</p>"; + selection.collapse(editor.querySelector("a[href]").firstChild, 2); + synthesizeKey("KEY_ArrowRight"); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, "<p><a href=\"about:blank\">abcXY</a></p><p>def</p>", + `${kDescription}: Typing X and Y at end of the <a> element should insert to end of it when caret is moved from middle of it (followed by <p>)`); + + editor.innerHTML = "<p><a href=\"about:blank\">abc</a></p><p>def</p>"; + selection.collapse(editor.querySelector("p + p").firstChild, 0); + synthesizeKey("KEY_ArrowLeft"); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, "<p><a href=\"about:blank\">abc</a>XY</p><p>def</p>", + `${kDescription}: Typing X and Y at end of the <a> element should insert to start of new text node when caret is moved from the following paragraph`); + + editor.innerHTML = "<p><b><a href=\"about:blank\">abc</a></b></p><p>def</p>"; + selection.collapse(editor.querySelector("p + p").firstChild, 0); + synthesizeKey("KEY_ArrowLeft"); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, "<p><b><a href=\"about:blank\">abc</a>XY</b></p><p>def</p>", + `${kDescription}: Typing X and Y at end of the <a> element should insert to end of wrapper <b> when caret is moved from the following paragraph`); + + editor.innerHTML = "<p><a href=\"about:blank\"><b>abc</b></a></p><p>def</p>"; + selection.collapse(editor.querySelector("p + p").firstChild, 0); + synthesizeKey("KEY_ArrowLeft"); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, "<p><a href=\"about:blank\"><b>abc</b></a><b>XY</b></p><p>def</p>", + `${kDescription}: Typing X and Y at end of the <a> element should insert to start of new <b> when caret is moved from the following paragraph`); + + // I'm not sure whether this behavior should be changed or not, but inconsistent with the case of Backspace from start of <a href>. + editor.innerHTML = "<p><a href=\"about:blank\">abc</a></p><p>def</p>"; + selection.collapse(editor.querySelector("a[href]").firstChild, 3); + synthesizeKey("KEY_Delete"); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, "<p><a href=\"about:blank\">abc</a>XYdef</p>", + `${kDescription}: Typing X and Y at end of the <a> element after joining paragraphs with Delete should insert to end of it`); + todo_is(editor.innerHTML, "<p><a href=\"about:blank\">abcXY</a>def</p>", + `${kDescription}: Typing X and Y at end of the <a> element after joining paragraphs with Delete should insert to end of it`); + + // I'm not sure whether this behavior should be changed or not, but inconsistent with the case of Backspace from start of <a href>. + editor.innerHTML = "<p><a href=\"about:blank\">abc</a></p><p><b>def</b></p>"; + selection.collapse(editor.querySelector("a[href]").firstChild, 3); + synthesizeKey("KEY_Delete"); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, "<p><a href=\"about:blank\">abc</a>XY<b>def</b></p>", + `${kDescription}: Typing X and Y at end of the <a> element after joining paragraphs with Delete should insert to end of it (following <p> has <b>)`); + todo_is(editor.innerHTML, "<p><a href=\"about:blank\">abcXY</a><b>def</b></p>", + `${kDescription}: Typing X and Y at end of the <a> element after joining paragraphs with Delete should insert to end of it (following <p> has <b>)`); + + editor.innerHTML = "<p><a href=\"about:blank\">abc</a></p><p>def</p>"; + selection.collapse(editor.querySelector("p + p").firstChild, 0); + synthesizeKey("KEY_Backspace"); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, "<p><a href=\"about:blank\">abc</a>XYdef</p>", + `${kDescription}: Typing X and Y at end of the <a> element after joining paragraphs with Backspace should insert to start of next text node`); + + editor.innerHTML = "<p><a href=\"about:blank\">abc</a></p><p><b>def</b></p>"; + selection.collapse(editor.querySelector("b").firstChild, 0); + synthesizeKey("KEY_Backspace"); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, "<p><a href=\"about:blank\">abc</a>XY<b>def</b></p>", + `${kDescription}: Typing X and Y at end of the <a> element after joining paragraphs with Backspace should insert before next <b>`); + + editor.innerHTML = "<p><a href=\"about:blank\">abc</a><br></p><p>def</p>"; + selection.collapse(editor.querySelector("p + p").firstChild, 0); + synthesizeKey("KEY_Backspace"); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, "<p><a href=\"about:blank\">abc</a>XYdef</p>", + `${kDescription}: Typing X and Y at end of the <a> element after joining paragraphs with Backspace should insert to start of next text node (invisible <br>)`); + + editor.innerHTML = "<p><a href=\"about:blank\">abc</a><br></p><p><b>def</b></p>"; + selection.collapse(editor.querySelector("b").firstChild, 0); + synthesizeKey("KEY_Backspace"); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, "<p><a href=\"about:blank\">abc</a>XY<b>def</b></p>", + `${kDescription}: Typing X and Y at end of the <a> element after joining paragraphs with Backspace should insert before next <b> (invisible <br>)`); + + if (kCanTestHomeEndKeys) { + editor.innerHTML = "<p><a href=\"about:blank\">abc</a></p>"; + selection.collapse(editor.querySelector("a[href]").firstChild, 1); + synthesizeKey("KEY_End"); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, "<p><a href=\"about:blank\">abc</a>XY</p>", + `${kDescription}: Typing X and Y at end of the <a> element after \`End\` key press should insert it outside the link`); + + editor.innerHTML = "<p><b><a href=\"about:blank\">abc</a></b><br></p>"; + selection.collapse(editor.querySelector("a[href]").firstChild, 1); + synthesizeKey("KEY_End"); + synthesizeKey("X"); + synthesizeKey("Y"); + todo_is(editor.innerHTML, "<p><b><a href=\"about:blank\">abc</a></b>XY<br></p>", + `${kDescription}: Typing X and Y at end of the <a> element wrapped by <b> and followed by an invisible <br> after \`End\` key press should insert it outside the link but in the <b>`); + + editor.innerHTML = "<p><a href=\"about:blank\">abc</a></p>"; + selection.collapse(editor.querySelector("a[href]").firstChild, 1); + synthesizeKey("KEY_End"); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, "<p><a href=\"about:blank\">abc</a>XY</p>", + `${kDescription}: Typing X and Y at end of the <a> element after \`End\` key press without selection change should insert it outside the link`); + + editor.innerHTML = "<p><b><a href=\"about:blank\">abc</a></b></p>"; + selection.collapse(editor.querySelector("a[href]").firstChild, editor.querySelector("a[href]").firstChild.length); + synthesizeKey("KEY_End"); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, "<p><b><a href=\"about:blank\">abc</a>XY</b></p>", + `${kDescription}: Typing X and Y at end of the <a> element wrapped by <b> after \`End\` key press without selection change should insert it outside the link but in the <b>`); + + editor.innerHTML = "<p><b><a href=\"about:blank\">abc</a></b><br></p>"; + selection.collapse(editor.querySelector("a[href]").firstChild, editor.querySelector("a[href]").firstChild.length); + synthesizeKey("KEY_End"); + synthesizeKey("X"); + synthesizeKey("Y"); + todo_is(editor.innerHTML, "<p><b><a href=\"about:blank\">abc</a></b>XY<br></p>", + `${kDescription}: Typing X and Y at end of the <a> element wrapped by <b> and followed by an invisible <br> after \`End\` key press without selection change should insert it outside the link but in the <b>`); + } + + editor.innerHTML = "<p><b><a href=\"about:blank\">abc</a></b></p>"; + selection.collapse(editor.querySelector("a[href]").firstChild, editor.querySelector("a[href]").firstChild.length); + synthesizeKey("KEY_ArrowRight"); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, "<p><b><a href=\"about:blank\">abc</a>XY</b></p>", + `${kDescription}: Typing X and Y at end of the <a> element wrapped by <b> after \`ArrowRight\` key press without selection change should insert it outside the link but in the <b>`); + + editor.innerHTML = "<p><b><a href=\"about:blank\">abc</a></b><br></p>"; + selection.collapse(editor.querySelector("a[href]").firstChild, editor.querySelector("a[href]").firstChild.length); + synthesizeKey("KEY_ArrowRight"); + synthesizeKey("X"); + synthesizeKey("Y"); + todo_is(editor.innerHTML, "<p><b><a href=\"about:blank\">abc</a></b>XY<br></p>", + `${kDescription}: Typing X and Y at end of the <a> element wrapped by <b> and followed by an invisible <br> after \`ArrowRight\` key press without selection change should insert it outside the link but in the <b>`); + + for (const startPos of [1, 3]) { + editor.innerHTML = '<p><a href="about:blank">abc</a></p>'; + selection.collapse(editor.querySelector("a").firstChild, startPos); + synthesizeMouse(editor.querySelector("a"), editor.querySelector("a").getBoundingClientRect().width - 1, 1, {}, win); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, '<p><a href="about:blank">abcXY</a></p>', + `${kDescription}: Typing X and Y after clicking right half of the last character of the link ${ + startPos === 3 ? "(without caret move)" : "" + } should insert them to end of the link`); + + editor.innerHTML = '<p><a href="about:blank">abc</a></p>'; + selection.collapse(editor.querySelector("a").firstChild, startPos); + synthesizeMouse(editor.querySelector("a"), editor.querySelector("a").getBoundingClientRect().width + 1, 1, {}, win); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, '<p><a href="about:blank">abc</a>XY</p>', + `${kDescription}: Typing X and Y after clicking "after" the last character of the link ${ + startPos === 3 ? "(without caret move)" : "" + } should insert them to after the link`); + + editor.innerHTML = '<p><b><a href="about:blank">abc</a></b></p>'; + selection.collapse(editor.querySelector("a").firstChild, startPos); + synthesizeMouse(editor.querySelector("a"), editor.querySelector("a").getBoundingClientRect().width - 1, 1, {}, win); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, '<p><b><a href="about:blank">abcXY</a></b></p>', + `${kDescription}: Typing X and Y after clicking right half of the last character of the link in <b> ${ + startPos === 3 ? "(without caret move)" : "" + } should insert them to end of the link`); + + editor.innerHTML = '<p><b><a href="about:blank">abc</a></b></p>'; + selection.collapse(editor.querySelector("a").firstChild, startPos); + synthesizeMouse(editor.querySelector("a"), editor.querySelector("a").getBoundingClientRect().width + 1, 1, {}, win); + synthesizeKey("X"); + synthesizeKey("Y"); + is(editor.innerHTML, '<p><b><a href="about:blank">abc</a>XY</b></p>', + `${kDescription}: Typing X and Y after clicking "after" the last character of the link in <b> ${ + startPos === 3 ? "(without caret move)" : "" + } should insert them to after the link`); + } + + // at middle of link + editor.innerHTML = '<p><a href="about:blank">abcde</a></p>'; + selection.collapse(editor.querySelector("a").firstChild, 0); + synthesizeMouseAtCenter(editor.querySelector("a"), {}, win); + synthesizeKey("X"); + synthesizeKey("Y"); + if (selection.focusOffset == 4) { + is(editor.innerHTML, '<p><a href="about:blank">abXYcde</a></p>', + `${kDescription}: Typing X and Y after clicking center of the link should insert them to the link`); + } else if (selection.focusOffset == 5) { + is(editor.innerHTML, '<p><a href="about:blank">abcXYde</a></p>', + `${kDescription}: Typing X and Y after clicking center of the link should insert them to the link`); + } else { + ok(false, `selection is collapsed at unexpected offset got ${selection.focusOffset} but expected 2 or 3`); + } +} + +SimpleTest.waitForFocus(() => { + let editor = document.querySelector("[contenteditable]"); + let selection = getSelection(); + editor.focus(); + doTest(editor, selection, window); + + let iframe = document.querySelector("iframe"); + editor = iframe.contentDocument.body; + selection = iframe.contentWindow.getSelection(); + iframe.contentDocument.designMode = "on"; + iframe.contentWindow.focus(); + doTest(editor, selection, iframe.contentWindow); + + SimpleTest.finish(); +}); +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_undo_after_spellchecker_replaces_word.html b/editor/libeditor/tests/test_undo_after_spellchecker_replaces_word.html new file mode 100644 index 0000000000..09a7d63d22 --- /dev/null +++ b/editor/libeditor/tests/test_undo_after_spellchecker_replaces_word.html @@ -0,0 +1,179 @@ +<!DOCTYPE html> +<html> +<head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<div id="display"></div> +<textarea id="textarea">abc abx abc</textarea> +<div id="contenteditable" contenteditable>abc abx abc</div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> +"use strict"; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.expectAssertions(0, 1); // In a11y module +SimpleTest.waitForFocus(async () => { + await SpecialPowers.pushPrefEnv({ + set: [ + // Even if `beforeinput` events for `setUserInput()` calls are not + // allowed to cancel, correcting the spells should be cancelable for + // compatibility with the other browsers. + ["dom.input_event.allow_to_cancel_set_user_input", false], + ], + }); + + let textarea = document.getElementById("textarea"); + let textEditor = SpecialPowers.wrap(textarea).editor; + let contenteditable = document.getElementById("contenteditable"); + let htmlEditor = SpecialPowers.wrap(window).docShell.editingSession.getEditorForWindow(window); + + function doTest(aElement, aRootElement, aEditor, aDescription) { + return new Promise(resolve => { + let inlineSpellChecker = aEditor.getInlineSpellChecker(true); + + aElement.focus(); + + function checkInputEvent(aEvent, aInputType, aData, aDataTransfer, aTargetRanges, aDescriptionInner) { + ok(aEvent instanceof InputEvent, + `${aDescription}"${aEvent.type}" event should be dispatched with InputEvent interface ${aDescriptionInner}`); + is(aEvent.cancelable, aEvent.type === "beforeinput" && aInputType !== "", + `${aDescription}"${aEvent.type}" event should ${aEvent.type === "beforeinput" ? "be" : "be never"} cancelable ${aDescriptionInner}`); + is(aEvent.bubbles, true, + `${aDescription}"${aEvent.type}" event should always bubble ${aDescriptionInner}`); + is(aEvent.inputType, aInputType, + `${aDescription}inputType of "${aEvent.type}" event should be "${aInputType}" ${aDescriptionInner}`); + is(aEvent.data, aData, + `${aDescription}data of "${aEvent.type}" event should be ${aData} ${aDescriptionInner}`); + if (aDataTransfer === null) { + is(aEvent.dataTransfer, null, + `${aDescription}dataTransfer of "${aEvent.type}" event should be null ${aDescriptionInner}`); + } else { + for (let item of aDataTransfer) { + is(aEvent.dataTransfer.getData(item.type), item.data, + `${aDescription}dataTransfer of "${aEvent.type}" event should have ${item.data} as ${item.type} ${aDescriptionInner}`); + } + } + let targetRanges = aEvent.getTargetRanges(); + if (aTargetRanges.length === 0) { + is(targetRanges.length, 0, + `${aDescription}getTargetRange() of "${aEvent.type}" event should return empty array ${aDescriptionInner}`); + } else { + is(targetRanges.length, aTargetRanges.length, + `${aDescription}getTargetRange() of "${aEvent.type}" event should return static range array ${aDescriptionInner}`); + if (targetRanges.length == aTargetRanges.length) { + for (let i = 0; i < targetRanges.length; i++) { + is(targetRanges[i].startContainer, aTargetRanges[i].startContainer, + `${aDescription}startContainer of getTargetRanges()[${i}] of "${aEvent.type}" event does not match ${aDescriptionInner}`); + is(targetRanges[i].startOffset, aTargetRanges[i].startOffset, + `${aDescription}startOffset of getTargetRanges()[${i}] of "${aEvent.type}" event does not match ${aDescriptionInner}`); + is(targetRanges[i].endContainer, aTargetRanges[i].endContainer, + `${aDescription}endContainer of getTargetRanges()[${i}] of "${aEvent.type}" event does not match ${aDescriptionInner}`); + is(targetRanges[i].endOffset, aTargetRanges[i].endOffset, + `${aDescription}endOffset of getTargetRanges()[${i}] of "${aEvent.type}" event does not match ${aDescriptionInner}`); + } + } + } + } + + let beforeInputEvents = []; + let inputEvents = []; + function onBeforeInput(aEvent) { + beforeInputEvents.push(aEvent); + } + function onInput(aEvent) { + inputEvents.push(aEvent); + } + + function getValue() { + return aElement === textarea ? aElement.value : aElement.innerHTML; + } + + const { maybeOnSpellCheck } = SpecialPowers.ChromeUtils.importESModule( + "resource://testing-common/AsyncSpellCheckTestHelper.sys.mjs" + ); + maybeOnSpellCheck(aElement, () => { + SimpleTest.executeSoon(() => { + aElement.addEventListener("beforeinput", onBeforeInput); + aElement.addEventListener("input", onInput); + + let misspelledWord = inlineSpellChecker.getMisspelledWord(aRootElement.firstChild, 5); + is(misspelledWord.startOffset, 4, + `${aDescription}Misspelled word should start from 4`); + is(misspelledWord.endOffset, 7, + `${aDescription}Misspelled word should end at 7`); + beforeInputEvents = []; + inputEvents = []; + inlineSpellChecker.replaceWord(aRootElement.firstChild, 5, "aux"); + is(getValue(), "abc aux abc", + `${aDescription}'abx' should be replaced with 'aux'`); + is(beforeInputEvents.length, 1, + `${aDescription}Only one "beforeinput" event should be fired when replacing a word with spellchecker`); + if (aElement === textarea) { + checkInputEvent(beforeInputEvents[0], "insertReplacementText", "aux", null, [], + "when replacing a word with spellchecker"); + } else { + checkInputEvent(beforeInputEvents[0], "insertReplacementText", null, [{type: "text/plain", data: "aux"}], + [{startContainer: aRootElement.firstChild, startOffset: 4, + endContainer: aRootElement.firstChild, endOffset: 7}], + "when replacing a word with spellchecker"); + } + is(inputEvents.length, 1, + `${aDescription}Only one "input" event should be fired when replacing a word with spellchecker`); + if (aElement === textarea) { + checkInputEvent(inputEvents[0], "insertReplacementText", "aux", null, [], + "when replacing a word with spellchecker"); + } else { + checkInputEvent(inputEvents[0], "insertReplacementText", null, [{type: "text/plain", data: "aux"}], [], + "when replacing a word with spellchecker"); + } + + beforeInputEvents = []; + inputEvents = []; + synthesizeKey("z", { accelKey: true }); + is(getValue(), "abc abx abc", + `${aDescription}'abx' should be restored by undo`); + is(beforeInputEvents.length, 1, + `${aDescription}Only one "beforeinput" event should be fired when undoing the replacing word`); + checkInputEvent(beforeInputEvents[0], "historyUndo", null, null, [], + "when undoing the replacing word"); + is(inputEvents.length, 1, + `${aDescription}Only one "input" event should be fired when undoing the replacing word`); + checkInputEvent(inputEvents[0], "historyUndo", null, null, [], + "when undoing the replacing word"); + + beforeInputEvents = []; + inputEvents = []; + synthesizeKey("z", { accelKey: true, shiftKey: true }); + is(getValue(), "abc aux abc", + `${aDescription}'aux' should be restored by redo`); + is(beforeInputEvents.length, 1, + `${aDescription}Only one "beforeinput" event should be fired when redoing the replacing word`); + checkInputEvent(beforeInputEvents[0], "historyRedo", null, null, [], + "when redoing the replacing word"); + is(inputEvents.length, 1, + `${aDescription}Only one "input" event should be fired when redoing the replacing word`); + checkInputEvent(inputEvents[0], "historyRedo", null, null, [], + "when redoing the replacing word"); + + aElement.removeEventListener("beforeinput", onBeforeInput); + aElement.removeEventListener("input", onInput); + + resolve(); + }); + }); + }); + } + + await doTest(textarea, textEditor.rootElement, textEditor, "<textarea>: "); + await doTest(contenteditable, contenteditable, htmlEditor, "<div contenteditable>: "); + + SimpleTest.finish(); +}); +</script> +</body> +</html> diff --git a/editor/libeditor/tests/test_undo_redo_stack_after_setting_value.html b/editor/libeditor/tests/test_undo_redo_stack_after_setting_value.html new file mode 100644 index 0000000000..6f33ccaf01 --- /dev/null +++ b/editor/libeditor/tests/test_undo_redo_stack_after_setting_value.html @@ -0,0 +1,168 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1473515 +--> +<html> +<head> + <title>Test for Bug 1473515</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1473515">Mozilla Bug 1473515</a> +<p id="display"></p> +<div id="content" style="display: none;"> + +</div> + +<input id="input"> +<textarea id="textarea"></textarea> + +<pre id="test"> + +<script class="testbody" type="application/javascript"> +"use strict"; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.expectAssertions(0, 1); // In a11y module +SimpleTest.waitForFocus(() => { + let editableElements = [ + document.getElementById("input"), + document.getElementById("textarea"), + ]; + for (let editableElement of editableElements) { + function checkInputEvent(aEvent, aInputType, aData, aDescription) { + ok(aEvent instanceof InputEvent, + `"${aEvent.type}" event should be dispatched with InputEvent interface ${aDescription}`); + is(aEvent.cancelable, aEvent.type === "beforeinput", + `"${aEvent.type}" event should ${aEvent.type === "beforeinput" ? "be" : "be never"} cancelable ${aDescription}`); + is(aEvent.bubbles, true, + `"${aEvent.type}" event should always bubble ${aDescription}`); + is(aEvent.inputType, aInputType, + `inputType of "${aEvent.type}" event should be "${aInputType}" ${aDescription}`); + is(aEvent.data, aData, + `data of "${aEvent.type}" event should be ${aData} ${aDescription}`); + is(aEvent.dataTransfer, null, + `dataTransfer of "${aEvent.type}" should be null ${aDescription}`); + is(aEvent.getTargetRanges().length, 0, + `getTargetRanges() of "${aEvent.type}" should return empty array ${aDescription}`); + } + + let beforeInputEvents = []; + let inputEvents = []; + function onBeforeInput(aEvent) { + beforeInputEvents.push(aEvent); + } + function onInput(aEvent) { + inputEvents.push(aEvent); + } + editableElement.addEventListener("beforeinput", onBeforeInput); + editableElement.addEventListener("input", onInput); + + editableElement.focus(); + + beforeInputEvents = []; + inputEvents = []; + synthesizeKey("a"); + is(beforeInputEvents.length, 1, + `Only one "beforeinput" event should be fired when inserting "a" with key on <${editableElement.tagName.toLowerCase()}> element`); + checkInputEvent(beforeInputEvents[0], "insertText", "a", `when inserting "a" with key on <${editableElement.tagName.toLowerCase()}> element`); + is(inputEvents.length, 1, + `Only one "input" event should be fired when inserting "a" with key on <${editableElement.tagName.toLowerCase()}> element`); + checkInputEvent(inputEvents[0], "insertText", "a", `when inserting "a" with key on <${editableElement.tagName.toLowerCase()}> element`); + + beforeInputEvents = []; + inputEvents = []; + synthesizeKey("c"); + is(beforeInputEvents.length, 1, + `Only one "beforeinput" event should be fired when inserting "c" with key on <${editableElement.tagName.toLowerCase()}> element`); + checkInputEvent(beforeInputEvents[0], "insertText", "c", `when inserting "c" with key on <${editableElement.tagName.toLowerCase()}> element`); + is(inputEvents.length, 1, + `Only one "input" event should be fired when inserting "c" with key on <${editableElement.tagName.toLowerCase()}> element`); + checkInputEvent(inputEvents[0], "insertText", "c", `when inserting "c" with key on <${editableElement.tagName.toLowerCase()}> element`); + + beforeInputEvents = []; + inputEvents = []; + synthesizeKey("KEY_ArrowLeft"); + is(beforeInputEvents.length, 0, + `No "beforeinput" event should be fired when pressing "ArrowLeft" key on <${editableElement.tagName.toLowerCase()}> element`); + is(inputEvents.length, 0, + `No "input" event should be fired when pressing "ArrowLeft" key on <${editableElement.tagName.toLowerCase()}> element`); + + beforeInputEvents = []; + inputEvents = []; + synthesizeKey("b"); + is(beforeInputEvents.length, 1, + `Only one "beforeinput" event should be fired when inserting "b" with key on <${editableElement.tagName.toLowerCase()}> element`); + checkInputEvent(beforeInputEvents[0], "insertText", "b", `when inserting "b" with key on <${editableElement.tagName.toLowerCase()}> element`); + is(inputEvents.length, 1, + `Only one "input" event should be fired when inserting "b" with key on <${editableElement.tagName.toLowerCase()}> element`); + checkInputEvent(inputEvents[0], "insertText", "b", `when inserting "b" with key on <${editableElement.tagName.toLowerCase()}> element`); + + let editor = SpecialPowers.wrap(editableElement).editor; + is( + editor.canUndo, + true, + `${editableElement.tagName}: Initially, the editor should have undo transactions` + ); + + beforeInputEvents = []; + inputEvents = []; + editableElement.value = "def"; + is(beforeInputEvents.length, 0, + `No "beforeinput" event should be fired when setting value of <${editableElement.tagName.toLowerCase()}> element`); + is(inputEvents.length, 0, + `No "input" event should be fired when setting value of <${editableElement.tagName.toLowerCase()}> element`); + + is( + editor.canUndo, + false, + `${editableElement.tagName}: After setting value, the editor should not have undo transactions` + ); + + beforeInputEvents = []; + inputEvents = []; + synthesizeKey("a"); + is(beforeInputEvents.length, 1, + `Only one "beforeinput" event should be fired when inserting "a" with key again on <${editableElement.tagName.toLowerCase()}> element`); + checkInputEvent(beforeInputEvents[0], "insertText", "a", `when inserting "a" with key again on <${editableElement.tagName.toLowerCase()}> element`); + is(inputEvents.length, 1, + `Only one "input" event should be fired when inserting "a" with key again on <${editableElement.tagName.toLowerCase()}> element`); + checkInputEvent(inputEvents[0], "insertText", "a", `when inserting "a" with key again on <${editableElement.tagName.toLowerCase()}> element`); + + beforeInputEvents = []; + inputEvents = []; + synthesizeKey("z", { accelKey: true }); + is(editableElement.value, "def", + editableElement.tagName + ": undo should work after setting value"); + is(beforeInputEvents.length, 1, + `Only one "beforeinput" event should be fired when undoing on <${editableElement.tagName.toLowerCase()}> element`); + checkInputEvent(beforeInputEvents[0], "historyUndo", null, `when undoing on <${editableElement.tagName.toLowerCase()}> element`); + is(inputEvents.length, 1, + `Only one "input" event should be fired when undoing on <${editableElement.tagName.toLowerCase()}> element`); + checkInputEvent(inputEvents[0], "historyUndo", null, `when undoing on <${editableElement.tagName.toLowerCase()}> element`); + + // Disable undo/redo. + editor.enableUndo(false); + is( + editor.canUndo, + false, + `${editableElement.tagName}: After calling enableUndo(false), the editor should not have undo transactions` + ); + editableElement.value = "hij"; + is( + editor.canUndo, + false, + `${editableElement.tagName}: After calling enableUndo(false), the editor should not create undo transaction for setting value` + ); + + editableElement.removeEventListener("beforeinput", onBeforeInput); + editableElement.removeEventListener("input", onInput); + } + SimpleTest.finish(); +}); +</script> +</pre> +</body> +</html> diff --git a/editor/libeditor/tests/test_undo_with_editingui.html b/editor/libeditor/tests/test_undo_with_editingui.html new file mode 100644 index 0000000000..fe3565880e --- /dev/null +++ b/editor/libeditor/tests/test_undo_with_editingui.html @@ -0,0 +1,160 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test for undo with editing UI</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none;"> + +</div> + +<div id="editable1" contenteditable="true"> +<table id="table1" border="1"> +<tr id="tr1"><td>ABCDEFG</td><td>HIJKLMN</td></tr> +<tr id="tr2"><td>ABCDEFG</td><td>HIJKLMN</td></tr> +</table> +<div id="edit1">test</div> +<img id="img1" src="green.png"> +<div id="abs1" style="position: absolute; top: 100px; left: 300px; width: 100px; height: 100px; background-color: green;"></div> +</div> +<pre id="test"> + +<script class="testbody" type="application/javascript"> +add_task(async function testAbsPosUI() { + await new Promise((resolve) => { + SimpleTest.waitForFocus(() => { + SimpleTest.executeSoon(resolve); + }, window); + }); + + document.execCommand("enableAbsolutePositionEditing", false, "true"); + ok(document.queryCommandState("enableAbsolutePositionEditing"), + "Enable absolute positioned editor"); + + let edit1 = document.getElementById("edit1"); + edit1.innerText = "test"; + synthesizeMouseAtCenter(edit1, {}); + synthesizeKey("a"); + isnot(edit1.firstChild.textContent, "test", "Text is modified"); + let abs1 = document.getElementById("abs1"); + ok(!abs1.hasAttribute("_moz_abspos"), "_moz_abspos attribute should be false yet"); + + let promiseForAbsEditor = new Promise((resolve) => { + document.addEventListener("selectionchange", () => { + resolve(); + }, {once: true}); + }); + synthesizeMouseAtCenter(abs1, {}); + await promiseForAbsEditor; + ok(abs1.hasAttribute("_moz_abspos"), "_moz_abspos attribute should be true"); + + synthesizeKey("z", { accelKey: true }); + is(edit1.firstChild.textContent, "test", "Text is restored by undo"); + + // TODO: no good way to move absolute position grab. + + document.execCommand("enableAbsolutePositionEditing", false, "false"); +}); + +add_task(function testResizerUI() { + document.execCommand("enableObjectResizing", false, "true"); + ok(document.queryCommandState("enableObjectResizing"), + "Enable object resizing editor"); + + let edit1 = document.getElementById("edit1"); + edit1.innerText = "test"; + synthesizeMouseAtCenter(edit1, {}); + synthesizeKey("h"); + isnot(edit1.firstChild.textContent, "test", "Text is modified"); + + let img1 = document.getElementById("img1"); + synthesizeMouseAtCenter(img1, {}); + ok(img1.hasAttribute("_moz_resizing"), + "_moz_resizing attribute should be true"); + + synthesizeKey("z", { accelKey: true }); + is(edit1.firstChild.textContent, "test", "Text is restored by undo"); + + // Resizer + + synthesizeMouseAtCenter(edit1, {}); + synthesizeKey("j"); + isnot(edit1.firstChild.textContent, "test", "Text is modified"); + + synthesizeMouseAtCenter(img1, {}); + ok(img1.hasAttribute("_moz_resizing"), + "_moz_resizing attribute should be true"); + + // Emulate drag & drop + let origWidth = img1.width; + let posX = img1.clientWidth; + let posY = img1.clientHeight - (img1.height / 2); + synthesizeMouse(img1, posX, posY, {type: "mousedown"}); + synthesizeMouse(img1, posX + 100, posY, {type: "mousemove"}); + synthesizeMouse(img1, posX + 100, posY, {type: "mouseup"}); + + isnot(img1.width, origWidth, "Image is resized"); + synthesizeKey("z", { accelKey: true }); + is(img1.width, origWidth, "Image width is restored by undo"); + + synthesizeKey("z", { accelKey: true }); + is(edit1.firstChild.textContent, "test", "Text is restored by undo"); + + document.execCommand("enableObjectResizing", false, "false"); +}); + +add_task(async function testInlineTableUI() { + document.execCommand("enableInlineTableEditing", false, "true"); + ok(document.queryCommandState("enableInlineTableEditing"), + "Enable Inline Table editor"); + + let tr1 = document.getElementById("tr1"); + synthesizeMouseAtCenter(tr1, {}); + synthesizeKey("o"); + isnot(tr1.firstChild.firstChild.textContent, "ABCDEFG", + "Text is modified"); + + let tr2 = document.getElementById("tr2"); + synthesizeMouseAtCenter(tr2, {}); + synthesizeKey("y"); + isnot(tr2.firstChild.firstChild.textContent, "ABCDEFG", + "Text is modified"); + + synthesizeKey("z", { accelKey: true }); + is(tr2.firstChild.firstChild.textContent, "ABCDEFG", + "Text is restored by undo"); + + synthesizeKey("z", { accelKey: true }); + is(tr1.firstChild.firstChild.textContent, "ABCDEFG", + "Text is restored by undo"); + + synthesizeMouseAtCenter(tr1, {}); + synthesizeKey("p"); + isnot(tr1.firstChild.firstChild.textContent, "ABCDEFG", + "Text is modified"); + + // Inline table editing UI + + synthesizeMouseAtCenter(tr2, {}); + synthesizeMouse(tr2, 0, tr2.clientHeight / 2, {}); + ok(!document.getElementById("tr2"), + "id=tr2 should be removed by a click in the row"); + + synthesizeKey("z", { accelKey: true }); + ok(document.getElementById("tr2"), "id=tr2 should be restored by undo"); + + synthesizeKey("z", { accelKey: true }); + is(tr1.firstChild.firstChild.textContent, "ABCDEFG", + "Text is restored by undo"); + + document.execCommand("enableInlineTableEditing", false, "false"); +}); + +</script> +</pre> +</body> +</html> |