capybara-chrome 0.1.22
Sign up to get free protection for your applications and to get access to all the features.
- 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{}
|