diff --git a/.eslintignore b/.eslintignore index 951a50edeee8..d48a8e380996 100644 --- a/.eslintignore +++ b/.eslintignore @@ -72,7 +72,8 @@ browser/extensions/pdfjs/content/web** browser/extensions/pocket/content/panels/js/tmpl.js browser/extensions/pocket/content/panels/js/vendor/** browser/locales/** -# vendor library files in activity-stream +# generated or library files in activity-stream +browser/extensions/activity-stream/data/content/activity-stream.bundle.js browser/extensions/activity-stream/vendor/** # imported from chromium browser/extensions/mortar/** diff --git a/accessible/.eslintrc.js b/accessible/.eslintrc.js index 3cd32ed6019e..563570d8b953 100644 --- a/accessible/.eslintrc.js +++ b/accessible/.eslintrc.js @@ -13,5 +13,12 @@ module.exports = { "dump": true, "Services": true, "XPCOMUtils": true + }, + "rules": { + // Warn about cyclomatic complexity in functions. + "complexity": ["error", 42], + + // Maximum depth callbacks can be nested. + "max-nested-callbacks": ["error", 10], } }; diff --git a/accessible/base/NotificationController.cpp b/accessible/base/NotificationController.cpp index 8f296a0550df..4fe2072e7970 100644 --- a/accessible/base/NotificationController.cpp +++ b/accessible/base/NotificationController.cpp @@ -676,7 +676,7 @@ NotificationController::WillRefresh(mozilla::TimeStamp aTime) } #endif - mDocument->ContentRemoved(containerElm, textNode); + mDocument->ContentRemoved(textAcc); continue; } diff --git a/accessible/base/TreeWalker.cpp b/accessible/base/TreeWalker.cpp index 8c04b5d6f740..9065826cdee5 100644 --- a/accessible/base/TreeWalker.cpp +++ b/accessible/base/TreeWalker.cpp @@ -52,6 +52,18 @@ TreeWalker:: MOZ_COUNT_CTOR(TreeWalker); } +TreeWalker:: + TreeWalker(DocAccessible* aDocument, nsIContent* aAnchorNode) : + mDoc(aDocument), mContext(nullptr), mAnchorNode(aAnchorNode), + mARIAOwnsIdx(0), + mChildFilter(nsIContent::eSkipPlaceholderContent | nsIContent::eAllChildren), + mFlags(eWalkCache), + mPhase(eAtStart) +{ + MOZ_ASSERT(aAnchorNode, "No anchor node for the accessible tree walker"); + MOZ_COUNT_CTOR(TreeWalker); +} + TreeWalker::~TreeWalker() { MOZ_COUNT_DTOR(TreeWalker); diff --git a/accessible/base/TreeWalker.h b/accessible/base/TreeWalker.h index 377a5e3b8bb3..a8fe8230ae75 100644 --- a/accessible/base/TreeWalker.h +++ b/accessible/base/TreeWalker.h @@ -47,6 +47,11 @@ public: */ TreeWalker(Accessible* aContext, nsIContent* aAnchorNode, uint32_t aFlags = eWalkCache); + /** + * Navigates the accessible children within the anchor node subtree. + */ + TreeWalker(DocAccessible* aDocument, nsIContent* aAnchorNode); + ~TreeWalker(); /** diff --git a/accessible/base/nsAccessibilityService.cpp b/accessible/base/nsAccessibilityService.cpp index fa7d89002097..161b2e9d8a10 100644 --- a/accessible/base/nsAccessibilityService.cpp +++ b/accessible/base/nsAccessibilityService.cpp @@ -524,7 +524,7 @@ nsAccessibilityService::DeckPanelSwitched(nsIPresShell* aPresShell, } #endif - document->ContentRemoved(aDeckNode, panelNode); + document->ContentRemoved(panelNode); } if (aCurrentBoxFrame) { @@ -582,26 +582,7 @@ nsAccessibilityService::ContentRemoved(nsIPresShell* aPresShell, #endif if (document) { - // Flatten hierarchy may be broken at this point so we cannot get a true - // container by traversing up the DOM tree. Find a parent of first accessible - // from the subtree of the given DOM node, that'll be a container. If no - // accessibles in subtree then we don't care about the change. - Accessible* child = document->GetAccessible(aChildNode); - if (!child) { - Accessible* container = document->GetContainerAccessible(aChildNode); - a11y::TreeWalker walker(container ? container : document, aChildNode, - a11y::TreeWalker::eWalkCache); - child = walker.Next(); - } - - if (child) { - MOZ_DIAGNOSTIC_ASSERT(child->Parent(), "Unattached accessible from tree"); - document->ContentRemoved(child->Parent(), aChildNode); -#ifdef A11Y_LOG - if (logging::IsEnabled(logging::eTree)) - logging::AccessibleNNode("real container", child->Parent()); -#endif - } + document->ContentRemoved(aChildNode); } #ifdef A11Y_LOG diff --git a/accessible/generic/DocAccessible.cpp b/accessible/generic/DocAccessible.cpp index ea1f6045cae9..159b375e5454 100644 --- a/accessible/generic/DocAccessible.cpp +++ b/accessible/generic/DocAccessible.cpp @@ -1170,10 +1170,7 @@ DocAccessible::ContentRemoved(nsIDocument* aDocument, // This one and content removal notification from layout may result in // double processing of same subtrees. If it pops up in profiling, then // consider reusing a document node cache to reject these notifications early. - Accessible* container = GetAccessibleOrContainer(aContainerNode); - if (container) { - UpdateTreeOnRemoval(container, aChildNode); - } + ContentRemoved(aChildNode); } void @@ -1382,7 +1379,7 @@ DocAccessible::RecreateAccessible(nsIContent* aContent) // should be coalesced with normal show/hide events. nsIContent* parent = aContent->GetFlattenedTreeParent(); - ContentRemoved(parent, aContent); + ContentRemoved(aContent); ContentInserted(parent, aContent, aContent->GetNextSibling()); } @@ -1972,35 +1969,38 @@ DocAccessible::FireEventsOnInsertion(Accessible* aContainer) } void -DocAccessible::UpdateTreeOnRemoval(Accessible* aContainer, nsIContent* aChildNode) +DocAccessible::ContentRemoved(Accessible* aContent) { - // If child node is not accessible then look for its accessible children. - Accessible* child = GetAccessible(aChildNode); + MOZ_DIAGNOSTIC_ASSERT(aContent->Parent(), "Unattached accessible from tree"); + #ifdef A11Y_LOG logging::TreeInfo("process content removal", 0, - "container", aContainer, "child", aChildNode); + "container", aContent->Parent(), "child", aContent, nullptr); #endif - TreeMutation mt(aContainer); - if (child) { - mt.BeforeRemoval(child); - MOZ_ASSERT(aContainer == child->Parent(), "Wrong parent"); - aContainer->RemoveChild(child); - UncacheChildrenInSubtree(child); - mt.Done(); - return; - } - - TreeWalker walker(aContainer, aChildNode, TreeWalker::eWalkCache); - while (Accessible* child = walker.Next()) { - mt.BeforeRemoval(child); - MOZ_ASSERT(aContainer == child->Parent(), "Wrong parent"); - aContainer->RemoveChild(child); - UncacheChildrenInSubtree(child); - } + TreeMutation mt(aContent->Parent()); + mt.BeforeRemoval(aContent); + aContent->Parent()->RemoveChild(aContent); + UncacheChildrenInSubtree(aContent); mt.Done(); } +void +DocAccessible::ContentRemoved(nsIContent* aContentNode) +{ + // If child node is not accessible then look for its accessible children. + Accessible* acc = GetAccessible(aContentNode); + if (acc) { + ContentRemoved(acc); + } + else { + TreeWalker walker(this, aContentNode); + while (Accessible* acc = walker.Next()) { + ContentRemoved(acc); + } + } +} + bool DocAccessible::RelocateARIAOwnedIfNeeded(nsIContent* aElement) { @@ -2051,7 +2051,7 @@ DocAccessible::ValidateARIAOwned() // If DOM node doesn't have a frame anymore then shutdown its accessible. if (child->Parent() && !child->GetFrame()) { - UpdateTreeOnRemoval(child->Parent(), child->GetContent()); + ContentRemoved(child); children->RemoveElementAt(idx); idx--; continue; diff --git a/accessible/generic/DocAccessible.h b/accessible/generic/DocAccessible.h index afa01044f044..92eeed1bc798 100644 --- a/accessible/generic/DocAccessible.h +++ b/accessible/generic/DocAccessible.h @@ -342,18 +342,10 @@ public: nsIContent* aEndChildNode); /** - * Notify the document accessible that content was removed. + * Update the tree on content removal. */ - void ContentRemoved(Accessible* aContainer, nsIContent* aChildNode) - { - // Update the whole tree of this document accessible when the container is - // null (document element is removed). - UpdateTreeOnRemoval((aContainer ? aContainer : this), aChildNode); - } - void ContentRemoved(nsIContent* aContainerNode, nsIContent* aChildNode) - { - ContentRemoved(AccessibleOrTrueContainer(aContainerNode), aChildNode); - } + void ContentRemoved(Accessible* aContent); + void ContentRemoved(nsIContent* aContentNode); /** * Updates accessible tree when rendered text is changed. @@ -512,11 +504,6 @@ protected: */ void ProcessInvalidationList(); - /** - * Update the accessible tree for content removal. - */ - void UpdateTreeOnRemoval(Accessible* aContainer, nsIContent* aChildNode); - /** * Validates all aria-owns connections and updates the tree accordingly. */ diff --git a/accessible/tests/browser/.eslintrc.js b/accessible/tests/browser/.eslintrc.js index 4c909e4ab391..f38b1b517867 100644 --- a/accessible/tests/browser/.eslintrc.js +++ b/accessible/tests/browser/.eslintrc.js @@ -62,7 +62,7 @@ module.exports = { // eslint-disable-line no-undef "comma-dangle": ["error", "never"], "comma-spacing": "error", "comma-style": ["error", "last"], - "complexity": ["error", 35], + "complexity": ["error", 20], "consistent-this": "off", "curly": ["error", "multi-line"], "default-case": "off", @@ -70,6 +70,7 @@ module.exports = { // eslint-disable-line no-undef "dot-notation": "error", "eol-last": "error", "eqeqeq": "off", + "func-call-spacing": "error", "func-names": "off", "func-style": "off", "generator-star": "off", @@ -141,7 +142,6 @@ module.exports = { // eslint-disable-line no-undef "no-shadow": "error", "no-shadow-restricted-names": "error", "no-space-before-semi": "off", - "no-spaced-func": "error", "no-sparse-arrays": "error", "no-sync": "off", "no-ternary": "off", diff --git a/browser/.eslintrc.js b/browser/.eslintrc.js index 679373e3b43a..4df38ac4f9c5 100644 --- a/browser/.eslintrc.js +++ b/browser/.eslintrc.js @@ -17,9 +17,6 @@ module.exports = { // which is a valid use case. "no-empty": "error", - // No spaces between function name and parentheses - "no-spaced-func": "error", - // Maximum depth callbacks can be nested. "max-nested-callbacks": ["error", 8], diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 68271e4ca190..560cfd116116 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1530,7 +1530,6 @@ pref("dom.ipc.reportProcessHangs", false); pref("dom.ipc.reportProcessHangs", true); #endif -pref("browser.reader.detectedFirstArticle", false); // Don't limit how many nodes we care about on desktop: pref("reader.parse-node-limit", 0); diff --git a/browser/base/content/aboutDialog.xul b/browser/base/content/aboutDialog.xul index 661f2c6419ac..c066710641db 100644 --- a/browser/base/content/aboutDialog.xul +++ b/browser/base/content/aboutDialog.xul @@ -71,7 +71,7 @@ + + """)) + button = self.marionette.find_element(By.TAG_NAME, "button") + self.assertEqual("none", button.value_of_css_property("pointer-events")) + + with self.assertRaisesRegexp(errors.ElementClickInterceptedException, + "does not have pointer events enabled"): + button.click() + self.assertFalse(self.marionette.execute_script("return window.clicked", sandbox=None)) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_execute_script.py b/testing/marionette/harness/marionette_harness/tests/unit/test_execute_script.py index 96f40374ba8d..66054376d622 100644 --- a/testing/marionette/harness/marionette_harness/tests/unit/test_execute_script.py +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_execute_script.py @@ -92,40 +92,62 @@ class TestExecuteContent(MarionetteTestCase): self.assertIsNone(self.marionette.execute_script("true")) def test_argument_null(self): - self.assertIsNone(self.marionette.execute_script("return arguments[0]", [None])) + self.assertIsNone(self.marionette.execute_script( + "return arguments[0]", + script_args=(None,), + sandbox="default")) + self.assertIsNone(self.marionette.execute_script( + "return arguments[0]", + script_args=(None,), + sandbox="system")) + self.assertIsNone(self.marionette.execute_script( + "return arguments[0]", + script_args=(None,), + sandbox=None)) def test_argument_number(self): self.assertEqual( - 1, self.marionette.execute_script("return arguments[0]", [1])) + 1, self.marionette.execute_script("return arguments[0]", (1,))) self.assertEqual( - 1.5, self.marionette.execute_script("return arguments[0]", [1.5])) + 1.5, self.marionette.execute_script("return arguments[0]", (1.5,))) def test_argument_boolean(self): - self.assertTrue(self.marionette.execute_script("return arguments[0]", [True])) + self.assertTrue(self.marionette.execute_script("return arguments[0]", (True,))) def test_argument_string(self): self.assertEqual( - "foo", self.marionette.execute_script("return arguments[0]", ["foo"])) + "foo", self.marionette.execute_script("return arguments[0]", ("foo",))) def test_argument_array(self): self.assertEqual( - [1, 2], self.marionette.execute_script("return arguments[0]", [[1, 2]])) + [1, 2], self.marionette.execute_script("return arguments[0]", ([1, 2],))) def test_argument_object(self): self.assertEqual({"foo": 1}, self.marionette.execute_script( - "return arguments[0]", [{"foo": 1}])) + "return arguments[0]", ({"foo": 1},))) - def test_globals(self): + def test_default_sandbox_globals(self): for property in globals: - self.assert_is_defined(property) + self.assert_is_defined(property, sandbox="default") + self.assert_is_defined("Components") self.assert_is_defined("window.wrappedJSObject") def test_system_globals(self): for property in globals: self.assert_is_defined(property, sandbox="system") + self.assert_is_defined("Components", sandbox="system") - self.assert_is_defined("window.wrappedJSObject") + self.assert_is_defined("window.wrappedJSObject", sandbox="system") + + def test_mutable_sandbox_globals(self): + for property in globals: + self.assert_is_defined(property, sandbox=None) + + # Components is there, but will be removed soon + self.assert_is_defined("Components", sandbox=None) + # wrappedJSObject is always there in sandboxes + self.assert_is_defined("window.wrappedJSObject", sandbox=None) def test_exception(self): self.assertRaises(errors.JavascriptException, @@ -142,10 +164,10 @@ class TestExecuteContent(MarionetteTestCase): self.assertIn("return b", cm.exception.stacktrace) def test_permission(self): - with self.assertRaises(errors.JavascriptException): - self.marionette.execute_script(""" - var c = Components.classes["@mozilla.org/preferences-service;1"]; - """) + for sandbox in ["default", None]: + with self.assertRaises(errors.JavascriptException): + self.marionette.execute_script( + "Components.classes['@mozilla.org/preferences-service;1']") def test_return_web_element(self): self.marionette.navigate(elements) @@ -188,21 +210,34 @@ class TestExecuteContent(MarionetteTestCase): self.assertEqual(self.marionette.execute_script( "return this.foobar", new_sandbox=False), [23, 42]) - def test_wrappedjsobject(self): + def test_mutable_sandbox_wrappedjsobject(self): + self.assert_is_defined("window.wrappedJSObject") + with self.assertRaises(errors.JavascriptException): + self.marionette.execute_script("window.wrappedJSObject.foo = 1", sandbox=None) + + def test_default_sandbox_wrappedjsobject(self): + self.assert_is_defined("window.wrappedJSObject", sandbox="default") + try: - self.marionette.execute_script("window.wrappedJSObject.foo = 3") - self.assertEqual( - self.marionette.execute_script("return window.wrappedJSObject.foo"), 3) + self.marionette.execute_script( + "window.wrappedJSObject.foo = 4", sandbox="default") + self.assertEqual(self.marionette.execute_script( + "return window.wrappedJSObject.foo", sandbox="default"), 4) finally: - self.marionette.execute_script("delete window.wrappedJSObject.foo") + self.marionette.execute_script( + "delete window.wrappedJSObject.foo", sandbox="default") def test_system_sandbox_wrappedjsobject(self): + self.assert_is_defined("window.wrappedJSObject", sandbox="system") + self.marionette.execute_script( "window.wrappedJSObject.foo = 4", sandbox="system") self.assertEqual(self.marionette.execute_script( "return window.wrappedJSObject.foo", sandbox="system"), 4) def test_system_dead_object(self): + self.assert_is_defined("window.wrappedJSObject", sandbox="system") + self.marionette.execute_script( "window.wrappedJSObject.foo = function() { return 'yo' }", sandbox="system") @@ -263,18 +298,40 @@ class TestExecuteContent(MarionetteTestCase): content_timeout_triggered, message="Scheduled setTimeout event was cancelled by call to execute_script") - def test_privileged_code_inspection(self): - # test permission denied on toString of unload event handler + def test_access_chrome_objects_in_event_listeners(self): + # sandbox.window.addEventListener/removeEventListener + # is used by Marionette for installing the unloadHandler which + # is used to return an error when a document is unloaded during + # script execution. + # + # Certain web frameworks, notably Angular, override + # window.addEventListener/removeEventListener and introspects + # objects passed to them. If these objects originates from chrome + # without having been cloned, a permission denied error is thrown + # as part of the security precautions put in place by the sandbox. + + # addEventListener is called when script is injected self.marionette.navigate(inline(""" """)) + window.addEventListener = (event, listener) => listener.toString(); + + """)) self.marionette.execute_script("", sandbox=None) + # removeEventListener is called when sandbox is unloaded + self.marionette.navigate(inline(""" + + """)) + self.marionette.execute_script("", sandbox=None) + + def test_access_global_objects_from_chrome(self): # test inspection of arguments self.marionette.execute_script("__webDriverArguments.toString()") + class TestExecuteChrome(WindowManagerMixin, TestExecuteContent): def setUp(self): @@ -286,8 +343,8 @@ class TestExecuteChrome(WindowManagerMixin, TestExecuteContent): super(TestExecuteChrome, self).tearDown() def test_permission(self): - self.assertEqual(1, self.marionette.execute_script(""" - var c = Components.classes["@mozilla.org/preferences-service;1"]; return 1;""")) + self.marionette.execute_script( + "Components.classes['@mozilla.org/preferences-service;1']") @skip_if_mobile("New windows not supported in Fennec") def test_unmarshal_element_collection(self): @@ -330,7 +387,16 @@ class TestExecuteChrome(WindowManagerMixin, TestExecuteContent): def test_window_set_timeout_is_not_cancelled(self): pass - def test_privileged_code_inspection(self): + def test_mutable_sandbox_wrappedjsobject(self): + pass + + def test_default_sandbox_wrappedjsobject(self): + pass + + def test_system_sandbox_wrappedjsobject(self): + pass + + def test_access_chrome_objects_in_event_listeners(self): pass diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_modal_dialogs.py b/testing/marionette/harness/marionette_harness/tests/unit/test_modal_dialogs.py index abfd248a3214..52020e00f016 100644 --- a/testing/marionette/harness/marionette_harness/tests/unit/test_modal_dialogs.py +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_modal_dialogs.py @@ -3,8 +3,8 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. from marionette_driver.by import By -from marionette_driver.errors import NoAlertPresentException, ElementNotInteractableException from marionette_driver.expected import element_present +from marionette_driver import errors from marionette_driver.marionette import Alert from marionette_driver.wait import Wait @@ -17,7 +17,7 @@ class BaseAlertTestCase(WindowManagerMixin, MarionetteTestCase): try: Alert(self.marionette).text return True - except NoAlertPresentException: + except errors.NoAlertPresentException: return False def wait_for_alert(self, timeout=None): @@ -53,8 +53,10 @@ class TestTabModalAlerts(BaseAlertTestCase): super(TestTabModalAlerts, self).tearDown() def test_no_alert_raises(self): - self.assertRaises(NoAlertPresentException, Alert(self.marionette).accept) - self.assertRaises(NoAlertPresentException, Alert(self.marionette).dismiss) + with self.assertRaises(errors.NoAlertPresentException): + Alert(self.marionette).accept() + with self.assertRaises(errors.NoAlertPresentException): + Alert(self.marionette).dismiss() def test_alert_accept(self): self.marionette.find_element(By.ID, "tab-modal-alert").click() @@ -112,7 +114,7 @@ class TestTabModalAlerts(BaseAlertTestCase): alert.dismiss() def test_alert_text(self): - with self.assertRaises(NoAlertPresentException): + with self.assertRaises(errors.NoAlertPresentException): alert = self.marionette.switch_to_alert() alert.text self.marionette.find_element(By.ID, "tab-modal-alert").click() @@ -122,7 +124,7 @@ class TestTabModalAlerts(BaseAlertTestCase): alert.accept() def test_prompt_text(self): - with self.assertRaises(NoAlertPresentException): + with self.assertRaises(errors.NoAlertPresentException): alert = self.marionette.switch_to_alert() alert.text self.marionette.find_element(By.ID, "tab-modal-prompt").click() @@ -132,7 +134,7 @@ class TestTabModalAlerts(BaseAlertTestCase): alert.accept() def test_confirm_text(self): - with self.assertRaises(NoAlertPresentException): + with self.assertRaises(errors.NoAlertPresentException): alert = self.marionette.switch_to_alert() alert.text self.marionette.find_element(By.ID, "tab-modal-confirm").click() @@ -142,11 +144,13 @@ class TestTabModalAlerts(BaseAlertTestCase): alert.accept() def test_set_text_throws(self): - self.assertRaises(NoAlertPresentException, Alert(self.marionette).send_keys, "Foo") + with self.assertRaises(errors.NoAlertPresentException): + Alert(self.marionette).send_keys("Foo") self.marionette.find_element(By.ID, "tab-modal-alert").click() self.wait_for_alert() alert = self.marionette.switch_to_alert() - self.assertRaises(ElementNotInteractableException, alert.send_keys, "Foo") + with self.assertRaises(errors.ElementNotInteractableException): + alert.send_keys("Foo") alert.accept() def test_set_text_accept(self): @@ -194,31 +198,11 @@ class TestTabModalAlerts(BaseAlertTestCase): alert.accept() self.wait_for_condition(lambda mn: mn.get_url() == "about:blank") - @skip_if_e10s("Bug 1325044") def test_unrelated_command_when_alert_present(self): - click_handler = self.marionette.find_element(By.ID, "click-handler") - text = self.marionette.find_element(By.ID, "click-result").text - self.assertEqual(text, "") - self.marionette.find_element(By.ID, "tab-modal-alert").click() self.wait_for_alert() - - # Commands succeed, but because the dialog blocks the event loop, - # our actions aren't reflected on the page. - text = self.marionette.find_element(By.ID, "click-result").text - self.assertEqual(text, "") - click_handler.click() - text = self.marionette.find_element(By.ID, "click-result").text - self.assertEqual(text, "") - - alert = self.marionette.switch_to_alert() - alert.accept() - - self.wait_for_alert_closed() - - click_handler.click() - text = self.marionette.find_element(By.ID, "click-result").text - self.assertEqual(text, "result") + with self.assertRaises(errors.UnexpectedAlertOpen): + self.marionette.find_element(By.ID, "click-result") class TestModalAlerts(BaseAlertTestCase): diff --git a/testing/marionette/interaction.js b/testing/marionette/interaction.js index b86391374ebc..dbad95631dc8 100644 --- a/testing/marionette/interaction.js +++ b/testing/marionette/interaction.js @@ -116,7 +116,9 @@ this.interaction = {}; */ interaction.clickElement = function* (el, strict = false, specCompat = false) { const a11y = accessibility.get(strict); - if (specCompat) { + if (element.isXULElement(el)) { + yield chromeClick(el, a11y); + } else if (specCompat) { yield webdriverClickElement(el, a11y); } else { yield seleniumClickElement(el, a11y); @@ -166,22 +168,10 @@ function* webdriverClickElement (el, a11y) { }); // step 8 - - // chrome elements - if (element.isXULElement(el)) { - if (el.localName == "option") { - interaction.selectOption(el); - } else { - el.click(); - } - - // content elements + if (el.localName == "option") { + interaction.selectOption(el); } else { - if (el.localName == "option") { - interaction.selectOption(el); - } else { - event.synthesizeMouseAtPoint(clickPoint.x, clickPoint.y, {}, win); - } + event.synthesizeMouseAtPoint(clickPoint.x, clickPoint.y, {}, win); } // step 9 @@ -192,6 +182,24 @@ function* webdriverClickElement (el, a11y) { // run post-navigation checks } +function* chromeClick (el, a11y) { + if (!atom.isElementEnabled(el)) { + throw new InvalidElementStateError("Element is not enabled"); + } + + yield a11y.getAccessible(el, true).then(acc => { + a11y.assertVisible(acc, el, true); + a11y.assertEnabled(acc, el, true); + a11y.assertActionable(acc, el); + }); + + if (el.localName == "option") { + interaction.selectOption(el); + } else { + el.click(); + } +} + function* seleniumClickElement (el, a11y) { let win = getWindow(el); @@ -214,24 +222,13 @@ function* seleniumClickElement (el, a11y) { a11y.assertActionable(acc, el); }); - // chrome elements - if (element.isXULElement(el)) { - if (el.localName == "option") { - interaction.selectOption(el); - } else { - el.click(); - } - - // content elements + if (el.localName == "option") { + interaction.selectOption(el); } else { - if (el.localName == "option") { - interaction.selectOption(el); - } else { - let rects = el.getClientRects(); - let centre = element.getInViewCentrePoint(rects[0], win); - let opts = {}; - event.synthesizeMouseAtPoint(centre.x, centre.y, opts, win); - } + let rects = el.getClientRects(); + let centre = element.getInViewCentrePoint(rects[0], win); + let opts = {}; + event.synthesizeMouseAtPoint(centre.x, centre.y, opts, win); } }; @@ -291,13 +288,20 @@ interaction.selectOption = function (el) { * * @return {Promise} * Promise is accepted once event queue is flushed, or rejected if - * |win| is unloaded before the queue can be flushed. + * |win| has closed or been unloaded before the queue can be flushed. */ interaction.flushEventLoop = function* (win) { let unloadEv; + return new Promise((resolve, reject) => { + if (win.closed) { + reject(); + return; + } + unloadEv = reject; win.addEventListener("unload", unloadEv, {once: true}); + win.requestAnimationFrame(resolve); }).then(() => { win.removeEventListener("unload", unloadEv); diff --git a/testing/marionette/jar.mn b/testing/marionette/jar.mn index dddfde3c54dd..998ada6fcb65 100644 --- a/testing/marionette/jar.mn +++ b/testing/marionette/jar.mn @@ -32,6 +32,9 @@ marionette.jar: content/assert.js (assert.js) content/addon.js (addon.js) content/session.js (session.js) + content/transport.js (transport.js) + content/packets.js (packets.js) + content/stream-utils.js (stream-utils.js) #ifdef ENABLE_TESTS content/test.xul (chrome/test.xul) content/test2.xul (chrome/test2.xul) diff --git a/testing/marionette/packets.js b/testing/marionette/packets.js new file mode 100644 index 000000000000..57364fc0a569 --- /dev/null +++ b/testing/marionette/packets.js @@ -0,0 +1,397 @@ +/* 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/. */ + +"use strict"; + +/** + * Packets contain read / write functionality for the different packet types + * supported by the debugging protocol, so that a transport can focus on + * delivery and queue management without worrying too much about the specific + * packet types. + * + * They are intended to be "one use only", so a new packet should be + * instantiated for each incoming or outgoing packet. + * + * A complete Packet type should expose at least the following: + * * read(stream, scriptableStream) + * Called when the input stream has data to read + * * write(stream) + * Called when the output stream is ready to write + * * get done() + * Returns true once the packet is done being read / written + * * destroy() + * Called to clean up at the end of use + */ + +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; +const { StreamUtils } = Cu.import("chrome://marionette/content/stream-utils.js"); + +const unicodeConverter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] + .createInstance(Ci.nsIScriptableUnicodeConverter); +unicodeConverter.charset = "UTF-8"; + +const defer = function () { + let deferred = { + promise: new Promise((resolve, reject) => { + deferred.resolve = resolve; + deferred.reject = reject; + }) + }; + return deferred; +}; + +this.EXPORTED_SYMBOLS = ["RawPacket", "Packet", "JSONPacket", "BulkPacket"]; + +// The transport's previous check ensured the header length did not exceed 20 +// characters. Here, we opt for the somewhat smaller, but still large limit of +// 1 TiB. +const PACKET_LENGTH_MAX = Math.pow(2, 40); + +/** + * A generic Packet processing object (extended by two subtypes below). + */ +function Packet(transport) { + this._transport = transport; + this._length = 0; +} + +/** + * Attempt to initialize a new Packet based on the incoming packet header we've + * received so far. We try each of the types in succession, trying JSON packets + * first since they are much more common. + * @param header string + * The packet header string to attempt parsing. + * @param transport DebuggerTransport + * The transport instance that will own the packet. + * @return Packet + * The parsed packet of the matching type, or null if no types matched. + */ +Packet.fromHeader = function (header, transport) { + return JSONPacket.fromHeader(header, transport) || + BulkPacket.fromHeader(header, transport); +}; + +Packet.prototype = { + + get length() { + return this._length; + }, + + set length(length) { + if (length > PACKET_LENGTH_MAX) { + throw Error("Packet length " + length + " exceeds the max length of " + + PACKET_LENGTH_MAX); + } + this._length = length; + }, + + destroy: function () { + this._transport = null; + } + +}; + +/** + * With a JSON packet (the typical packet type sent via the transport), data is + * transferred as a JSON packet serialized into a string, with the string length + * prepended to the packet, followed by a colon ([length]:[packet]). The + * contents of the JSON packet are specified in the Remote Debugging Protocol + * specification. + * @param transport DebuggerTransport + * The transport instance that will own the packet. + */ +function JSONPacket(transport) { + Packet.call(this, transport); + this._data = ""; + this._done = false; +} + +/** + * Attempt to initialize a new JSONPacket based on the incoming packet header + * we've received so far. + * @param header string + * The packet header string to attempt parsing. + * @param transport DebuggerTransport + * The transport instance that will own the packet. + * @return JSONPacket + * The parsed packet, or null if it's not a match. + */ +JSONPacket.fromHeader = function (header, transport) { + let match = this.HEADER_PATTERN.exec(header); + + if (!match) { + return null; + } + + let packet = new JSONPacket(transport); + packet.length = +match[1]; + return packet; +}; + +JSONPacket.HEADER_PATTERN = /^(\d+):$/; + +JSONPacket.prototype = Object.create(Packet.prototype); + +Object.defineProperty(JSONPacket.prototype, "object", { + /** + * Gets the object (not the serialized string) being read or written. + */ + get: function () { + return this._object; + }, + + /** + * Sets the object to be sent when write() is called. + */ + set: function (object) { + this._object = object; + let data = JSON.stringify(object); + this._data = unicodeConverter.ConvertFromUnicode(data); + this.length = this._data.length; + } +}); + +JSONPacket.prototype.read = function (stream, scriptableStream) { + + // Read in more packet data. + this._readData(stream, scriptableStream); + + if (!this.done) { + // Don't have a complete packet yet. + return; + } + + let json = this._data; + try { + json = unicodeConverter.ConvertToUnicode(json); + this._object = JSON.parse(json); + } catch (e) { + let msg = "Error parsing incoming packet: " + json + " (" + e + + " - " + e.stack + ")"; + console.error(msg); + dump(msg + "\n"); + return; + } + + this._transport._onJSONObjectReady(this._object); +}; + +JSONPacket.prototype._readData = function (stream, scriptableStream) { + let bytesToRead = Math.min(this.length - this._data.length, + stream.available()); + this._data += scriptableStream.readBytes(bytesToRead); + this._done = this._data.length === this.length; +}; + +JSONPacket.prototype.write = function (stream) { + + if (this._outgoing === undefined) { + // Format the serialized packet to a buffer + this._outgoing = this.length + ":" + this._data; + } + + let written = stream.write(this._outgoing, this._outgoing.length); + this._outgoing = this._outgoing.slice(written); + this._done = !this._outgoing.length; +}; + +Object.defineProperty(JSONPacket.prototype, "done", { + get: function () { + return this._done; + } +}); + +JSONPacket.prototype.toString = function () { + return JSON.stringify(this._object, null, 2); +}; + +/** + * With a bulk packet, data is transferred by temporarily handing over the + * transport's input or output stream to the application layer for writing data + * directly. This can be much faster for large data sets, and avoids various + * stages of copies and data duplication inherent in the JSON packet type. The + * bulk packet looks like: + * + * bulk [actor] [type] [length]:[data] + * + * The interpretation of the data portion depends on the kind of actor and the + * packet's type. See the Remote Debugging Protocol Stream Transport spec for + * more details. + * @param transport DebuggerTransport + * The transport instance that will own the packet. + */ +function BulkPacket(transport) { + Packet.call(this, transport); + this._done = false; + this._readyForWriting = defer(); +} + +/** + * Attempt to initialize a new BulkPacket based on the incoming packet header + * we've received so far. + * @param header string + * The packet header string to attempt parsing. + * @param transport DebuggerTransport + * The transport instance that will own the packet. + * @return BulkPacket + * The parsed packet, or null if it's not a match. + */ +BulkPacket.fromHeader = function (header, transport) { + let match = this.HEADER_PATTERN.exec(header); + + if (!match) { + return null; + } + + let packet = new BulkPacket(transport); + packet.header = { + actor: match[1], + type: match[2], + length: +match[3] + }; + return packet; +}; + +BulkPacket.HEADER_PATTERN = /^bulk ([^: ]+) ([^: ]+) (\d+):$/; + +BulkPacket.prototype = Object.create(Packet.prototype); + +BulkPacket.prototype.read = function (stream) { + // Temporarily pause monitoring of the input stream + this._transport.pauseIncoming(); + + let deferred = defer(); + + this._transport._onBulkReadReady({ + actor: this.actor, + type: this.type, + length: this.length, + copyTo: (output) => { + let copying = StreamUtils.copyStream(stream, output, this.length); + deferred.resolve(copying); + return copying; + }, + stream: stream, + done: deferred + }); + + // Await the result of reading from the stream + deferred.promise.then(() => { + this._done = true; + this._transport.resumeIncoming(); + }, this._transport.close); + + // Ensure this is only done once + this.read = () => { + throw new Error("Tried to read() a BulkPacket's stream multiple times."); + }; +}; + +BulkPacket.prototype.write = function (stream) { + if (this._outgoingHeader === undefined) { + // Format the serialized packet header to a buffer + this._outgoingHeader = "bulk " + this.actor + " " + this.type + " " + + this.length + ":"; + } + + // Write the header, or whatever's left of it to write. + if (this._outgoingHeader.length) { + let written = stream.write(this._outgoingHeader, + this._outgoingHeader.length); + this._outgoingHeader = this._outgoingHeader.slice(written); + return; + } + + // Temporarily pause the monitoring of the output stream + this._transport.pauseOutgoing(); + + let deferred = defer(); + + this._readyForWriting.resolve({ + copyFrom: (input) => { + let copying = StreamUtils.copyStream(input, stream, this.length); + deferred.resolve(copying); + return copying; + }, + stream: stream, + done: deferred + }); + + // Await the result of writing to the stream + deferred.promise.then(() => { + this._done = true; + this._transport.resumeOutgoing(); + }, this._transport.close); + + // Ensure this is only done once + this.write = () => { + throw new Error("Tried to write() a BulkPacket's stream multiple times."); + }; +}; + +Object.defineProperty(BulkPacket.prototype, "streamReadyForWriting", { + get: function () { + return this._readyForWriting.promise; + } +}); + +Object.defineProperty(BulkPacket.prototype, "header", { + get: function () { + return { + actor: this.actor, + type: this.type, + length: this.length + }; + }, + + set: function (header) { + this.actor = header.actor; + this.type = header.type; + this.length = header.length; + }, +}); + +Object.defineProperty(BulkPacket.prototype, "done", { + get: function () { + return this._done; + }, +}); + +BulkPacket.prototype.toString = function () { + return "Bulk: " + JSON.stringify(this.header, null, 2); +}; + +/** + * RawPacket is used to test the transport's error handling of malformed + * packets, by writing data directly onto the stream. + * @param transport DebuggerTransport + * The transport instance that will own the packet. + * @param data string + * The raw string to send out onto the stream. + */ +function RawPacket(transport, data) { + Packet.call(this, transport); + this._data = data; + this.length = data.length; + this._done = false; +} + +RawPacket.prototype = Object.create(Packet.prototype); + +RawPacket.prototype.read = function (stream) { + // This hasn't yet been needed for testing. + throw Error("Not implmented."); +}; + +RawPacket.prototype.write = function (stream) { + let written = stream.write(this._data, this._data.length); + this._data = this._data.slice(written); + this._done = !this._data.length; +}; + +Object.defineProperty(RawPacket.prototype, "done", { + get: function () { + return this._done; + } +}); diff --git a/testing/marionette/server.js b/testing/marionette/server.js index c5e2fda6fef0..8f8f7b381d3a 100644 --- a/testing/marionette/server.js +++ b/testing/marionette/server.js @@ -19,8 +19,7 @@ Cu.import("chrome://marionette/content/driver.js"); Cu.import("chrome://marionette/content/error.js"); Cu.import("chrome://marionette/content/message.js"); -// Bug 1083711: Load transport.js as an SDK module instead of subscript -loader.loadSubScript("resource://devtools/shared/transport/transport.js"); +Cu.import("chrome://marionette/content/transport.js"); const logger = Log.repository.getLogger("Marionette"); @@ -95,9 +94,6 @@ const RECOMMENDED_PREFS = new Map([ // thumbnails in general cannot hurt ["browser.pagethumbnails.capturing_disabled", true], - // Avoid performing Reader Mode intros during tests - ["browser.reader.detectedFirstArticle", true], - // Disable safebrowsing components. // // These should also be set in the profile prior to starting Firefox, diff --git a/testing/marionette/stream-utils.js b/testing/marionette/stream-utils.js new file mode 100644 index 000000000000..c75dbbcac706 --- /dev/null +++ b/testing/marionette/stream-utils.js @@ -0,0 +1,242 @@ +/* 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/. */ + +"use strict"; + +const {Constructor: CC, classes: Cc, interfaces: Ci, utils: Cu} = Components; + +Cu.import("resource://devtools/shared/event-emitter.js"); +Cu.import("resource://gre/modules/Services.jsm"); + +const IOUtil = Cc["@mozilla.org/io-util;1"].getService(Ci.nsIIOUtil); +const ScriptableInputStream = + CC("@mozilla.org/scriptableinputstream;1", + "nsIScriptableInputStream", "init"); + +this.EXPORTED_SYMBOLS = ["StreamUtils"]; + +const BUFFER_SIZE = 0x8000; + +/** + * This helper function (and its companion object) are used by bulk senders and + * receivers to read and write data in and out of other streams. Functions that + * make use of this tool are passed to callers when it is time to read or write + * bulk data. It is highly recommended to use these copier functions instead of + * the stream directly because the copier enforces the agreed upon length. + * Since bulk mode reuses an existing stream, the sender and receiver must write + * and read exactly the agreed upon amount of data, or else the entire transport + * will be left in a invalid state. Additionally, other methods of stream + * copying (such as NetUtil.asyncCopy) close the streams involved, which would + * terminate the debugging transport, and so it is avoided here. + * + * Overall, this *works*, but clearly the optimal solution would be able to just + * use the streams directly. If it were possible to fully implement + * nsIInputStream / nsIOutputStream in JS, wrapper streams could be created to + * enforce the length and avoid closing, and consumers could use familiar stream + * utilities like NetUtil.asyncCopy. + * + * The function takes two async streams and copies a precise number of bytes + * from one to the other. Copying begins immediately, but may complete at some + * future time depending on data size. Use the returned promise to know when + * it's complete. + * + * @param input nsIAsyncInputStream + * The stream to copy from. + * @param output nsIAsyncOutputStream + * The stream to copy to. + * @param length Integer + * The amount of data that needs to be copied. + * @return Promise + * The promise is resolved when copying completes or rejected if any + * (unexpected) errors occur. + */ +function copyStream(input, output, length) { + let copier = new StreamCopier(input, output, length); + return copier.copy(); +} + +function StreamCopier(input, output, length) { + EventEmitter.decorate(this); + this._id = StreamCopier._nextId++; + this.input = input; + // Save off the base output stream, since we know it's async as we've required + this.baseAsyncOutput = output; + if (IOUtil.outputStreamIsBuffered(output)) { + this.output = output; + } else { + this.output = Cc["@mozilla.org/network/buffered-output-stream;1"] + .createInstance(Ci.nsIBufferedOutputStream); + this.output.init(output, BUFFER_SIZE); + } + this._length = length; + this._amountLeft = length; + this._deferred = { + promise: new Promise((resolve, reject) => { + this._deferred.resolve = resolve; + this._deferred.reject = reject; + }) + }; + + this._copy = this._copy.bind(this); + this._flush = this._flush.bind(this); + this._destroy = this._destroy.bind(this); + + // Copy promise's then method up to this object. + // Allows the copier to offer a promise interface for the simple succeed or + // fail scenarios, but also emit events (due to the EventEmitter) for other + // states, like progress. + this.then = this._deferred.promise.then.bind(this._deferred.promise); + this.then(this._destroy, this._destroy); + + // Stream ready callback starts as |_copy|, but may switch to |_flush| at end + // if flushing would block the output stream. + this._streamReadyCallback = this._copy; +} +StreamCopier._nextId = 0; + +StreamCopier.prototype = { + + copy: function () { + // Dispatch to the next tick so that it's possible to attach a progress + // event listener, even for extremely fast copies (like when testing). + Services.tm.currentThread.dispatch(() => { + try { + this._copy(); + } catch (e) { + this._deferred.reject(e); + } + }, 0); + return this; + }, + + _copy: function () { + let bytesAvailable = this.input.available(); + let amountToCopy = Math.min(bytesAvailable, this._amountLeft); + this._debug("Trying to copy: " + amountToCopy); + + let bytesCopied; + try { + bytesCopied = this.output.writeFrom(this.input, amountToCopy); + } catch (e) { + if (e.result == Cr.NS_BASE_STREAM_WOULD_BLOCK) { + this._debug("Base stream would block, will retry"); + this._debug("Waiting for output stream"); + this.baseAsyncOutput.asyncWait(this, 0, 0, Services.tm.currentThread); + return; + } + throw e; + } + + this._amountLeft -= bytesCopied; + this._debug("Copied: " + bytesCopied + + ", Left: " + this._amountLeft); + this._emitProgress(); + + if (this._amountLeft === 0) { + this._debug("Copy done!"); + this._flush(); + return; + } + + this._debug("Waiting for input stream"); + this.input.asyncWait(this, 0, 0, Services.tm.currentThread); + }, + + _emitProgress: function () { + this.emit("progress", { + bytesSent: this._length - this._amountLeft, + totalBytes: this._length + }); + }, + + _flush: function () { + try { + this.output.flush(); + } catch (e) { + if (e.result == Cr.NS_BASE_STREAM_WOULD_BLOCK || + e.result == Cr.NS_ERROR_FAILURE) { + this._debug("Flush would block, will retry"); + this._streamReadyCallback = this._flush; + this._debug("Waiting for output stream"); + this.baseAsyncOutput.asyncWait(this, 0, 0, Services.tm.currentThread); + return; + } + throw e; + } + this._deferred.resolve(); + }, + + _destroy: function () { + this._destroy = null; + this._copy = null; + this._flush = null; + this.input = null; + this.output = null; + }, + + // nsIInputStreamCallback + onInputStreamReady: function () { + this._streamReadyCallback(); + }, + + // nsIOutputStreamCallback + onOutputStreamReady: function () { + this._streamReadyCallback(); + }, + + _debug: function (msg) { + } + +}; + +/** + * Read from a stream, one byte at a time, up to the next |delimiter| + * character, but stopping if we've read |count| without finding it. Reading + * also terminates early if there are less than |count| bytes available on the + * stream. In that case, we only read as many bytes as the stream currently has + * to offer. + * TODO: This implementation could be removed if bug 984651 is fixed, which + * provides a native version of the same idea. + * @param stream nsIInputStream + * The input stream to read from. + * @param delimiter string + * The character we're trying to find. + * @param count integer + * The max number of characters to read while searching. + * @return string + * The data collected. If the delimiter was found, this string will + * end with it. + */ +function delimitedRead(stream, delimiter, count) { + let scriptableStream; + if (stream instanceof Ci.nsIScriptableInputStream) { + scriptableStream = stream; + } else { + scriptableStream = new ScriptableInputStream(stream); + } + + let data = ""; + + // Don't exceed what's available on the stream + count = Math.min(count, stream.available()); + + if (count <= 0) { + return data; + } + + let char; + while (char !== delimiter && count > 0) { + char = scriptableStream.readBytes(1); + count--; + data += char; + } + + return data; +} + +const StreamUtils = { + copyStream, + delimitedRead +}; + diff --git a/testing/marionette/test_assert.js b/testing/marionette/test_assert.js index 44dc1b8b8b26..767abb1791f5 100644 --- a/testing/marionette/test_assert.js +++ b/testing/marionette/test_assert.js @@ -33,6 +33,14 @@ add_test(function test_platforms() { run_next_test(); }); +add_test(function test_noUserPrompt() { + assert.noUserPrompt(null); + assert.noUserPrompt(undefined); + Assert.throws(() => assert.noUserPrompt({}), UnexpectedAlertOpenError); + + run_next_test(); +}); + add_test(function test_defined() { assert.defined({}); Assert.throws(() => assert.defined(undefined), InvalidArgumentError); diff --git a/testing/marionette/test_error.js b/testing/marionette/test_error.js index f27212637373..a905f02f0604 100644 --- a/testing/marionette/test_error.js +++ b/testing/marionette/test_error.js @@ -209,15 +209,25 @@ add_test(function test_ElementClickInterceptedError() { return otherEl; }, }, + style: { + pointerEvents: "auto", + } }; - let err = new ElementClickInterceptedError(obscuredEl, {x: 1, y: 2}); - equal("ElementClickInterceptedError", err.name); + let err1 = new ElementClickInterceptedError(obscuredEl, {x: 1, y: 2}); + equal("ElementClickInterceptedError", err1.name); equal("Element is not clickable at point (1,2) " + "because another element obscures it", - err.message); - equal("element click intercepted", err.status); - ok(err instanceof WebDriverError); + err1.message); + equal("element click intercepted", err1.status); + ok(err1 instanceof WebDriverError); + + obscuredEl.style.pointerEvents = "none"; + let err2 = new ElementClickInterceptedError(obscuredEl, {x: 1, y: 2}); + equal("Element is not clickable at point (1,2) " + + "because it does not have pointer events enabled, " + + "and element would receive the click instead", + err2.message); run_next_test(); }); diff --git a/testing/marionette/transport.js b/testing/marionette/transport.js new file mode 100644 index 000000000000..ad5711f5ec5d --- /dev/null +++ b/testing/marionette/transport.js @@ -0,0 +1,896 @@ +/* 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/. */ + +"use strict"; + +/* global Pipe, ScriptableInputStream, uneval */ + +const {Constructor: CC, classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://devtools/shared/event-emitter.js"); +Cu.import("chrome://marionette/content/stream-utils.js"); +const { Packet, JSONPacket, BulkPacket } = + Cu.import("chrome://marionette/content/packets.js"); +const defer = function () { + let deferred = { + promise: new Promise((resolve, reject) => { + deferred.resolve = resolve; + deferred.reject = reject; + }) + }; + return deferred; +}; +const executeSoon = function (func) { + Services.tm.mainThread.dispatch(func, Ci.nsIThread.DISPATCH_NORMAL); +}; +const flags = { wantVerbose: false, wantLogging: false }; + +const dumpv = + flags.wantVerbose ? + function (msg) {dump(msg + "\n");} : + function () {}; + +const Pipe = CC("@mozilla.org/pipe;1", "nsIPipe", "init"); + +const ScriptableInputStream = CC("@mozilla.org/scriptableinputstream;1", + "nsIScriptableInputStream", "init"); + +this.EXPORTED_SYMBOLS = ["DebuggerTransport"]; + +const PACKET_HEADER_MAX = 200; + +/** + * An adapter that handles data transfers between the debugger client and + * server. It can work with both nsIPipe and nsIServerSocket transports so + * long as the properly created input and output streams are specified. + * (However, for intra-process connections, LocalDebuggerTransport, below, + * is more efficient than using an nsIPipe pair with DebuggerTransport.) + * + * @param input nsIAsyncInputStream + * The input stream. + * @param output nsIAsyncOutputStream + * The output stream. + * + * Given a DebuggerTransport instance dt: + * 1) Set dt.hooks to a packet handler object (described below). + * 2) Call dt.ready() to begin watching for input packets. + * 3) Call dt.send() / dt.startBulkSend() to send packets. + * 4) Call dt.close() to close the connection, and disengage from the event + * loop. + * + * A packet handler is an object with the following methods: + * + * - onPacket(packet) - called when we have received a complete packet. + * |packet| is the parsed form of the packet --- a JavaScript value, not + * a JSON-syntax string. + * + * - onBulkPacket(packet) - called when we have switched to bulk packet + * receiving mode. |packet| is an object containing: + * * actor: Name of actor that will receive the packet + * * type: Name of actor's method that should be called on receipt + * * length: Size of the data to be read + * * stream: This input stream should only be used directly if you can ensure + * that you will read exactly |length| bytes and will not close the + * stream when reading is complete + * * done: If you use the stream directly (instead of |copyTo| below), you + * must signal completion by resolving / rejecting this deferred. + * If it's rejected, the transport will be closed. If an Error is + * supplied as a rejection value, it will be logged via |dump|. + * If you do use |copyTo|, resolving is taken care of for you when + * copying completes. + * * copyTo: A helper function for getting your data out of the stream that + * meets the stream handling requirements above, and has the + * following signature: + * @param output nsIAsyncOutputStream + * The stream to copy to. + * @return Promise + * The promise is resolved when copying completes or rejected if any + * (unexpected) errors occur. + * This object also emits "progress" events for each chunk that is + * copied. See stream-utils.js. + * + * - onClosed(reason) - called when the connection is closed. |reason| is + * an optional nsresult or object, typically passed when the transport is + * closed due to some error in a underlying stream. + * + * See ./packets.js and the Remote Debugging Protocol specification for more + * details on the format of these packets. + */ +function DebuggerTransport(input, output) { + EventEmitter.decorate(this); + + this._input = input; + this._scriptableInput = new ScriptableInputStream(input); + this._output = output; + + // The current incoming (possibly partial) header, which will determine which + // type of Packet |_incoming| below will become. + this._incomingHeader = ""; + // The current incoming Packet object + this._incoming = null; + // A queue of outgoing Packet objects + this._outgoing = []; + + this.hooks = null; + this.active = false; + + this._incomingEnabled = true; + this._outgoingEnabled = true; + + this.close = this.close.bind(this); +} + +DebuggerTransport.prototype = { + /** + * Transmit an object as a JSON packet. + * + * This method returns immediately, without waiting for the entire + * packet to be transmitted, registering event handlers as needed to + * transmit the entire packet. Packets are transmitted in the order + * they are passed to this method. + */ + send: function (object) { + this.emit("send", object); + + let packet = new JSONPacket(this); + packet.object = object; + this._outgoing.push(packet); + this._flushOutgoing(); + }, + + /** + * Transmit streaming data via a bulk packet. + * + * This method initiates the bulk send process by queuing up the header data. + * The caller receives eventual access to a stream for writing. + * + * N.B.: Do *not* attempt to close the stream handed to you, as it will + * continue to be used by this transport afterwards. Most users should + * instead use the provided |copyFrom| function instead. + * + * @param header Object + * This is modeled after the format of JSON packets above, but does not + * actually contain the data, but is instead just a routing header: + * * actor: Name of actor that will receive the packet + * * type: Name of actor's method that should be called on receipt + * * length: Size of the data to be sent + * @return Promise + * The promise will be resolved when you are allowed to write to the + * stream with an object containing: + * * stream: This output stream should only be used directly if + * you can ensure that you will write exactly |length| + * bytes and will not close the stream when writing is + * complete + * * done: If you use the stream directly (instead of |copyFrom| + * below), you must signal completion by resolving / + * rejecting this deferred. If it's rejected, the + * transport will be closed. If an Error is supplied as + * a rejection value, it will be logged via |dump|. If + * you do use |copyFrom|, resolving is taken care of for + * you when copying completes. + * * copyFrom: A helper function for getting your data onto the + * stream that meets the stream handling requirements + * above, and has the following signature: + * @param input nsIAsyncInputStream + * The stream to copy from. + * @return Promise + * The promise is resolved when copying completes or + * rejected if any (unexpected) errors occur. + * This object also emits "progress" events for each chunk + * that is copied. See stream-utils.js. + */ + startBulkSend: function (header) { + this.emit("startbulksend", header); + + let packet = new BulkPacket(this); + packet.header = header; + this._outgoing.push(packet); + this._flushOutgoing(); + return packet.streamReadyForWriting; + }, + + /** + * Close the transport. + * @param reason nsresult / object (optional) + * The status code or error message that corresponds to the reason for + * closing the transport (likely because a stream closed or failed). + */ + close: function (reason) { + this.emit("close", reason); + + this.active = false; + this._input.close(); + this._scriptableInput.close(); + this._output.close(); + this._destroyIncoming(); + this._destroyAllOutgoing(); + if (this.hooks) { + this.hooks.onClosed(reason); + this.hooks = null; + } + if (reason) { + dumpv("Transport closed: " + reason); + } else { + dumpv("Transport closed."); + } + }, + + /** + * The currently outgoing packet (at the top of the queue). + */ + get _currentOutgoing() { + return this._outgoing[0]; + }, + + /** + * Flush data to the outgoing stream. Waits until the output stream notifies + * us that it is ready to be written to (via onOutputStreamReady). + */ + _flushOutgoing: function () { + if (!this._outgoingEnabled || this._outgoing.length === 0) { + return; + } + + // If the top of the packet queue has nothing more to send, remove it. + if (this._currentOutgoing.done) { + this._finishCurrentOutgoing(); + } + + if (this._outgoing.length > 0) { + let threadManager = Cc["@mozilla.org/thread-manager;1"].getService(); + this._output.asyncWait(this, 0, 0, threadManager.currentThread); + } + }, + + /** + * Pause this transport's attempts to write to the output stream. This is + * used when we've temporarily handed off our output stream for writing bulk + * data. + */ + pauseOutgoing: function () { + this._outgoingEnabled = false; + }, + + /** + * Resume this transport's attempts to write to the output stream. + */ + resumeOutgoing: function () { + this._outgoingEnabled = true; + this._flushOutgoing(); + }, + + // nsIOutputStreamCallback + /** + * This is called when the output stream is ready for more data to be written. + * The current outgoing packet will attempt to write some amount of data, but + * may not complete. + */ + onOutputStreamReady: function (stream) { + if (!this._outgoingEnabled || this._outgoing.length === 0) { + return; + } + + try { + this._currentOutgoing.write(stream); + } catch (e) { + if (e.result != Cr.NS_BASE_STREAM_WOULD_BLOCK) { + this.close(e.result); + return; + } + throw e; + } + + this._flushOutgoing(); + }, + + /** + * Remove the current outgoing packet from the queue upon completion. + */ + _finishCurrentOutgoing: function () { + if (this._currentOutgoing) { + this._currentOutgoing.destroy(); + this._outgoing.shift(); + } + }, + + /** + * Clear the entire outgoing queue. + */ + _destroyAllOutgoing: function () { + for (let packet of this._outgoing) { + packet.destroy(); + } + this._outgoing = []; + }, + + /** + * Initialize the input stream for reading. Once this method has been called, + * we watch for packets on the input stream, and pass them to the appropriate + * handlers via this.hooks. + */ + ready: function () { + this.active = true; + this._waitForIncoming(); + }, + + /** + * Asks the input stream to notify us (via onInputStreamReady) when it is + * ready for reading. + */ + _waitForIncoming: function () { + if (this._incomingEnabled) { + let threadManager = Cc["@mozilla.org/thread-manager;1"].getService(); + this._input.asyncWait(this, 0, 0, threadManager.currentThread); + } + }, + + /** + * Pause this transport's attempts to read from the input stream. This is + * used when we've temporarily handed off our input stream for reading bulk + * data. + */ + pauseIncoming: function () { + this._incomingEnabled = false; + }, + + /** + * Resume this transport's attempts to read from the input stream. + */ + resumeIncoming: function () { + this._incomingEnabled = true; + this._flushIncoming(); + this._waitForIncoming(); + }, + + // nsIInputStreamCallback + /** + * Called when the stream is either readable or closed. + */ + onInputStreamReady: function (stream) { + try { + while (stream.available() && this._incomingEnabled && + this._processIncoming(stream, stream.available())) { + // Loop until there is nothing more to process + } + this._waitForIncoming(); + } catch (e) { + if (e.result != Cr.NS_BASE_STREAM_WOULD_BLOCK) { + this.close(e.result); + } else { + throw e; + } + } + }, + + /** + * Process the incoming data. Will create a new currently incoming Packet if + * needed. Tells the incoming Packet to read as much data as it can, but + * reading may not complete. The Packet signals that its data is ready for + * delivery by calling one of this transport's _on*Ready methods (see + * ./packets.js and the _on*Ready methods below). + * @return boolean + * Whether incoming stream processing should continue for any + * remaining data. + */ + _processIncoming: function (stream, count) { + dumpv("Data available: " + count); + + if (!count) { + dumpv("Nothing to read, skipping"); + return false; + } + + try { + if (!this._incoming) { + dumpv("Creating a new packet from incoming"); + + if (!this._readHeader(stream)) { + // Not enough data to read packet type + return false; + } + + // Attempt to create a new Packet by trying to parse each possible + // header pattern. + this._incoming = Packet.fromHeader(this._incomingHeader, this); + if (!this._incoming) { + throw new Error("No packet types for header: " + + this._incomingHeader); + } + } + + if (!this._incoming.done) { + // We have an incomplete packet, keep reading it. + dumpv("Existing packet incomplete, keep reading"); + this._incoming.read(stream, this._scriptableInput); + } + } catch (e) { + let msg = "Error reading incoming packet: (" + e + " - " + e.stack + ")"; + dump(msg + "\n"); + + // Now in an invalid state, shut down the transport. + this.close(); + return false; + } + + if (!this._incoming.done) { + // Still not complete, we'll wait for more data. + dumpv("Packet not done, wait for more"); + return true; + } + + // Ready for next packet + this._flushIncoming(); + return true; + }, + + /** + * Read as far as we can into the incoming data, attempting to build up a + * complete packet header (which terminates with ":"). We'll only read up to + * PACKET_HEADER_MAX characters. + * @return boolean + * True if we now have a complete header. + */ + _readHeader: function () { + let amountToRead = PACKET_HEADER_MAX - this._incomingHeader.length; + this._incomingHeader += + StreamUtils.delimitedRead(this._scriptableInput, ":", amountToRead); + if (flags.wantVerbose) { + dumpv("Header read: " + this._incomingHeader); + } + + if (this._incomingHeader.endsWith(":")) { + if (flags.wantVerbose) { + dumpv("Found packet header successfully: " + this._incomingHeader); + } + return true; + } + + if (this._incomingHeader.length >= PACKET_HEADER_MAX) { + throw new Error("Failed to parse packet header!"); + } + + // Not enough data yet. + return false; + }, + + /** + * If the incoming packet is done, log it as needed and clear the buffer. + */ + _flushIncoming: function () { + if (!this._incoming.done) { + return; + } + if (flags.wantLogging) { + dumpv("Got: " + this._incoming); + } + this._destroyIncoming(); + }, + + /** + * Handler triggered by an incoming JSONPacket completing it's |read| method. + * Delivers the packet to this.hooks.onPacket. + */ + _onJSONObjectReady: function (object) { + executeSoon(() => { + // Ensure the transport is still alive by the time this runs. + if (this.active) { + this.emit("packet", object); + this.hooks.onPacket(object); + } + }); + }, + + /** + * Handler triggered by an incoming BulkPacket entering the |read| phase for + * the stream portion of the packet. Delivers info about the incoming + * streaming data to this.hooks.onBulkPacket. See the main comment on the + * transport at the top of this file for more details. + */ + _onBulkReadReady: function (...args) { + executeSoon(() => { + // Ensure the transport is still alive by the time this runs. + if (this.active) { + this.emit("bulkpacket", ...args); + this.hooks.onBulkPacket(...args); + } + }); + }, + + /** + * Remove all handlers and references related to the current incoming packet, + * either because it is now complete or because the transport is closing. + */ + _destroyIncoming: function () { + if (this._incoming) { + this._incoming.destroy(); + } + this._incomingHeader = ""; + this._incoming = null; + } + +}; + +/** + * An adapter that handles data transfers between the debugger client and + * server when they both run in the same process. It presents the same API as + * DebuggerTransport, but instead of transmitting serialized messages across a + * connection it merely calls the packet dispatcher of the other side. + * + * @param other LocalDebuggerTransport + * The other endpoint for this debugger connection. + * + * @see DebuggerTransport + */ +function LocalDebuggerTransport(other) { + EventEmitter.decorate(this); + + this.other = other; + this.hooks = null; + + // A packet number, shared between this and this.other. This isn't used by the + // protocol at all, but it makes the packet traces a lot easier to follow. + this._serial = this.other ? this.other._serial : { count: 0 }; + this.close = this.close.bind(this); +} + +LocalDebuggerTransport.prototype = { + /** + * Transmit a message by directly calling the onPacket handler of the other + * endpoint. + */ + send: function (packet) { + this.emit("send", packet); + + let serial = this._serial.count++; + if (flags.wantLogging) { + // Check 'from' first, as 'echo' packets have both. + if (packet.from) { + dumpv("Packet " + serial + " sent from " + uneval(packet.from)); + } else if (packet.to) { + dumpv("Packet " + serial + " sent to " + uneval(packet.to)); + } + } + this._deepFreeze(packet); + let other = this.other; + if (other) { + executeSoon(() => { + // Avoid the cost of JSON.stringify() when logging is disabled. + if (flags.wantLogging) { + dumpv("Received packet " + serial + ": " + JSON.stringify(packet, null, 2)); + } + if (other.hooks) { + other.emit("packet", packet); + other.hooks.onPacket(packet); + } + }); + } + }, + + /** + * Send a streaming bulk packet directly to the onBulkPacket handler of the + * other endpoint. + * + * This case is much simpler than the full DebuggerTransport, since there is + * no primary stream we have to worry about managing while we hand it off to + * others temporarily. Instead, we can just make a single use pipe and be + * done with it. + */ + startBulkSend: function ({actor, type, length}) { + this.emit("startbulksend", {actor, type, length}); + + let serial = this._serial.count++; + + dumpv("Sent bulk packet " + serial + " for actor " + actor); + if (!this.other) { + let error = new Error("startBulkSend: other side of transport missing"); + return Promise.reject(error); + } + + let pipe = new Pipe(true, true, 0, 0, null); + + executeSoon(() => { + dumpv("Received bulk packet " + serial); + if (!this.other.hooks) { + return; + } + + // Receiver + let deferred = defer(); + let packet = { + actor: actor, + type: type, + length: length, + copyTo: (output) => { + let copying = + StreamUtils.copyStream(pipe.inputStream, output, length); + deferred.resolve(copying); + return copying; + }, + stream: pipe.inputStream, + done: deferred + }; + + this.other.emit("bulkpacket", packet); + this.other.hooks.onBulkPacket(packet); + + // Await the result of reading from the stream + deferred.promise.then(() => pipe.inputStream.close(), this.close); + }); + + // Sender + let sendDeferred = defer(); + + // The remote transport is not capable of resolving immediately here, so we + // shouldn't be able to either. + executeSoon(() => { + let copyDeferred = defer(); + + sendDeferred.resolve({ + copyFrom: (input) => { + let copying = + StreamUtils.copyStream(input, pipe.outputStream, length); + copyDeferred.resolve(copying); + return copying; + }, + stream: pipe.outputStream, + done: copyDeferred + }); + + // Await the result of writing to the stream + copyDeferred.promise.then(() => pipe.outputStream.close(), this.close); + }); + + return sendDeferred.promise; + }, + + /** + * Close the transport. + */ + close: function () { + this.emit("close"); + + if (this.other) { + // Remove the reference to the other endpoint before calling close(), to + // avoid infinite recursion. + let other = this.other; + this.other = null; + other.close(); + } + if (this.hooks) { + try { + this.hooks.onClosed(); + } catch (ex) { + console.error(ex); + } + this.hooks = null; + } + }, + + /** + * An empty method for emulating the DebuggerTransport API. + */ + ready: function () {}, + + /** + * Helper function that makes an object fully immutable. + */ + _deepFreeze: function (object) { + Object.freeze(object); + for (let prop in object) { + // Freeze the properties that are objects, not on the prototype, and not + // already frozen. Note that this might leave an unfrozen reference + // somewhere in the object if there is an already frozen object containing + // an unfrozen object. + if (object.hasOwnProperty(prop) && typeof object === "object" && + !Object.isFrozen(object)) { + this._deepFreeze(object[prop]); + } + } + } +}; + +/** + * A transport for the debugging protocol that uses nsIMessageManagers to + * exchange packets with servers running in child processes. + * + * In the parent process, |mm| should be the nsIMessageSender for the + * child process. In a child process, |mm| should be the child process + * message manager, which sends packets to the parent. + * + * |prefix| is a string included in the message names, to distinguish + * multiple servers running in the same child process. + * + * This transport exchanges messages named 'debug::packet', where + * is |prefix|, whose data is the protocol packet. + */ +function ChildDebuggerTransport(mm, prefix) { + EventEmitter.decorate(this); + + this._mm = mm; + this._messageName = "debug:" + prefix + ":packet"; +} + +/* + * To avoid confusion, we use 'message' to mean something that + * nsIMessageSender conveys, and 'packet' to mean a remote debugging + * protocol packet. + */ +ChildDebuggerTransport.prototype = { + constructor: ChildDebuggerTransport, + + hooks: null, + + _addListener() { + this._mm.addMessageListener(this._messageName, this); + }, + + _removeListener() { + try { + this._mm.removeMessageListener(this._messageName, this); + } catch (e) { + if (e.result != Cr.NS_ERROR_NULL_POINTER) { + throw e; + } + // In some cases, especially when using messageManagers in non-e10s mode, we reach + // this point with a dead messageManager which only throws errors but does not + // seem to indicate in any other way that it is dead. + } + }, + + ready: function () { + this._addListener(); + }, + + close: function () { + this._removeListener(); + this.emit("close"); + this.hooks.onClosed(); + }, + + receiveMessage: function ({data}) { + this.emit("packet", data); + this.hooks.onPacket(data); + }, + + send: function (packet) { + this.emit("send", packet); + try { + this._mm.sendAsyncMessage(this._messageName, packet); + } catch (e) { + if (e.result != Cr.NS_ERROR_NULL_POINTER) { + throw e; + } + // In some cases, especially when using messageManagers in non-e10s mode, we reach + // this point with a dead messageManager which only throws errors but does not + // seem to indicate in any other way that it is dead. + } + }, + + startBulkSend: function () { + throw new Error("Can't send bulk data to child processes."); + }, + + swapBrowser(mm) { + this._removeListener(); + this._mm = mm; + this._addListener(); + }, +}; + +// WorkerDebuggerTransport is defined differently depending on whether we are +// on the main thread or a worker thread. In the former case, we are required +// by the devtools loader, and isWorker will be false. Otherwise, we are +// required by the worker loader, and isWorker will be true. +// +// Each worker debugger supports only a single connection to the main thread. +// However, its theoretically possible for multiple servers to connect to the +// same worker. Consequently, each transport has a connection id, to allow +// messages from multiple connections to be multiplexed on a single channel. + +if (!this.isWorker) { + // Main thread + (function () { + /** + * A transport that uses a WorkerDebugger to send packets from the main + * thread to a worker thread. + */ + function WorkerDebuggerTransport(dbg, id) { + this._dbg = dbg; + this._id = id; + this.onMessage = this._onMessage.bind(this); + } + + WorkerDebuggerTransport.prototype = { + constructor: WorkerDebuggerTransport, + + ready: function () { + this._dbg.addListener(this); + }, + + close: function () { + this._dbg.removeListener(this); + if (this.hooks) { + this.hooks.onClosed(); + } + }, + + send: function (packet) { + this._dbg.postMessage(JSON.stringify({ + type: "message", + id: this._id, + message: packet + })); + }, + + startBulkSend: function () { + throw new Error("Can't send bulk data from worker threads!"); + }, + + _onMessage: function (message) { + let packet = JSON.parse(message); + if (packet.type !== "message" || packet.id !== this._id) { + return; + } + + if (this.hooks) { + this.hooks.onPacket(packet.message); + } + } + }; + + }).call(this); +} else { + // Worker thread + (function () { + /** + * A transport that uses a WorkerDebuggerGlobalScope to send packets from a + * worker thread to the main thread. + */ + function WorkerDebuggerTransport(scope, id) { + this._scope = scope; + this._id = id; + this._onMessage = this._onMessage.bind(this); + } + + WorkerDebuggerTransport.prototype = { + constructor: WorkerDebuggerTransport, + + ready: function () { + this._scope.addEventListener("message", this._onMessage); + }, + + close: function () { + this._scope.removeEventListener("message", this._onMessage); + if (this.hooks) { + this.hooks.onClosed(); + } + }, + + send: function (packet) { + this._scope.postMessage(JSON.stringify({ + type: "message", + id: this._id, + message: packet + })); + }, + + startBulkSend: function () { + throw new Error("Can't send bulk data from worker threads!"); + }, + + _onMessage: function (event) { + let packet = JSON.parse(event.data); + if (packet.type !== "message" || packet.id !== this._id) { + return; + } + + if (this.hooks) { + this.hooks.onPacket(packet.message); + } + } + }; + + }).call(this); +} diff --git a/testing/mochitest/leaks.py b/testing/mochitest/leaks.py index 6af10b3d2286..2c428180a91c 100644 --- a/testing/mochitest/leaks.py +++ b/testing/mochitest/leaks.py @@ -167,6 +167,7 @@ class LSANLeaks(object): self.logger = logger self.inReport = False self.fatalError = False + self.symbolizerError = False self.foundFrames = set([]) self.recordMoreFrames = None self.currStack = None @@ -188,6 +189,8 @@ class LSANLeaks(object): "==\d+==ERROR: LeakSanitizer: detected memory leaks") self.fatalErrorRegExp = re.compile( "==\d+==LeakSanitizer has encountered a fatal error.") + self.symbolizerOomRegExp = re.compile( + "LLVMSymbolizer: error reading file: Cannot allocate memory") self.stackFrameRegExp = re.compile(" #\d+ 0x[0-9a-f]+ in ([^( 1.00: self._tinderbox_print('CPU {}
{:,.1f} ({:,.1f}%)'.format( attr, value, percent)) diff --git a/testing/mozharness/mozharness/mozilla/building/buildbase.py b/testing/mozharness/mozharness/mozilla/building/buildbase.py index fc45873937ed..1d7e6a2b03ad 100755 --- a/testing/mozharness/mozharness/mozilla/building/buildbase.py +++ b/testing/mozharness/mozharness/mozilla/building/buildbase.py @@ -1919,13 +1919,21 @@ or run without that action (ie: --no-{action})" 'subtests': [], } - for stat in ['cache_write_errors', 'requests_not_cacheable']: - yield { - 'name': 'sccache %s' % stat, - 'value': stats['stats'][stat], - 'extraOptions': self.perfherder_resource_options(), - 'subtests': [], - } + yield { + 'name': 'sccache cache_write_errors', + 'value': stats['stats']['cache_write_errors'], + 'extraOptions': self.perfherder_resource_options(), + 'alertThreshold': 50.0, + 'subtests': [], + } + + yield { + 'name': 'sccache requests_not_cacheable', + 'value': stats['stats']['requests_not_cacheable'], + 'extraOptions': self.perfherder_resource_options(), + 'alertThreshold': 50.0, + 'subtests': [], + } def get_firefox_version(self): versionFilePath = os.path.join( diff --git a/testing/profiles/prefs_general.js b/testing/profiles/prefs_general.js index 1b0ef3538423..afba1a48fca6 100644 --- a/testing/profiles/prefs_general.js +++ b/testing/profiles/prefs_general.js @@ -323,9 +323,6 @@ user_pref("dom.ipc.tabs.shutdownTimeoutSecs", 0); // Don't block add-ons for e10s user_pref("extensions.e10sBlocksEnabling", false); -// Avoid performing Reader Mode intros during tests. -user_pref("browser.reader.detectedFirstArticle", true); - // Make tests run consistently on DevEdition (which has a lightweight theme // selected by default). user_pref("lightweightThemes.selectedThemeID", ""); diff --git a/testing/specialpowers/content/MockFilePicker.jsm b/testing/specialpowers/content/MockFilePicker.jsm index f139cac47937..42e4da09fa30 100644 --- a/testing/specialpowers/content/MockFilePicker.jsm +++ b/testing/specialpowers/content/MockFilePicker.jsm @@ -72,6 +72,7 @@ this.MockFilePicker = { this.appendFilterCallback = null; this.appendFiltersCallback = null; this.displayDirectory = null; + this.displaySpecialDirectory = ""; this.filterIndex = 0; this.mode = null; this.returnData = []; @@ -179,6 +180,7 @@ MockFilePickerInstance.prototype = { parent: null, filterIndex: 0, displayDirectory: null, + displaySpecialDirectory: "", get file() { if (MockFilePicker.returnData.length >= 1) { return MockFilePicker.returnData[0].nsIFile; @@ -269,6 +271,7 @@ MockFilePickerInstance.prototype = { } MockFilePicker.displayDirectory = this.displayDirectory; + MockFilePicker.displaySpecialDirectory = this.displaySpecialDirectory; MockFilePicker.shown = true; if (typeof MockFilePicker.showCallback == "function") { try { diff --git a/testing/talos/talos/scripts/MozillaFileLogger.js b/testing/talos/talos/scripts/MozillaFileLogger.js index fb1115b9a59e..cf8dbb2c8cce 100644 --- a/testing/talos/talos/scripts/MozillaFileLogger.js +++ b/testing/talos/talos/scripts/MozillaFileLogger.js @@ -9,11 +9,10 @@ function contentDispatchEvent(type, data, sync) { data = {}; } - var element = document.createEvent("datacontainerevent"); - element.initEvent("contentEvent", true, false); - element.setData("sync", sync); - element.setData("type", type); - element.setData("data", JSON.stringify(data)); + var element = new CustomEvent("contentEvent", { + bubbles: true, + detail: { sync: sync, type: type, data: JSON.stringify(data) } + }); document.dispatchEvent(element); } diff --git a/testing/web-platform/meta/MANIFEST.json b/testing/web-platform/meta/MANIFEST.json index 58c757a99550..1fb5a318bb5e 100644 --- a/testing/web-platform/meta/MANIFEST.json +++ b/testing/web-platform/meta/MANIFEST.json @@ -86370,6 +86370,12 @@ {} ] ], + "cssom/getComputedStyle-pseudo.html": [ + [ + "/cssom/getComputedStyle-pseudo.html", + {} + ] + ], "cssom/historical.html": [ [ "/cssom/historical.html", @@ -165017,6 +165023,10 @@ "3a0e6f64f70f863d679e537c4bfb76aaa0d3598a", "testharness" ], + "cssom/getComputedStyle-pseudo.html": [ + "6c74e57a2e32a13cc3b7e955a2d89dafdf6d1730", + "testharness" + ], "cssom/historical.html": [ "2c78218b89efb9bdf60cf708920be142051347c7", "testharness" diff --git a/testing/web-platform/meta/dom/nodes/Document-createEvent.html.ini b/testing/web-platform/meta/dom/nodes/Document-createEvent.html.ini index 46760df2ea7e..79cc9209d37b 100644 --- a/testing/web-platform/meta/dom/nodes/Document-createEvent.html.ini +++ b/testing/web-platform/meta/dom/nodes/Document-createEvent.html.ini @@ -48,10 +48,6 @@ expected: FAIL bug: https://github.com/whatwg/dom/issues/362, 1314303 - [Should throw NOT_SUPPORTED_ERR for pluralized legacy event interface "DragEvents"] - expected: FAIL - bug: 1251198 - [FocusEvent should be an alias for FocusEvent.] expected: FAIL bug: https://github.com/whatwg/dom/issues/362, 1314303 @@ -148,10 +144,6 @@ expected: FAIL bug: https://github.com/whatwg/dom/issues/362, 1314303 - [Should throw NOT_SUPPORTED_ERR for pluralized legacy event interface "TextEvents"] - expected: FAIL - bug: 1251198 - [TrackEvent should be an alias for TrackEvent.] expected: FAIL bug: https://github.com/whatwg/dom/issues/362, 1314303 @@ -248,22 +240,6 @@ expected: FAIL bug: https://github.com/whatwg/dom/issues/362, 1314303 - [Should throw NOT_SUPPORTED_ERR for non-legacy event interface "CommandEvent"] - expected: FAIL - bug: 1251198 - - [Should throw NOT_SUPPORTED_ERR for pluralized non-legacy event interface "CommandEvents"] - expected: FAIL - bug: 1251198 - - [Should throw NOT_SUPPORTED_ERR for non-legacy event interface "DataContainerEvent"] - expected: FAIL - bug: 1251198 - - [Should throw NOT_SUPPORTED_ERR for pluralized non-legacy event interface "DataContainerEvents"] - expected: FAIL - bug: 1251198 - [Should throw NOT_SUPPORTED_ERR for pluralized non-legacy event interface "KeyEvents"] expected: FAIL bug: 1251198 @@ -280,43 +256,11 @@ expected: FAIL bug: 1251198 - [Should throw NOT_SUPPORTED_ERR for non-legacy event interface "NotifyPaintEvent"] - expected: FAIL - bug: 1251198 - - [Should throw NOT_SUPPORTED_ERR for non-legacy event interface "PageTransition"] - expected: FAIL - bug: 1251198 - - [Should throw NOT_SUPPORTED_ERR for pluralized non-legacy event interface "PopUpEvents"] - expected: FAIL - bug: 1251198 - [Should throw NOT_SUPPORTED_ERR for non-legacy event interface "ScrollAreaEvent"] expected: FAIL bug: 1251198 - [Should throw NOT_SUPPORTED_ERR for non-legacy event interface "SimpleGestureEvent"] - expected: FAIL - bug: 1251198 - - [Should throw NOT_SUPPORTED_ERR for non-legacy event interface "SVGEvent"] - expected: FAIL - bug: 1251198 - [Should throw NOT_SUPPORTED_ERR for non-legacy event interface "TimeEvent"] expected: FAIL bug: 1251198 - [Should throw NOT_SUPPORTED_ERR for pluralized non-legacy event interface "TimeEvents"] - expected: FAIL - bug: 1251198 - - [Should throw NOT_SUPPORTED_ERR for non-legacy event interface "XULCommandEvent"] - expected: FAIL - bug: 1251198 - - [Should throw NOT_SUPPORTED_ERR for pluralized non-legacy event interface "XULCommandEvents"] - expected: FAIL - bug: 1251198 - diff --git a/testing/web-platform/meta/selection/interfaces.html.ini b/testing/web-platform/meta/selection/interfaces.html.ini index 2e254cfdb2b1..6d8dce9b3580 100644 --- a/testing/web-platform/meta/selection/interfaces.html.ini +++ b/testing/web-platform/meta/selection/interfaces.html.ini @@ -3,30 +3,6 @@ [Selection interface: attribute type] expected: FAIL - [Selection interface: operation empty()] - expected: FAIL - - [Selection interface: operation collapse(Node,unsigned long)] - expected: FAIL - - [Selection interface: operation setPosition(Node,unsigned long)] - expected: FAIL - - [Selection interface: operation extend(Node,unsigned long)] - expected: FAIL - - [Selection interface: operation containsNode(Node,boolean)] - expected: FAIL - [Selection interface: getSelection() must inherit property "type" with the proper type (6)] expected: FAIL - [Selection interface: getSelection() must inherit property "empty" with the proper type (11)] - expected: FAIL - - [Selection interface: getSelection() must inherit property "setPosition" with the proper type (13)] - expected: FAIL - - [Selection interface: calling setPosition(Node,unsigned long) on getSelection() with too few arguments must throw TypeError] - expected: FAIL - diff --git a/testing/web-platform/meta/selection/removeRange.html.ini b/testing/web-platform/meta/selection/removeRange.html.ini deleted file mode 100644 index 7285e6bd9244..000000000000 --- a/testing/web-platform/meta/selection/removeRange.html.ini +++ /dev/null @@ -1,86 +0,0 @@ -[removeRange.html] - type: testharness - [removeRange() with Range 0] - expected: FAIL - - [removeRange() with Range 1] - expected: FAIL - - [removeRange() with Range 2] - expected: FAIL - - [removeRange() with Range 3] - expected: FAIL - - [removeRange() with Range 4] - expected: FAIL - - [removeRange() with Range 5] - expected: FAIL - - [removeRange() with Range 6] - expected: FAIL - - [removeRange() with Range 7] - expected: FAIL - - [removeRange() with Range 14] - expected: FAIL - - [removeRange() with Range 15] - expected: FAIL - - [removeRange() with Range 16] - expected: FAIL - - [removeRange() with Range 17] - expected: FAIL - - [removeRange() with Range 18] - expected: FAIL - - [removeRange() with Range 22] - expected: FAIL - - [removeRange() with Range 23] - expected: FAIL - - [removeRange() with Range 26] - expected: FAIL - - [removeRange() with Range 27] - expected: FAIL - - [removeRange() with Range 28] - expected: FAIL - - [removeRange() with Range 29] - expected: FAIL - - [removeRange() with Range 30] - expected: FAIL - - [removeRange() with Range 31] - expected: FAIL - - [removeRange() with Range 32] - expected: FAIL - - [removeRange() with Range 34] - expected: FAIL - - [removeRange() with Range 35] - expected: FAIL - - [removeRange() with Range 36] - expected: FAIL - - [removeRange() with Range 37] - expected: FAIL - - [removeRange() with Range 38] - expected: FAIL - - [removeRange() with Range 39] - expected: FAIL - diff --git a/testing/web-platform/mozilla/tests/dom/classList.html b/testing/web-platform/mozilla/tests/dom/classList.html index ecefb0096ec6..b19dbdd8adcd 100644 --- a/testing/web-platform/mozilla/tests/dom/classList.html +++ b/testing/web-platform/mozilla/tests/dom/classList.html @@ -254,12 +254,10 @@ function testClassList(e, desc) { function checkAdd(before, argument, after, expectedException) { checkModification(e, "add", argument, undefined, before, after, expectedException, desc); - // Also check force toggle - // XXX https://github.com/whatwg/dom/issues/443 - //if (!Array.isArray(argument)) { - // checkModification(e, "toggle", [argument, true], true, before, after, - // expectedException); - //} + if (!Array.isArray(argument)) { + checkModification(e, "toggle", [argument, true], true, before, after, + expectedException, desc); + } } checkAdd(null, "", null, "SyntaxError"); @@ -309,12 +307,10 @@ function testClassList(e, desc) { function checkRemove(before, argument, after, expectedException) { checkModification(e, "remove", argument, undefined, before, after, expectedException, desc); - // Also check force toggle - // XXX https://github.com/whatwg/dom/issues/443 - //if (!Array.isArray(argument)) { - // checkModification(e, "toggle", [argument, false], false, before, after, - // expectedException); - //} + if (!Array.isArray(argument)) { + checkModification(e, "toggle", [argument, false], false, before, after, + expectedException, desc); + } } checkRemove(null, "", null, "SyntaxError"); @@ -407,24 +403,6 @@ function testClassList(e, desc) { checkToggle("", undefined, true, "undefined"); - // tests for the force argument handling - // XXX Remove these if https://github.com/whatwg/dom/issues/443 is fixed - - function checkForceToggle(before, argument, force, expectedRes, after, expectedException) { - checkModification(e, "toggle", [argument, force], expectedRes, before, - after, expectedException, desc); - } - - checkForceToggle("", "a", true, true, "a"); - checkForceToggle("a", "a", true, true, "a"); - checkForceToggle("a", "b", true, true, "a b"); - checkForceToggle("a b", "b", true, true, "a b"); - checkForceToggle("", "a", false, false, ""); - checkForceToggle("a", "a", false, false, ""); - checkForceToggle("a", "b", false, false, "a"); - checkForceToggle("a b", "b", false, false, "a"); - - // replace() method function checkReplace(before, token, newToken, after, expectedException) { checkModification(e, "replace", [token, newToken], undefined, before, @@ -464,19 +442,15 @@ function testClassList(e, desc) { checkReplace("a", "A", "b", "a"); checkReplace("a b", "b", "A", "a A"); checkReplace("a b c", "d", "e", "a b c"); - // https://github.com/whatwg/dom/issues/443 checkReplace("a a a b", "a", "a", "a b"); - checkReplace("a a a b", "c", "d", "a a a b"); + checkReplace("a a a b", "c", "d", "a b"); checkReplace(null, "a", "b", null); checkReplace("", "a", "b", ""); - checkReplace(" ", "a", "b", " "); + checkReplace(" ", "a", "b", ""); checkReplace(" a \f", "a", "b", "b"); checkReplace("a b c", "b", "d", "a d c"); - // https://github.com/whatwg/dom/issues/442 - // Implementations agree on the first one here, so I test it, but disagree on - // the second, so no test until the spec decides what to say. checkReplace("a b c", "c", "a", "a b"); - //checkReplace("c b a", "c", "a", ???); + checkReplace("c b a", "c", "a", "a b"); checkReplace("a b a", "a", "c", "c b"); checkReplace("a b a", "b", "c", "a c"); checkReplace(" a a b", "a", "c", "c b"); diff --git a/testing/web-platform/tests/cssom/getComputedStyle-pseudo.html b/testing/web-platform/tests/cssom/getComputedStyle-pseudo.html new file mode 100644 index 000000000000..8efd484fc21d --- /dev/null +++ b/testing/web-platform/tests/cssom/getComputedStyle-pseudo.html @@ -0,0 +1,43 @@ + + +CSSOM: Correct resolution of resolved value for display-affected pseudo-elements + + + + + + +
+
+
+ diff --git a/testing/web-platform/tests/dom/nodes/Element-classlist.html b/testing/web-platform/tests/dom/nodes/Element-classlist.html index 5beee0942064..d87db5e9f24f 100644 --- a/testing/web-platform/tests/dom/nodes/Element-classlist.html +++ b/testing/web-platform/tests/dom/nodes/Element-classlist.html @@ -202,12 +202,10 @@ function testClassList(e, desc) { function checkAdd(before, argument, after, expectedException) { checkModification(e, "add", argument, undefined, before, after, expectedException, desc); - // Also check force toggle - // XXX https://github.com/whatwg/dom/issues/443 - //if (!Array.isArray(argument)) { - // checkModification(e, "toggle", [argument, true], true, before, after, - // expectedException); - //} + if (!Array.isArray(argument)) { + checkModification(e, "toggle", [argument, true], true, before, after, + expectedException, desc); + } } checkAdd(null, "", null, "SyntaxError"); @@ -257,12 +255,10 @@ function testClassList(e, desc) { function checkRemove(before, argument, after, expectedException) { checkModification(e, "remove", argument, undefined, before, after, expectedException, desc); - // Also check force toggle - // XXX https://github.com/whatwg/dom/issues/443 - //if (!Array.isArray(argument)) { - // checkModification(e, "toggle", [argument, false], false, before, after, - // expectedException); - //} + if (!Array.isArray(argument)) { + checkModification(e, "toggle", [argument, false], false, before, after, + expectedException, desc); + } } checkRemove(null, "", null, "SyntaxError"); @@ -355,24 +351,6 @@ function testClassList(e, desc) { checkToggle("", undefined, true, "undefined"); - // tests for the force argument handling - // XXX Remove these if https://github.com/whatwg/dom/issues/443 is fixed - - function checkForceToggle(before, argument, force, expectedRes, after, expectedException) { - checkModification(e, "toggle", [argument, force], expectedRes, before, - after, expectedException, desc); - } - - checkForceToggle("", "a", true, true, "a"); - checkForceToggle("a", "a", true, true, "a"); - checkForceToggle("a", "b", true, true, "a b"); - checkForceToggle("a b", "b", true, true, "a b"); - checkForceToggle("", "a", false, false, ""); - checkForceToggle("a", "a", false, false, ""); - checkForceToggle("a", "b", false, false, "a"); - checkForceToggle("a b", "b", false, false, "a"); - - // replace() method function checkReplace(before, token, newToken, after, expectedException) { checkModification(e, "replace", [token, newToken], undefined, before, @@ -412,19 +390,15 @@ function testClassList(e, desc) { checkReplace("a", "A", "b", "a"); checkReplace("a b", "b", "A", "a A"); checkReplace("a b c", "d", "e", "a b c"); - // https://github.com/whatwg/dom/issues/443 checkReplace("a a a b", "a", "a", "a b"); - checkReplace("a a a b", "c", "d", "a a a b"); + checkReplace("a a a b", "c", "d", "a b"); checkReplace(null, "a", "b", null); checkReplace("", "a", "b", ""); - checkReplace(" ", "a", "b", " "); + checkReplace(" ", "a", "b", ""); checkReplace(" a \f", "a", "b", "b"); checkReplace("a b c", "b", "d", "a d c"); - // https://github.com/whatwg/dom/issues/442 - // Implementations agree on the first one here, so I test it, but disagree on - // the second, so no test until the spec decides what to say. checkReplace("a b c", "c", "a", "a b"); - //checkReplace("c b a", "c", "a", ???); + checkReplace("c b a", "c", "a", "a b"); checkReplace("a b a", "a", "c", "c b"); checkReplace("a b a", "b", "c", "a c"); checkReplace(" a a b", "a", "c", "c b"); diff --git a/testing/web-platform/tests/dom/nodes/MutationObserver-attributes.html b/testing/web-platform/tests/dom/nodes/MutationObserver-attributes.html index 6721b7eecd13..359f993fff47 100644 --- a/testing/web-platform/tests/dom/nodes/MutationObserver-attributes.html +++ b/testing/web-platform/tests/dom/nodes/MutationObserver-attributes.html @@ -221,16 +221,18 @@ runMutationTest(n42, var n43 = document.getElementById('n43'); runMutationTest(n43, {"attributes":true, "attributeOldValue": true}, - [{type: "attributes", oldValue: "n43", attributeName: "id"}], + [{type: "attributes", oldValue: "c01 c02", attributeName: "class"}, + {type: "attributes", oldValue: "n43", attributeName: "id"}], function() { n43.classList.toggle("c03", false); n43.id = "n430"; }, - "attributes Element.classList.toggle: forced missing token removal no mutation"); + "attributes Element.classList.toggle: forced missing token removal no-op"); var n44 = document.getElementById('n44'); runMutationTest(n44, {"attributes":true, "attributeOldValue": true}, - [{type: "attributes", oldValue: "n44", attributeName: "id"}], + [{type: "attributes", oldValue: "c01 c02", attributeName: "class"}, + {type: "attributes", oldValue: "n44", attributeName: "id"}], function() { n44.classList.toggle("c01", true); n44.id = "n440"; }, - "attributes Element.classList.toggle: forced existing token addition no mutation"); + "attributes Element.classList.toggle: forced existing token addition no-op"); var n45 = document.getElementById('n45'); runMutationTest(n45, diff --git a/testing/web-platform/tests/selection/interfaces.html b/testing/web-platform/tests/selection/interfaces.html index 888b23fd65d7..8fed814bf285 100644 --- a/testing/web-platform/tests/selection/interfaces.html +++ b/testing/web-platform/tests/selection/interfaces.html @@ -6,7 +6,7 @@