Web Automation: upstream safaridriver's JavaScript atom implementations
authorcarlosgc@webkit.org <carlosgc@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 12 Jul 2017 10:56:22 +0000 (10:56 +0000)
committercarlosgc@webkit.org <carlosgc@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 12 Jul 2017 10:56:22 +0000 (10:56 +0000)
https://bugs.webkit.org/show_bug.cgi?id=172060
<rdar://problem/32168187>

Reviewed by Brian Burg.

* UIProcess/Automation/atoms/ElementAttribute.js: Added.
* UIProcess/Automation/atoms/ElementDisplayed.js: Added.
* UIProcess/Automation/atoms/FindNodes.js: Added.
* UIProcess/Automation/atoms/FormElementClear.js: Added.
* UIProcess/Automation/atoms/FormSubmit.js: Added.

git-svn-id: http://svn.webkit.org/repository/webkit/trunk@219388 268f45cc-cd09-0410-ab3c-d52691b4dbfc

Source/WebKit2/ChangeLog
Source/WebKit2/UIProcess/Automation/atoms/ElementAttribute.js [new file with mode: 0644]
Source/WebKit2/UIProcess/Automation/atoms/ElementDisplayed.js [new file with mode: 0644]
Source/WebKit2/UIProcess/Automation/atoms/FindNodes.js [new file with mode: 0644]
Source/WebKit2/UIProcess/Automation/atoms/FormElementClear.js [new file with mode: 0644]
Source/WebKit2/UIProcess/Automation/atoms/FormSubmit.js [new file with mode: 0644]

index 8402a67..658f618 100644 (file)
@@ -1,3 +1,17 @@
+2017-07-12  Carlos Garcia Campos  <cgarcia@igalia.com>
+
+        Web Automation: upstream safaridriver's JavaScript atom implementations
+        https://bugs.webkit.org/show_bug.cgi?id=172060
+        <rdar://problem/32168187>
+
+        Reviewed by Brian Burg.
+
+        * UIProcess/Automation/atoms/ElementAttribute.js: Added.
+        * UIProcess/Automation/atoms/ElementDisplayed.js: Added.
+        * UIProcess/Automation/atoms/FindNodes.js: Added.
+        * UIProcess/Automation/atoms/FormElementClear.js: Added.
+        * UIProcess/Automation/atoms/FormSubmit.js: Added.
+
 2017-07-11  Dean Jackson  <dino@apple.com>
 
         Remove NAVIGATOR_HWCONCURRENCY
diff --git a/Source/WebKit2/UIProcess/Automation/atoms/ElementAttribute.js b/Source/WebKit2/UIProcess/Automation/atoms/ElementAttribute.js
new file mode 100644 (file)
index 0000000..3a41f69
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2017 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+function(element, attributeName) {
+    "use strict";
+
+    let lowercaseAttributeName = attributeName.toLowerCase();
+    let tagName = element.tagName.toUpperCase();
+
+    // Special Case: "selected" / "checked" work analogously
+    // on options and checkboxes.
+    if (lowercaseAttributeName === "selected" || lowercaseAttributeName === "checked") {
+        switch (tagName) {
+        case "OPTION":
+            return element.selected ? "true" : null;
+        case "INPUT":
+            if (element.type === "checkbox" || element.type === "radio")
+                return element.checked ? "true" : null;
+            break;
+        }
+    }
+
+    // Special Case: For images and links, require the existence of
+    // the attribute before accessing the property.
+    if ((tagName === "IMG" && lowercaseAttributeName === "src") || (tagName === "A" && lowercaseAttributeName === "href")) {
+        let value = element.getAttribute(lowercaseAttributeName);
+        return value ? element[lowercaseAttributeName] : value;
+    }
+
+    // Prefer the element's property over the attribute if the
+    // property value is not null and not an object. For boolean
+    // properties return null if false.
+    try {
+        const propertyNameAliases = {"class": "className", "readonly": "readOnly"};
+        let propertyName = propertyNameAliases[attributeName] || attributeName;
+        let value = element[propertyName];
+        if (value != null && typeof value !== "object") {
+            if (typeof value === "boolean")
+                return value ? "true" : null;
+            return value.toString();
+        }
+    } catch (e) { }
+
+    return element.getAttribute(attributeName);
+}
diff --git a/Source/WebKit2/UIProcess/Automation/atoms/ElementDisplayed.js b/Source/WebKit2/UIProcess/Automation/atoms/ElementDisplayed.js
new file mode 100644 (file)
index 0000000..2af49ba
--- /dev/null
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2017 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+function isShown(element) {
+    "use strict";
+
+    function nodeIsElement(node) {
+        if (!node)
+            return false;
+
+        switch (node.nodeType) {
+        case Node.ELEMENT_NODE:
+        case Node.DOCUMENT_NODE:
+        case Node.DOCUMENT_FRAGMENT_NODE:
+            return true;
+
+        default:
+            return false;
+        }
+    }
+
+    function parentElementForElement(element) {
+        if (!element)
+            return null;
+
+        return enclosingNodeOrSelfMatchingPredicate(element.parentNode, nodeIsElement);
+    }
+
+    function enclosingNodeOrSelfMatchingPredicate(targetNode, predicate) {
+        for (let node = targetNode; node && node !== targetNode.ownerDocument; node = node.parentNode)
+            if (predicate(node))
+                return node;
+
+        return null;
+    }
+
+    function enclosingElementOrSelfMatchingPredicate(targetElement, predicate) {
+        for (let element = targetElement; element && element !== targetElement.ownerDocument; element = parentElementForElement(element))
+            if (predicate(element))
+                return element;
+
+        return null;
+    }
+
+    function cascadedStylePropertyForElement(element, property) {
+        if (!element || !property)
+            return null;
+
+        let computedStyle = window.getComputedStyle(element);
+        let computedStyleProperty = computedStyle.getPropertyValue(property);
+        if (computedStyleProperty && computedStyleProperty !== "inherit")
+            return computedStyleProperty;
+
+        // Ideally getPropertyValue would return the 'used' or 'actual' value, but
+        // it doesn't for legacy reasons. So we need to do our own poor man's cascade.
+        // Fall back to the first non-'inherit' value found in an ancestor.
+        // In any case, getPropertyValue will not return 'initial'.
+
+        // FIXME: will this incorrectly inherit non-inheritable CSS properties?
+        // I think all important non-inheritable properties (width, height, etc.)
+        // for our purposes here are specially resolved, so this may not be an issue.
+        // Specification is here: https://drafts.csswg.org/cssom/#resolved-values
+        let parentElement = parentElementForElement(element);
+        return cascadedStylePropertyForElement(parentElement, property);
+    }
+
+    function elementSubtreeHasNonZeroDimensions(element) {
+        let boundingBox = element.getBoundingClientRect();
+        if (boundingBox.width > 0 && boundingBox.height > 0)
+            return true;
+
+        // Paths can have a zero width or height. Treat them as shown if the stroke width is positive.
+        if (element.tagName.toUpperCase() === "PATH" && boundingBox.width + boundingBox.height > 0) {
+            let strokeWidth = cascadedStylePropertyForElement(element, "stroke-width");
+            return !!strokeWidth && (parseInt(strokeWidth, 10) > 0);
+        }
+
+        let cascadedOverflow = cascadedStylePropertyForElement(element, "overflow");
+        if (cascadedOverflow === "hidden")
+            return false;
+
+        // If the container's overflow is not hidden and it has zero size, consider the
+        // container to have non-zero dimensions if a child node has non-zero dimensions.
+        return Array.from(element.childNodes).some((childNode) => {
+            if (childNode.nodeType === Node.TEXT_NODE)
+                return true;
+
+            if (nodeIsElement(childNode))
+                return elementSubtreeHasNonZeroDimensions(childNode);
+
+            return false;
+        });
+    }
+
+    function elementOverflowsContainer(element) {
+        let cascadedOverflow = cascadedStylePropertyForElement(element, "overflow");
+        if (cascadedOverflow !== "hidden")
+            return false;
+
+        // FIXME: this needs to take into account the scroll position of the element,
+        // the display modes of it and its ancestors, and the container it overflows.
+        // See Selenium's bot.dom.getOverflowState atom for an exhaustive list of edge cases.
+        return true;
+    }
+
+    function isElementSubtreeHiddenByOverflow(element) {
+        if (!element)
+            return false;
+
+        if (!elementOverflowsContainer(element))
+            return false;
+
+        // This element's subtree is hidden by overflow if all child subtrees are as well.
+        return Array.from(element.childNodes).every((childNode) => {
+            // Returns true if the child node is overflowed or otherwise hidden.
+            // Base case: not an element, has zero size, scrolled out, or doesn't overflow container.
+            if (!nodeIsElement(childNode))
+                return true;
+
+            if (!elementSubtreeHasNonZeroDimensions(childNode))
+                return true;
+
+            // Recurse.
+            return isElementSubtreeHiddenByOverflow(childNode);
+        });
+    }
+
+    // This is a partial reimplementation of Selenium's "element is displayed" algorithm.
+    // When the W3C specification's algorithm stabilizes, we should implement that.
+
+    if (!(element instanceof Element))
+        throw new Error("Cannot check the displayedness of a non-Element argument.");
+
+    // If this command is misdirected to the wrong document, treat it as not shown.
+    if (!document.contains(element))
+        return false;
+
+    // Special cases for specific tag names.
+    switch (element.tagName.toUpperCase()) {
+    case "BODY":
+        return true;
+
+    case "SCRIPT":
+    case "NOSCRIPT":
+        return false;
+
+    case "OPTGROUP":
+    case "OPTION":
+        // Option/optgroup are considered shown if the containing <select> is shown.
+        let enclosingSelectElement = enclosingNodeOrSelfMatchingPredicate(element, (e) => e.tagName.toUpperCase() === "SELECT");
+        return isShown(enclosingSelectElement);
+
+    case "INPUT":
+        // <input type="hidden"> is considered not shown.
+        if (element.type === "hidden")
+            return false;
+        break;
+
+    case "MAP":
+        // FIXME: Selenium has special handling for <map> elements. We don't do anything now.
+
+    default:
+        break;
+    }
+
+    if (cascadedStylePropertyForElement(element, "visibility") !== "visible")
+        return false;
+
+    let hasAncestorWithZeroOpacity = !!enclosingElementOrSelfMatchingPredicate(element, (e) => {
+        return Number(cascadedStylePropertyForElement(e, "opacity")) === 0;
+    });
+    let hasAncestorWithDisplayNone = !!enclosingElementOrSelfMatchingPredicate(element, (e) => {
+        return cascadedStylePropertyForElement(e, "display") === "none";
+    });
+    if (hasAncestorWithZeroOpacity || hasAncestorWithDisplayNone)
+        return false;
+
+    if (!elementSubtreeHasNonZeroDimensions(element))
+        return false;
+
+    if (isElementSubtreeHiddenByOverflow(element))
+        return false;
+
+    return true;
+}
diff --git a/Source/WebKit2/UIProcess/Automation/atoms/FindNodes.js b/Source/WebKit2/UIProcess/Automation/atoms/FindNodes.js
new file mode 100644 (file)
index 0000000..5c8dcfe
--- /dev/null
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2017 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+function(strategy, ancestorElement, query, firstResultOnly, timeoutDuration, callback) {
+    ancestorElement = ancestorElement || document;
+
+    switch (strategy) {
+    case "id":
+        strategy = "css selector";
+        query = "[id=\"" + escape(query) + "\"]";
+        break;
+    case "name":
+        strategy = "css selector";
+        query = "[name=\"" + escape(query) + "\"]";
+        break;
+    case "link text":
+        strategy = "xpath";
+        query = ".//a[@href][text() = \"" + escape(query) + "\"]";
+        break;
+    case "partial link text":
+        strategy = "xpath";
+        query = ".//a[@href][contains(text(), \"" + escape(query) + "\")]";
+        break;
+    }
+
+    switch (strategy) {
+    case "css selector":
+    case "tag name":
+    case "class name":
+    case "xpath":
+        break;
+    default:
+        // Unknown strategy.
+        callback(firstResultOnly ? null : []);
+        return;
+    }
+
+    function escape(string) {
+        return string.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
+    }
+
+    function tryToFindNode() {
+        try {
+            switch (strategy) {
+            case "css selector":
+                if (firstResultOnly)
+                    return ancestorElement.querySelector(query) || null;
+                return Array.from(ancestorElement.querySelectorAll(query));
+
+            case "tag name":
+                let tagNameResult = ancestorElement.getElementsByTagName(query);
+                if (firstResultOnly)
+                    return tagNameResult[0] || null;
+                return Array.from(tagNameResult);
+
+            case "class name":
+                let classNameResult = ancestorElement.getElementsByClassName(query);
+                if (firstResultOnly)
+                    return classNameResult[0] || null;
+                return Array.from(classNameResult);
+
+            case "xpath":
+                if (firstResultOnly) {
+                    let xpathResult = document.evaluate(query, ancestorElement, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
+                    if (!xpathResult)
+                        return null;
+                    return xpathResult.singleNodeValue;
+                }
+
+                let xpathResult = document.evaluate(query, ancestorElement, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
+                if (!xpathResult || !xpathResult.snapshotLength)
+                    return [];
+
+                let arrayResult = [];
+                for (let i = 0; i < xpathResult.snapshotLength; ++i)
+                    arrayResult.push(xpathResult.snapshotItem(i));
+                return arrayResult;
+            }
+        } catch (error) {
+            if (error instanceof XPathException && error.code === XPathException.INVALID_EXPRESSION_ERR)
+                return "InvalidXPathExpression";
+            // FIXME: Bad CSS can throw an error that we should report back to the endpoint. There is no
+            // special error code for that though, so we just return an empty match.
+            return firstResultOnly ? null : [];
+        }
+    }
+
+    const pollInterval = 50;
+    let pollUntil = performance.now() + timeoutDuration;
+
+    function pollForNode() {
+        let result = tryToFindNode();
+
+        // Report any valid results.
+        if (typeof result === "string" || result instanceof Node || (result instanceof Array && result.length)) {
+            callback(result);
+            return;
+        }
+
+        // Schedule another attempt if we have time remaining.
+        let durationRemaining = pollUntil - performance.now();
+        if (durationRemaining < pollInterval) {
+            callback(firstResultOnly ? null : []);
+            return;
+        }
+
+        setTimeout(pollForNode, pollInterval);
+    }
+
+    pollForNode();
+}
diff --git a/Source/WebKit2/UIProcess/Automation/atoms/FormElementClear.js b/Source/WebKit2/UIProcess/Automation/atoms/FormElementClear.js
new file mode 100644 (file)
index 0000000..8702dc1
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2017 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+function(element) {
+    "use strict";
+
+    if (element.disabled || element.readOnly)
+        throw {name: "InvalidElementState", message: "Element must be user-editable in order to clear."};
+
+    if (element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement) {
+        element.value = "";
+        return;
+    }
+
+    if (element instanceof HTMLInputElement) {
+        switch (element.type) {
+        case "button":
+        case "submit":
+        case "reset":
+            return;
+
+        case "radio":
+        case "checkbox":
+            element.checked = false;
+            return;
+
+        default:
+            element.value = "";
+            return;
+        }
+    }
+
+    if (element.isContentEditable) {
+        element.innerHTML = "";
+        return;
+    }
+}
diff --git a/Source/WebKit2/UIProcess/Automation/atoms/FormSubmit.js b/Source/WebKit2/UIProcess/Automation/atoms/FormSubmit.js
new file mode 100644 (file)
index 0000000..d431874
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2017 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+function(element) {
+    for (let currentElement = element; currentElement && currentElement !== document; currentElement = currentElement.parentNode) {
+        if (!(currentElement instanceof HTMLFormElement))
+            continue;
+        currentElement.submit();
+        break;
+    }
+}