capybara-chrome 0.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +5 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +110 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/capybara-chrome.gemspec +31 -0
- data/lib/capybara-chrome.rb +1 -0
- data/lib/capybara/chrome.rb +56 -0
- data/lib/capybara/chrome/browser.rb +393 -0
- data/lib/capybara/chrome/configuration.rb +77 -0
- data/lib/capybara/chrome/debug.rb +17 -0
- data/lib/capybara/chrome/driver.rb +38 -0
- data/lib/capybara/chrome/errors.rb +15 -0
- data/lib/capybara/chrome/node.rb +343 -0
- data/lib/capybara/chrome/rdp_client.rb +204 -0
- data/lib/capybara/chrome/rdp_socket.rb +29 -0
- data/lib/capybara/chrome/rdp_web_socket_client.rb +51 -0
- data/lib/capybara/chrome/repeat_timeout.rb +15 -0
- data/lib/capybara/chrome/service.rb +109 -0
- data/lib/capybara/chrome/version.rb +5 -0
- data/lib/chrome_remote_helper.js +340 -0
- metadata +154 -0
@@ -0,0 +1,204 @@
|
|
1
|
+
module Capybara::Chrome
|
2
|
+
|
3
|
+
# Chrome Remote Debugging Protocol (RDP) Client
|
4
|
+
class RDPClient
|
5
|
+
require "open-uri"
|
6
|
+
|
7
|
+
include Debug
|
8
|
+
|
9
|
+
attr_reader :response_events, :response_messages, :loader_ids, :listen_mutex, :handler_calls, :ws, :handlers, :browser
|
10
|
+
|
11
|
+
def initialize(chrome_host:, chrome_port:, browser:)
|
12
|
+
@chrome_host = chrome_host
|
13
|
+
@chrome_port = chrome_port
|
14
|
+
@browser = browser
|
15
|
+
@last_id = 0
|
16
|
+
@ws = nil
|
17
|
+
@ws_thread = nil
|
18
|
+
@ws_mutex = Mutex.new
|
19
|
+
@handlers = Hash.new { |hash, key| hash[key] = [] }
|
20
|
+
@listen_mutex = Mutex.new
|
21
|
+
@response_messages = {}
|
22
|
+
@response_events = []
|
23
|
+
@read_mutex = Mutex.new
|
24
|
+
@handler_mutex = Mutex.new
|
25
|
+
@loader_ids = []
|
26
|
+
@handler_calls = []
|
27
|
+
end
|
28
|
+
|
29
|
+
def reset
|
30
|
+
@calling_handlers = false
|
31
|
+
response_messages.clear
|
32
|
+
response_events.clear
|
33
|
+
loader_ids.clear
|
34
|
+
end
|
35
|
+
|
36
|
+
def generate_unique_id
|
37
|
+
@last_id += 1
|
38
|
+
end
|
39
|
+
|
40
|
+
def send_cmd!(command, params={})
|
41
|
+
debug command, params
|
42
|
+
msg_id = generate_unique_id
|
43
|
+
send_msg({method: command, params: params, id: msg_id}.to_json)
|
44
|
+
msg_id
|
45
|
+
end
|
46
|
+
|
47
|
+
# Errno::EPIPE
|
48
|
+
def send_cmd(command, params={})
|
49
|
+
msg_id = send_cmd!(command, params)
|
50
|
+
|
51
|
+
debug "waiting #{command} #{msg_id}"
|
52
|
+
msg = nil
|
53
|
+
begin
|
54
|
+
until msg = @response_messages[msg_id]
|
55
|
+
read_and_process(1)
|
56
|
+
end
|
57
|
+
@response_messages.delete msg_id
|
58
|
+
rescue Timeout::Error
|
59
|
+
puts "TimeoutError #{command} #{params.inspect} #{msg_id}"
|
60
|
+
send_cmd! "Runtime.terminateExecution"
|
61
|
+
puts "Recovering"
|
62
|
+
recover_chrome_crash
|
63
|
+
raise ResponseTimeoutError
|
64
|
+
rescue WebSocketError => e
|
65
|
+
puts "send_cmd received websocket error #{e.inspect}"
|
66
|
+
recover_chrome_crash
|
67
|
+
raise e
|
68
|
+
rescue Errno::EPIPE, EOFError => e
|
69
|
+
puts "send_cmd received EPIPE or EOF error #{e.inspect}"
|
70
|
+
recover_chrome_crash
|
71
|
+
raise e
|
72
|
+
rescue => e
|
73
|
+
puts "send_cmd caught error #{e.inspect} when issuing #{command} #{params.inspect}"
|
74
|
+
puts caller
|
75
|
+
raise e
|
76
|
+
end
|
77
|
+
return msg["result"]
|
78
|
+
end
|
79
|
+
|
80
|
+
def recover_chrome_crash
|
81
|
+
$stderr.puts "Chrome Crashed... #{Capybara::Chrome.wants_to_quit.inspect} #{::RSpec.wants_to_quit.inspect}" unless Capybara::Chrome.wants_to_quit
|
82
|
+
browser.restart_chrome
|
83
|
+
browser.start_remote
|
84
|
+
end
|
85
|
+
|
86
|
+
def send_msg(msg)
|
87
|
+
retries ||= 0
|
88
|
+
ws.send_msg(msg)
|
89
|
+
rescue Errno::EPIPE, EOFError => exception
|
90
|
+
retries += 1
|
91
|
+
recover_chrome_crash
|
92
|
+
if retries < 5 && !::Capybara::Chrome.wants_to_quit
|
93
|
+
retry
|
94
|
+
else
|
95
|
+
raise exception
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def on(event_name, &block)
|
100
|
+
handlers[event_name] << block
|
101
|
+
end
|
102
|
+
|
103
|
+
def wait_for(event_name, timeout=Capybara.default_max_wait_time)
|
104
|
+
@response_events.clear
|
105
|
+
msg = nil
|
106
|
+
loop do
|
107
|
+
msgs = @response_events.select {|v| v["method"] == event_name}
|
108
|
+
if msgs.any?
|
109
|
+
if block_given?
|
110
|
+
do_return = msgs.detect do |m|
|
111
|
+
val = yield m["params"]
|
112
|
+
end
|
113
|
+
if do_return
|
114
|
+
msg = do_return.dup
|
115
|
+
@response_events.delete do_return
|
116
|
+
break
|
117
|
+
else
|
118
|
+
read_and_process(1)
|
119
|
+
next
|
120
|
+
end
|
121
|
+
else
|
122
|
+
msg = msgs.first.dup
|
123
|
+
msgs.each {|m| @response_events.delete m}
|
124
|
+
break
|
125
|
+
end
|
126
|
+
else
|
127
|
+
end
|
128
|
+
read_and_process(1)
|
129
|
+
end
|
130
|
+
return msg && msg["params"]
|
131
|
+
rescue Timeout::Error
|
132
|
+
nil
|
133
|
+
end
|
134
|
+
|
135
|
+
def process_messages
|
136
|
+
n = 0
|
137
|
+
while @ws.messages.any? do
|
138
|
+
n += 1
|
139
|
+
msg_raw = @ws.messages.shift
|
140
|
+
if msg_raw
|
141
|
+
msg = JSON.parse(msg_raw)
|
142
|
+
if msg["method"]
|
143
|
+
hs = handlers[msg["method"]]
|
144
|
+
if hs.any?
|
145
|
+
@handler_calls << [msg["method"], msg["params"]]
|
146
|
+
end
|
147
|
+
@response_events << msg
|
148
|
+
else
|
149
|
+
@response_messages[msg["id"]] = msg
|
150
|
+
if msg["exceptionDetails"]
|
151
|
+
puts JSException.new(val["exceptionDetails"]["exception"].inspect)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
else
|
155
|
+
p ["no msg_raw", msg_raw]
|
156
|
+
end
|
157
|
+
end
|
158
|
+
n
|
159
|
+
end
|
160
|
+
|
161
|
+
def read_and_process(timeout=0)
|
162
|
+
return unless Thread.current == Thread.main
|
163
|
+
ready = select [@ws.socket.io], [], [], timeout
|
164
|
+
if ready
|
165
|
+
@ws.parse_input
|
166
|
+
process_messages
|
167
|
+
end
|
168
|
+
if !@calling_handlers
|
169
|
+
@calling_handlers = true
|
170
|
+
while obj = @handler_calls.shift do
|
171
|
+
handlers[obj[0]].each {|h| h.call obj[1]}
|
172
|
+
end
|
173
|
+
@calling_handlers = false
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def discover_ws_url
|
178
|
+
response = open("http://#{@chrome_host}:#{@chrome_port}/json")
|
179
|
+
data = JSON.parse(response.read)
|
180
|
+
first_page = data.detect {|e| e["type"] == "page"}
|
181
|
+
@ws_url = first_page["webSocketDebuggerUrl"]
|
182
|
+
end
|
183
|
+
|
184
|
+
def start
|
185
|
+
browser.wait_for_chrome
|
186
|
+
browser.with_retry do
|
187
|
+
discover_ws_url
|
188
|
+
end
|
189
|
+
@ws = RDPWebSocketClient.new @ws_url
|
190
|
+
send_cmd! "Network.enable"
|
191
|
+
send_cmd! "Network.clearBrowserCookies"
|
192
|
+
send_cmd! "Page.enable"
|
193
|
+
send_cmd! "DOM.enable"
|
194
|
+
send_cmd! "CSS.enable"
|
195
|
+
send_cmd! "Page.setDownloadBehavior", behavior: "allow", downloadPath: Capybara::Chrome.configuration.download_path
|
196
|
+
helper_js = File.expand_path(File.join("..", "..", "chrome_remote_helper.js"), File.dirname(__FILE__))
|
197
|
+
send_cmd! "Page.addScriptToEvaluateOnNewDocument", source: File.read(helper_js)
|
198
|
+
|
199
|
+
Thread.abort_on_exception = true
|
200
|
+
return
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Capybara::Chrome
|
2
|
+
class RDPSocket
|
3
|
+
READ_LEN = 4096
|
4
|
+
attr_reader :url, :io
|
5
|
+
|
6
|
+
def initialize(url)
|
7
|
+
uri = URI.parse(url)
|
8
|
+
@url = uri.to_s
|
9
|
+
@io = TCPSocket.new(uri.host, uri.port)
|
10
|
+
end
|
11
|
+
|
12
|
+
def write(data)
|
13
|
+
io.sendmsg data
|
14
|
+
end
|
15
|
+
|
16
|
+
def read
|
17
|
+
buf = ""
|
18
|
+
loop do
|
19
|
+
buf << io.recv_nonblock(READ_LEN)
|
20
|
+
end
|
21
|
+
rescue IO::EAGAINWaitReadable
|
22
|
+
if buf.size == 0
|
23
|
+
puts "buf is #{buf.size}"
|
24
|
+
puts caller[0..10]
|
25
|
+
end
|
26
|
+
buf
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Capybara::Chrome
|
2
|
+
class RDPWebSocketClient
|
3
|
+
attr_reader :socket, :driver, :messages, :status
|
4
|
+
|
5
|
+
def initialize(url)
|
6
|
+
@socket = RDPSocket.new(url)
|
7
|
+
@driver = ::WebSocket::Driver.client(socket)
|
8
|
+
|
9
|
+
@messages = []
|
10
|
+
@status = :closed
|
11
|
+
|
12
|
+
setup_driver
|
13
|
+
start_driver
|
14
|
+
end
|
15
|
+
|
16
|
+
def send_msg(msg)
|
17
|
+
driver.text msg
|
18
|
+
end
|
19
|
+
|
20
|
+
def parse_input
|
21
|
+
@driver.parse(@socket.read)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def setup_driver
|
27
|
+
driver.on(:message) do |e|
|
28
|
+
messages << e.data
|
29
|
+
end
|
30
|
+
|
31
|
+
driver.on(:error) do |e|
|
32
|
+
raise WebSocketError.new e.message
|
33
|
+
end
|
34
|
+
|
35
|
+
driver.on(:close) do |e|
|
36
|
+
raise "closed"
|
37
|
+
@status = :closed
|
38
|
+
end
|
39
|
+
|
40
|
+
driver.on(:open) do |e|
|
41
|
+
@status = :open
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def start_driver
|
46
|
+
driver.start
|
47
|
+
select [socket.io]
|
48
|
+
parse_input until status == :open
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module Capybara::Chrome
|
2
|
+
module Service
|
3
|
+
|
4
|
+
# "--disable-gpu",
|
5
|
+
# '--js-flags="--max-old-space-size=500"',
|
6
|
+
CHROME_ARGS = [
|
7
|
+
"--headless",
|
8
|
+
"--enable-automation",
|
9
|
+
"--crash-dumps-dir=/tmp",
|
10
|
+
"--disable-background-networking",
|
11
|
+
"--disable-background-timer-throttling",
|
12
|
+
"--disable-breakpad",
|
13
|
+
"--disable-client-side-phishing-detection",
|
14
|
+
"--disable-default-apps",
|
15
|
+
"--disable-dev-shm-usage",
|
16
|
+
"--disable-extensions",
|
17
|
+
"--disable-features=site-per-process",
|
18
|
+
"--disable-hang-monitor",
|
19
|
+
"--disable-popup-blocking",
|
20
|
+
"--disable-prompt-on-repost",
|
21
|
+
"--disable-sync",
|
22
|
+
"--disable-translate",
|
23
|
+
"--metrics-recording-only",
|
24
|
+
"--no-first-run",
|
25
|
+
"--no-pings",
|
26
|
+
"--safebrowsing-disable-auto-update",
|
27
|
+
"--hide-scrollbars",
|
28
|
+
"--mute-audio",
|
29
|
+
]
|
30
|
+
|
31
|
+
def chrome_pid
|
32
|
+
@chrome_pid
|
33
|
+
end
|
34
|
+
|
35
|
+
def start_chrome
|
36
|
+
return if chrome_running?
|
37
|
+
debug "Starting Chrome", chrome_path, chrome_args
|
38
|
+
@chrome_pid = Process.spawn chrome_path, *chrome_args, :out=>"/dev/null"
|
39
|
+
at_exit { stop_chrome }
|
40
|
+
end
|
41
|
+
|
42
|
+
def stop_chrome
|
43
|
+
Process.kill "TERM", chrome_pid rescue nil
|
44
|
+
end
|
45
|
+
|
46
|
+
def restart_chrome
|
47
|
+
stop_chrome
|
48
|
+
if chrome_running?
|
49
|
+
@chrome_port = find_available_port(@chrome_host)
|
50
|
+
end
|
51
|
+
start_chrome
|
52
|
+
end
|
53
|
+
|
54
|
+
def wait_for_chrome
|
55
|
+
running = false
|
56
|
+
while !running
|
57
|
+
running = chrome_running?
|
58
|
+
sleep 0.02
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def chrome_running?
|
63
|
+
socket = TCPSocket.new(@chrome_host, @chrome_port) rescue false
|
64
|
+
socket.close if socket
|
65
|
+
!!socket
|
66
|
+
end
|
67
|
+
|
68
|
+
def chrome_path
|
69
|
+
case os
|
70
|
+
when :macosx
|
71
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
72
|
+
when :linux
|
73
|
+
# /opt/google/chrome/chrome
|
74
|
+
"google-chrome-stable"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def chrome_args
|
79
|
+
CHROME_ARGS + ["--remote-debugging-port=#{@chrome_port}"]
|
80
|
+
end
|
81
|
+
|
82
|
+
def os
|
83
|
+
@os ||= (
|
84
|
+
host_os = RbConfig::CONFIG['host_os']
|
85
|
+
case host_os
|
86
|
+
when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
|
87
|
+
:windows
|
88
|
+
when /darwin|mac os/
|
89
|
+
:macosx
|
90
|
+
when /linux/
|
91
|
+
:linux
|
92
|
+
when /solaris|bsd/
|
93
|
+
:unix
|
94
|
+
else
|
95
|
+
raise Error::WebDriverError, "unknown os: #{host_os.inspect}"
|
96
|
+
end
|
97
|
+
)
|
98
|
+
end
|
99
|
+
|
100
|
+
def find_available_port(host)
|
101
|
+
sleep rand * 0.7 # slight delay to account for concurrent browsers
|
102
|
+
server = TCPServer.new(host, 0)
|
103
|
+
server.addr[1]
|
104
|
+
ensure
|
105
|
+
server.close if server
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,340 @@
|
|
1
|
+
window.ChromeRemotePageLoaded = document.readyState == "complete"
|
2
|
+
window.ChromeRemoteHelper = {
|
3
|
+
DOMContentLoaded: document.readyState == "interactive" || document.readyState == "complete",
|
4
|
+
windowLoaded: document.readyState == "complete",
|
5
|
+
windowUnloading: false,
|
6
|
+
nextIndex: 0,
|
7
|
+
nodes: {},
|
8
|
+
nodeClicks: {},
|
9
|
+
TEXT_TYPES: ["date", "email", "number", "password", "search", "tel", "text", "textarea", "url"],
|
10
|
+
windowWaitPromise: null,
|
11
|
+
|
12
|
+
registerNode: function(node) {
|
13
|
+
this.nextIndex++;
|
14
|
+
this.nodes[this.nextIndex] = node;
|
15
|
+
return this.nextIndex;
|
16
|
+
},
|
17
|
+
|
18
|
+
waitPromise: function(truthFn, delay) {
|
19
|
+
var ms = 0;
|
20
|
+
var intId;
|
21
|
+
var p = new Promise(function(resolve, reject) {
|
22
|
+
var truthy = truthFn();
|
23
|
+
if (truthy) {
|
24
|
+
resolve(truthy);
|
25
|
+
} else if (this.windowUnloading) {
|
26
|
+
if (intId) {
|
27
|
+
window.clearInterval(intId);
|
28
|
+
}
|
29
|
+
resolve(false);
|
30
|
+
} else {
|
31
|
+
intId = window.setInterval(function() {
|
32
|
+
truthy = truthFn(intId);
|
33
|
+
ms += delay;
|
34
|
+
if (truthy || ms > 500) {
|
35
|
+
window.clearInterval(intId);
|
36
|
+
resolve(truthy);
|
37
|
+
} else if (this.windowUnloading) {
|
38
|
+
resolve(false);
|
39
|
+
} else {
|
40
|
+
console.log("not truthy", truthy, this);
|
41
|
+
}
|
42
|
+
}.bind(this), delay);
|
43
|
+
}
|
44
|
+
}.bind(this));
|
45
|
+
return p;
|
46
|
+
},
|
47
|
+
|
48
|
+
waitUnload: function() {
|
49
|
+
var ms = 0;
|
50
|
+
var delay = 1;
|
51
|
+
var intId;
|
52
|
+
var p = new Promise(function(resolve, reject) {
|
53
|
+
if (this.windowUnloading) {
|
54
|
+
resolve(true);
|
55
|
+
}
|
56
|
+
var fn = function() {
|
57
|
+
ms += delay;
|
58
|
+
if (ms >= 100) {
|
59
|
+
//clearInterval(intId);
|
60
|
+
return true;
|
61
|
+
} else {
|
62
|
+
if (this.windowUnloading) {
|
63
|
+
// clearInterval(intId);
|
64
|
+
return true;
|
65
|
+
} else {
|
66
|
+
return false;
|
67
|
+
}
|
68
|
+
}
|
69
|
+
}.bind(this);
|
70
|
+
var redo = function() {
|
71
|
+
if (fn()) {
|
72
|
+
return resolve(true);
|
73
|
+
} else {
|
74
|
+
window.setTimeout(redo, delay);
|
75
|
+
}
|
76
|
+
}.bind(this);
|
77
|
+
redo();
|
78
|
+
|
79
|
+
// intId = window.setInterval(, delay);
|
80
|
+
}.bind(this));
|
81
|
+
return p;
|
82
|
+
},
|
83
|
+
|
84
|
+
waitWindowLoaded: function() {
|
85
|
+
var ms = 0;
|
86
|
+
var delay = 0;
|
87
|
+
this.windowWaitPromise = this.waitPromise(function() {
|
88
|
+
return this.windowLoaded;
|
89
|
+
}.bind(this), delay);
|
90
|
+
return this.windowWaitPromise;
|
91
|
+
},
|
92
|
+
|
93
|
+
waitDOMContentLoaded: function() {
|
94
|
+
return this.waitPromise(function() {
|
95
|
+
return this.DOMContentLoaded;
|
96
|
+
}.bind(this), 5);
|
97
|
+
},
|
98
|
+
|
99
|
+
findCss: function(query) {
|
100
|
+
return this.findCssRelativeTo(document, query);
|
101
|
+
},
|
102
|
+
|
103
|
+
findCssWithin: function (index, query) {
|
104
|
+
return this.findCssRelativeTo(this.getNode(index), query);
|
105
|
+
},
|
106
|
+
|
107
|
+
findCssRelativeTo: function(reference, query) {
|
108
|
+
return this.waitDOMContentLoaded().then(function() {
|
109
|
+
var results = [];
|
110
|
+
var list = reference.querySelectorAll(query);
|
111
|
+
for (i = 0; i < list.length; i++) {
|
112
|
+
results.push(this.registerNode(list[i]));
|
113
|
+
}
|
114
|
+
return results.join(",");
|
115
|
+
}.bind(this));
|
116
|
+
},
|
117
|
+
|
118
|
+
findXPath: function(query) {
|
119
|
+
return this.findXpathRelativeTo(document, query);
|
120
|
+
},
|
121
|
+
|
122
|
+
findXPathWithin: function(index, query) {
|
123
|
+
return this.findXpathRelativeTo(this.getNode(index), query);
|
124
|
+
},
|
125
|
+
|
126
|
+
findXpathRelativeTo: function(reference, query) {
|
127
|
+
return this.waitDOMContentLoaded().then(function() {
|
128
|
+
var iterator = document.evaluate(query, reference, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
|
129
|
+
var node;
|
130
|
+
var results = [];
|
131
|
+
while (node = iterator.iterateNext()) {
|
132
|
+
results.push(this.registerNode(node));
|
133
|
+
}
|
134
|
+
return results.join(",");
|
135
|
+
}.bind(this));
|
136
|
+
},
|
137
|
+
|
138
|
+
getXPathNode: function(node, path) {
|
139
|
+
path = path || [];
|
140
|
+
if (node.parentNode) {
|
141
|
+
path = this.getXPathNode(node.parentNode, path);
|
142
|
+
}
|
143
|
+
|
144
|
+
var first = node;
|
145
|
+
while (first.previousSibling)
|
146
|
+
first = first.previousSibling;
|
147
|
+
|
148
|
+
var count = 0;
|
149
|
+
var index = 0;
|
150
|
+
var iter = first;
|
151
|
+
while (iter) {
|
152
|
+
if (iter.nodeType == 1 && iter.nodeName == node.nodeName)
|
153
|
+
count++;
|
154
|
+
if (iter.isSameNode(node))
|
155
|
+
index = count;
|
156
|
+
iter = iter.nextSibling;
|
157
|
+
continue;
|
158
|
+
}
|
159
|
+
|
160
|
+
if (node.nodeType == 1)
|
161
|
+
path.push(node.nodeName.toLowerCase() + (node.id ? "[@id='"+node.id+"']" : count > 1 ? "["+index+"]" : ''));
|
162
|
+
|
163
|
+
return path;
|
164
|
+
},
|
165
|
+
|
166
|
+
nodePathForNode: function(index) {
|
167
|
+
return this.pathForNode(this.getNode(index));
|
168
|
+
},
|
169
|
+
|
170
|
+
pathForNode: function(node) {
|
171
|
+
return "/" + this.getXPathNode(node).join("/");
|
172
|
+
},
|
173
|
+
|
174
|
+
getNode: function(index) {
|
175
|
+
var node = this.nodes[index];
|
176
|
+
if (!node) {
|
177
|
+
throw new NodeNotFoundError("No node found with id:"+index+". Registered nodes:"+Object.keys(this.nodes).length);
|
178
|
+
}
|
179
|
+
return node;
|
180
|
+
},
|
181
|
+
|
182
|
+
onSelf: function(index, script) {
|
183
|
+
var node = this.getNode(index);
|
184
|
+
// console.log("onSelf " + index + " " + node.tagName + " " + (node.parentElement && node.parentElement.tagName) + " " + script);
|
185
|
+
var fn = Function("'use strict';"+script).bind(node)
|
186
|
+
var pp = new Promise(function(resolve) { resolve(fn()); });
|
187
|
+
return pp;
|
188
|
+
},
|
189
|
+
|
190
|
+
// args should be an array
|
191
|
+
onSelfValue: function(index, meth, args) {
|
192
|
+
var node = this.getNode(index);
|
193
|
+
var val = node[meth];
|
194
|
+
if (typeof val === "function") {
|
195
|
+
val.apply(node, args);
|
196
|
+
} else {
|
197
|
+
return val;
|
198
|
+
}
|
199
|
+
},
|
200
|
+
|
201
|
+
dispatchEvent: function(node, eventName) {
|
202
|
+
var eventObject;
|
203
|
+
if (eventName == "click") {
|
204
|
+
eventObject = new MouseEvent("click", {bubbles: true, cancelable: true});
|
205
|
+
} else {
|
206
|
+
eventObject = document.createEvent("HTMLEvents");
|
207
|
+
eventObject.initEvent(eventName, true, true);
|
208
|
+
}
|
209
|
+
return node.dispatchEvent(eventObject);
|
210
|
+
},
|
211
|
+
|
212
|
+
nodeSetType: function(index) {
|
213
|
+
var node = this.getNode(index);
|
214
|
+
return (node.type || node.tagName).toLowerCase();
|
215
|
+
},
|
216
|
+
|
217
|
+
nodeSet: function(index, value, type) {
|
218
|
+
var node = this.getNode(index);
|
219
|
+
if (type == "checkbox" || type == "radio") {
|
220
|
+
if (value == "true" && !node.checked) {
|
221
|
+
return node.click();
|
222
|
+
} else if (value == "false" && node.checked) {
|
223
|
+
return node.click();
|
224
|
+
}
|
225
|
+
} else {
|
226
|
+
return node.value = value;
|
227
|
+
}
|
228
|
+
},
|
229
|
+
|
230
|
+
nodeVisible: function(index) {
|
231
|
+
return this.visible(this.getNode(index));
|
232
|
+
},
|
233
|
+
|
234
|
+
visible: function(node) {
|
235
|
+
var styles = node.ownerDocument.defaultView.getComputedStyle(node);
|
236
|
+
if (styles["visibility"] == "hidden" || styles["display"] == "none" || styles["opacity"] == 0) {
|
237
|
+
return false;
|
238
|
+
}
|
239
|
+
while (node = node.parentElement) {
|
240
|
+
styles = node.ownerDocument.defaultView.getComputedStyle(node);
|
241
|
+
if (styles["display"] == "none" || styles["opacity"] == 0) {
|
242
|
+
return false;
|
243
|
+
}
|
244
|
+
}
|
245
|
+
return true;
|
246
|
+
},
|
247
|
+
|
248
|
+
nodeText: function(index) {
|
249
|
+
return this.text(this.getNode(index));
|
250
|
+
},
|
251
|
+
|
252
|
+
text: function(node) {
|
253
|
+
var type = node instanceof HTMLFormElement ? 'form' : (node.type || node.tagName).toLowerCase();
|
254
|
+
if (type == "textarea") {
|
255
|
+
return node.innerHTML;
|
256
|
+
} else {
|
257
|
+
var visible_text = node.innerText;
|
258
|
+
return typeof visible_text === "string" ? visible_text : node.textContent;
|
259
|
+
}
|
260
|
+
},
|
261
|
+
|
262
|
+
nodeIsNodeAtPosition: function(index, pos) {
|
263
|
+
return this.isNodeAtPosition(this.getNode(index), pos);
|
264
|
+
},
|
265
|
+
|
266
|
+
isNodeAtPosition: function(node, pos) {
|
267
|
+
var nodeAtPosition =
|
268
|
+
document.elementFromPoint(pos.relativeX, pos.relativeY);
|
269
|
+
var overlappingPath;
|
270
|
+
|
271
|
+
|
272
|
+
if (nodeAtPosition) {
|
273
|
+
// console.log("is node at position" + nodeAtPosition.tagName)
|
274
|
+
overlappingPath = this.pathForNode(nodeAtPosition)
|
275
|
+
}
|
276
|
+
|
277
|
+
if (!this.isNodeOrChildAtPosition(node, pos, nodeAtPosition)) {
|
278
|
+
// console.log("Would throw " + overlappingPath + " " + this.pathForNode(node))
|
279
|
+
return false;
|
280
|
+
}
|
281
|
+
|
282
|
+
return true;
|
283
|
+
},
|
284
|
+
|
285
|
+
isNodeOrChildAtPosition: function(expectedNode, pos, currentNode) {
|
286
|
+
if (currentNode == expectedNode) {
|
287
|
+
return true;
|
288
|
+
} else if (currentNode) {
|
289
|
+
return this.isNodeOrChildAtPosition(
|
290
|
+
expectedNode,
|
291
|
+
pos,
|
292
|
+
currentNode.parentNode
|
293
|
+
);
|
294
|
+
} else {
|
295
|
+
return false;
|
296
|
+
}
|
297
|
+
},
|
298
|
+
|
299
|
+
nodeGetDimensions: function(index) {
|
300
|
+
return this.getDimensions(this.getNode(index));
|
301
|
+
},
|
302
|
+
|
303
|
+
getDimensions: function(node) {
|
304
|
+
return JSON.stringify(node.getBoundingClientRect());
|
305
|
+
},
|
306
|
+
|
307
|
+
// don't attach state to the node because the node can go away after click
|
308
|
+
attachClickListener: function(index) {
|
309
|
+
var node = this.getNode(index);
|
310
|
+
this.nodeClicks[index] = false;
|
311
|
+
var fn = function() {
|
312
|
+
this.nodeClicks[index] = true;
|
313
|
+
node.removeEventListener("click", fn);
|
314
|
+
}.bind(this);
|
315
|
+
node.addEventListener("click", fn);
|
316
|
+
},
|
317
|
+
|
318
|
+
nodeVerifyClicked: function(index) {
|
319
|
+
return this.nodeClicks[index];
|
320
|
+
}
|
321
|
+
}
|
322
|
+
|
323
|
+
document.addEventListener("DOMContentLoaded", function() {
|
324
|
+
ChromeRemoteHelper.DOMContentLoaded = true;
|
325
|
+
});
|
326
|
+
|
327
|
+
window.addEventListener("load", function() {
|
328
|
+
ChromeRemoteHelper.windowLoaded = true;
|
329
|
+
ChromeRemotePageLoaded = true;
|
330
|
+
});
|
331
|
+
|
332
|
+
window.addEventListener("beforeunload", function() {
|
333
|
+
ChromeRemoteHelper.windowUnloading = true;
|
334
|
+
ChromeRemoteHelper.windowLoaded = true;
|
335
|
+
});
|
336
|
+
|
337
|
+
window.addEventListener("unload", function() {
|
338
|
+
});
|
339
|
+
|
340
|
+
class NodeNotFoundError extends Error{}
|