Web Automation: upstream safaridriver's JavaScript atom implementations
[WebKit.git] / Source / WebKit2 / UIProcess / Automation / atoms / FindNodes.js
1 /*
2  * Copyright (C) 2017 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 function(strategy, ancestorElement, query, firstResultOnly, timeoutDuration, callback) {
27     ancestorElement = ancestorElement || document;
28
29     switch (strategy) {
30     case "id":
31         strategy = "css selector";
32         query = "[id=\"" + escape(query) + "\"]";
33         break;
34     case "name":
35         strategy = "css selector";
36         query = "[name=\"" + escape(query) + "\"]";
37         break;
38     case "link text":
39         strategy = "xpath";
40         query = ".//a[@href][text() = \"" + escape(query) + "\"]";
41         break;
42     case "partial link text":
43         strategy = "xpath";
44         query = ".//a[@href][contains(text(), \"" + escape(query) + "\")]";
45         break;
46     }
47
48     switch (strategy) {
49     case "css selector":
50     case "tag name":
51     case "class name":
52     case "xpath":
53         break;
54     default:
55         // Unknown strategy.
56         callback(firstResultOnly ? null : []);
57         return;
58     }
59
60     function escape(string) {
61         return string.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
62     }
63
64     function tryToFindNode() {
65         try {
66             switch (strategy) {
67             case "css selector":
68                 if (firstResultOnly)
69                     return ancestorElement.querySelector(query) || null;
70                 return Array.from(ancestorElement.querySelectorAll(query));
71
72             case "tag name":
73                 let tagNameResult = ancestorElement.getElementsByTagName(query);
74                 if (firstResultOnly)
75                     return tagNameResult[0] || null;
76                 return Array.from(tagNameResult);
77
78             case "class name":
79                 let classNameResult = ancestorElement.getElementsByClassName(query);
80                 if (firstResultOnly)
81                     return classNameResult[0] || null;
82                 return Array.from(classNameResult);
83
84             case "xpath":
85                 if (firstResultOnly) {
86                     let xpathResult = document.evaluate(query, ancestorElement, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
87                     if (!xpathResult)
88                         return null;
89                     return xpathResult.singleNodeValue;
90                 }
91
92                 let xpathResult = document.evaluate(query, ancestorElement, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
93                 if (!xpathResult || !xpathResult.snapshotLength)
94                     return [];
95
96                 let arrayResult = [];
97                 for (let i = 0; i < xpathResult.snapshotLength; ++i)
98                     arrayResult.push(xpathResult.snapshotItem(i));
99                 return arrayResult;
100             }
101         } catch (error) {
102             if (error instanceof XPathException && error.code === XPathException.INVALID_EXPRESSION_ERR)
103                 return "InvalidXPathExpression";
104             // FIXME: Bad CSS can throw an error that we should report back to the endpoint. There is no
105             // special error code for that though, so we just return an empty match.
106             return firstResultOnly ? null : [];
107         }
108     }
109
110     const pollInterval = 50;
111     let pollUntil = performance.now() + timeoutDuration;
112
113     function pollForNode() {
114         let result = tryToFindNode();
115
116         // Report any valid results.
117         if (typeof result === "string" || result instanceof Node || (result instanceof Array && result.length)) {
118             callback(result);
119             return;
120         }
121
122         // Schedule another attempt if we have time remaining.
123         let durationRemaining = pollUntil - performance.now();
124         if (durationRemaining < pollInterval) {
125             callback(firstResultOnly ? null : []);
126             return;
127         }
128
129         setTimeout(pollForNode, pollInterval);
130     }
131
132     pollForNode();
133 }