capybara-chrome 0.1.22

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,77 @@
1
+ module Capybara::Chrome
2
+ class Configuration
3
+
4
+ DEFAULT_ALLOWED_URLS = [
5
+ %r(.*127\.0\.0\.1),
6
+ %r(.*localhost),
7
+ "data:*,*"
8
+ ].freeze
9
+ DEFAULT_MAX_WAIT_TIME = 10
10
+ DEFAULT_DOWNLOAD_PATH = "/tmp"
11
+ # set Capybara::Chrome::Configuration.chrome_port = 9222 for easy debugging
12
+ DEFAULT_CHROME_PORT = nil
13
+ DEFAULT_TRAP_INTERRUPT = true
14
+ DEFAULT_DEBUG = false
15
+
16
+ attr_accessor :max_wait_time, :download_path, :chrome_port, :trap_interrupt, :debug
17
+
18
+ def initialize
19
+ @allowed_urls = DEFAULT_ALLOWED_URLS.dup
20
+ @blocked_urls = []
21
+ @max_wait_time = DEFAULT_MAX_WAIT_TIME
22
+ @download_path = DEFAULT_DOWNLOAD_PATH
23
+ @chrome_port = DEFAULT_CHROME_PORT
24
+ @trap_interrupt = DEFAULT_TRAP_INTERRUPT
25
+ @debug = DEFAULT_DEBUG
26
+ end
27
+
28
+ def block_unknown_urls
29
+ @block_unknown_urls = true
30
+ end
31
+
32
+ def allow_unknown_urls
33
+ allow_url(/.*/)
34
+ end
35
+
36
+ def re_url(url)
37
+ url.is_a?(Regexp) ? url : Regexp.new(Regexp.escape(url))
38
+ end
39
+
40
+ def allow_url(url)
41
+ @allowed_urls << re_url(url)
42
+ end
43
+
44
+ def block_url(url)
45
+ @blocked_urls << re_url(url)
46
+ end
47
+
48
+ def url_match?(pattern, url)
49
+ pattern === url
50
+ end
51
+
52
+ def url_allowed?(url)
53
+ @allowed_urls.detect {|pattern| url_match?(pattern, url)}
54
+ end
55
+
56
+ def block_url?(url)
57
+ if url_allowed?(url)
58
+ false
59
+ else
60
+ @block_unknown_urls || @blocked_urls.detect {|pattern| url_match?(pattern, url)}
61
+ end
62
+ end
63
+
64
+ def skip_image_loading
65
+ @skip_image_loading = true
66
+ end
67
+
68
+ def skip_image_loading?
69
+ @skip_image_loading
70
+ end
71
+
72
+ def trap_interrupt?
73
+ @trap_interrupt
74
+ end
75
+
76
+ end
77
+ end
@@ -0,0 +1,17 @@
1
+ module Capybara
2
+ module Chrome
3
+ module Debug
4
+ def debug(*args)
5
+ if Capybara::Chrome.configuration.debug
6
+ p [caller_locations(1,1)[0].label, *args, Time.now.to_i]
7
+ end
8
+ return args[0]
9
+ end
10
+
11
+ def info(*args)
12
+ p [caller_locations(1,1)[0].label, *args, Time.now.to_i]
13
+ return args[0]
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,38 @@
1
+ module Capybara::Chrome
2
+ class Driver < Capybara::Driver::Base
3
+ extend Forwardable
4
+
5
+ def initialize(app, options={})
6
+ @app = app
7
+ @options = options
8
+ @session = nil
9
+ end
10
+
11
+ def_delegators :browser, :visit, :find_css, :html, :evaluate_script, :evaluate_async_script, :execute_script, :status_code, :save_screenshot, :render, :current_url, :header, :console_messages, :error_messages, :dismiss_modal, :accept_modal, :title, :unrecognized_scheme_requests, :wait_for_load
12
+
13
+ def browser
14
+ @browser ||= Browser.new(self, port: @options[:port])
15
+ end
16
+
17
+ def find_xpath(query)
18
+ nodes = browser.find_xpath(query)
19
+ end
20
+
21
+ def start
22
+ browser.start
23
+ end
24
+
25
+ def needs_server?
26
+ true
27
+ end
28
+
29
+ def wait?
30
+ true
31
+ end
32
+
33
+ def reset!
34
+ browser.reset
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,15 @@
1
+ module Capybara::Chrome
2
+
3
+ class Error < StandardError
4
+ end
5
+
6
+ class JSException < Error
7
+ end
8
+
9
+ class ResponseTimeoutError < Error
10
+ end
11
+
12
+ class WebSocketError < Error
13
+ end
14
+
15
+ end
@@ -0,0 +1,343 @@
1
+ module Capybara::Chrome
2
+ class Node < ::Capybara::Driver::Node
3
+ include Debug
4
+ attr_reader :browser, :id
5
+
6
+ KEY_DATA = Hash.new do |h, k|
7
+ h[k] = {text: k.to_s}
8
+ end.merge!({
9
+ cancel: { key_code: 3, code: "Abort", key: "Cancel"},
10
+ help: { key_code: 6, code: "Help", key: "Help"},
11
+ backspace: { key_code: 8, code: "Backspace", key: "Backspace"},
12
+ tab: { key_code: 9, code: "Tab", key: "Tab"},
13
+ delete: { key_code: 46, code: "Delete", key: "Delete"},
14
+ home: { key_code: 36, code: "Home", key: "Home"},
15
+ end: { key_code: 35, code: "End", key: "End"},
16
+ left: { key_code: 37, code: "ArrowLeft", key: "ArrowLeft"},
17
+ right: { key_code: 39, code: "ArrowRight", key: "ArrowRight"},
18
+ up: { key_code: 38, code: "ArrowUp", key: "ArrowUp"},
19
+ down: { key_code: 40, code: "ArrowDown", key: "ArrowDown"},
20
+ return: { key_code: 13, code: "Enter", key: "Enter"},
21
+ enter: { key_code: 13, code: "Enter", key: "Enter"},
22
+ "\r" => { key_code: 13, code: "Enter", key: "Enter", text: "\r"},
23
+ "\n" => { key_code: 13, code: "Enter", key: "Enter", text: "\r"},
24
+ space: { key_code: 32, code: "Space", key: " ", text: " "},
25
+ shift: { key_code: 16, code: "Shift", key: "ShiftLeft", location: 1},
26
+ control: { key_code: 17, code: "ControlLeft", key: "Control", text: "\r"},
27
+ alt: { key_code: 18, code: "AltLeft", key: "Alt", location: 1},
28
+ meta: { key_code: 91, code: "MetaLeft", key: "Meta", location: 1},
29
+ })
30
+
31
+ def initialize(driver, browser, id)
32
+ raise "hell" if id == 0
33
+ @driver = driver
34
+ @browser = browser
35
+ @id = id
36
+ @mouse_x = 0
37
+ @mouse_y = 0
38
+ end
39
+
40
+ def html
41
+ browser.evaluate_script %( ChromeRemoteHelper.waitDOMContentLoaded(); )
42
+ browser.evaluate_script %( ChromeRemoteHelper.onSelfValue(#{id}, "outerHTML") )
43
+ end
44
+
45
+ def all_text
46
+ browser.evaluate_script %( ChromeRemoteHelper.waitDOMContentLoaded(); )
47
+ text = browser.evaluate_script %( ChromeRemoteHelper.onSelfValue(#{id}, "textContent") )
48
+ if Capybara::VERSION.to_f < 3.0
49
+ Capybara::Helpers.normalize_whitespace(text)
50
+ else
51
+ text.gsub(/[\u200b\u200e\u200f]/, '')
52
+ .gsub(/[\ \n\f\t\v\u2028\u2029]+/, ' ')
53
+ .gsub(/\A[[:space:]&&[^\u00a0]]+/, "")
54
+ .gsub(/[[:space:]&&[^\u00a0]]+\z/, "")
55
+ .tr("\u00a0", ' ')
56
+ end
57
+ end
58
+
59
+ def visible_text
60
+ raw_text.to_s.gsub(/\ +/, ' ')
61
+ .gsub(/[\ \n]*\n[\ \n]*/, "\n")
62
+ .gsub(/\A[[:space:]&&[^\u00a0]]+/, "")
63
+ .gsub(/[[:space:]&&[^\u00a0]]+\z/, "")
64
+ .tr("\u00a0", ' ')
65
+ end
66
+ alias text visible_text
67
+
68
+ def raw_text
69
+ browser.evaluate_script %( ChromeRemoteHelper.nodeText(#{id}) )
70
+ end
71
+
72
+ def visible?
73
+ browser.evaluate_script %( ChromeRemoteHelper.nodeVisible(#{id}) )
74
+ end
75
+
76
+ def is_connected?
77
+ # on_self_value %( return this.isConnected )
78
+ val = browser.evaluate_script %( ChromeRemoteHelper.onSelfValue(#{id}, "isConnected") )
79
+ end
80
+
81
+ def get_dimensions
82
+ browser.wait_for_load
83
+ val = browser.evaluate_script %( ChromeRemoteHelper.nodeGetDimensions(#{id}) )
84
+ val = JSON.parse(val) rescue {}
85
+ val
86
+ end
87
+
88
+ def expect_node_at_position(x,y)
89
+ in_position = browser.evaluate_script %( ChromeRemoteHelper.nodeIsNodeAtPosition(#{id}, {relativeX: #{x}, relativeY: #{y}}) )
90
+ if !in_position
91
+ raise "Element '<#{tag_name}>' not at expected position: #{x},#{y}"
92
+ end
93
+ end
94
+
95
+ def move_mouse(x, y, steps: 1)
96
+ if steps >= 1
97
+ (0..x).step(steps).each do |dx|
98
+ send_cmd! "Input.dispatchMouseEvent", type: "mouseMoved", x: dx, y: 0
99
+ end
100
+ (0..y).step(steps).each do |dy|
101
+ send_cmd! "Input.dispatchMouseEvent", type: "mouseMoved", x: x, y: dy
102
+ end
103
+ end
104
+ send_cmd! "Input.dispatchMouseEvent", type: "mouseMoved", x: x, y: y
105
+ end
106
+
107
+ def click
108
+ browser.with_retry do
109
+ on_self("this.scrollIntoViewIfNeeded();");
110
+ dim = get_dimensions
111
+ if dim["width"] == 0 && dim["height"] == 0
112
+ puts "DIM IS 0"
113
+ puts html
114
+ raise Capybara::ElementNotFound
115
+ end
116
+ xd = [1, dim["width"]/2, dim["width"]/3]
117
+ yd = [1, dim["height"]/2, dim["height"]/3]
118
+ strategy = rand(0..xd.size-1)
119
+ cx = (dim["x"] + xd[strategy]).floor
120
+ cy = (dim["y"] + yd[strategy]).floor
121
+ move_mouse(cx, cy, steps: 0)
122
+ expect_node_at_position(cx, cy)
123
+ send_cmd! "Input.dispatchMouseEvent", type: "mousePressed", x: cx, y: cy, clickCount: 1, button: "left"
124
+ send_cmd! "Input.dispatchMouseEvent", type: "mouseReleased", x: cx, y: cy, clickCount: 1, button: "left"
125
+ vv = browser.wait_for_load
126
+ end
127
+ end
128
+
129
+ def find_css(query)
130
+ browser.query_selector_all(query, id)
131
+ end
132
+
133
+ def find_xpath(query)
134
+ browser.find_xpath query, id
135
+ end
136
+
137
+ def path
138
+ browser.evaluate_script %( ChromeRemoteHelper.nodePathForNode(#{id}) )
139
+ end
140
+ alias get_xpath path
141
+
142
+ def disabled?
143
+ val = browser.evaluate_script %( ChromeRemoteHelper.onSelfValue(#{id}, "disabled") )
144
+ debug val
145
+ val
146
+ end
147
+
148
+ TEXT_TYPES = %w(date email number password search tel text textarea url)
149
+ def set(value, options={})
150
+ value = value.to_s.gsub('"', '\"')
151
+ type = browser.evaluate_script %( ChromeRemoteHelper.nodeSetType(#{id}) )
152
+
153
+ if type == "file"
154
+ send_cmd "DOM.setFileInputFiles", files: [value.to_s], nodeId: node_id
155
+ elsif TEXT_TYPES.include?(type)
156
+ script = "this.value = ''; this.focus();"
157
+ if value.blank?
158
+ script << %(ChromeRemoteHelper.dispatchEvent(this, "change"))
159
+ end
160
+ on_self script
161
+ type_string(value.to_s)
162
+ else
163
+ browser.evaluate_script %( ChromeRemoteHelper.nodeSet(#{id}, "#{value}", "#{type}") )
164
+ end
165
+ end
166
+
167
+ def node_id
168
+ browser.get_document
169
+ val = browser.execute_script %(ChromeRemoteHelper.onSelf(#{id}, "return this;"))
170
+ send_cmd("DOM.requestNode", objectId: val["result"]["objectId"])["nodeId"]
171
+ end
172
+
173
+ def type_string(string)
174
+ ary = string.chars
175
+ ary.each do |char|
176
+ char.tr!("\n", "\r")
177
+ send_cmd! "Input.dispatchKeyEvent", {type: "keyDown", text: char}
178
+ send_cmd! "Input.dispatchKeyEvent", {type: "keyUp"}
179
+ end
180
+ end
181
+
182
+ def send_keys(*keys)
183
+ keys.each do |key|
184
+ if key.is_a? Array
185
+ mods, new_keys = get_modifiers(key)
186
+ new_keys.each do |k|
187
+ send_key_data(get_key_data(k), modifiers: mods)
188
+ end
189
+ else
190
+ send_key_data(get_key_data(key))
191
+ end
192
+ end
193
+ end
194
+
195
+ def get_key_data(key)
196
+ KEY_DATA[key]
197
+ end
198
+
199
+ def send_key_data(data, modifiers: 0)
200
+ type = data[:text] ? "keyDown" : "rawKeyDown"
201
+ send_cmd! "Input.dispatchKeyEvent", {type: type, text: data[:text].to_s, windowsVirtualKeyCode: data[:key_code].to_i, code: data[:code].to_s, key: data[:key].to_s, modifiers: modifiers}
202
+ send_cmd! "Input.dispatchKeyEvent", {type: "keyUp", modifiers: modifiers}
203
+ end
204
+
205
+ def get_modifiers(ary)
206
+ mods = []
207
+ keys = []
208
+ ary.each do |k|
209
+ case k
210
+ when :alt
211
+ mods << 1
212
+ when :control
213
+ mods << 2
214
+ when :meta, :command
215
+ mods << 4
216
+ when :shift
217
+ mods << 8
218
+ else
219
+ keys << k
220
+ end
221
+ end
222
+ [mods.inject(&:|), keys]
223
+ end
224
+
225
+ def focus
226
+ val = browser.evaluate_script %( ChromeRemoteHelper.onSelfValue(#{id}, "focus") )
227
+ end
228
+
229
+ def value(*args)
230
+ debug args
231
+ val = browser.evaluate_script %( ChromeRemoteHelper.onSelfValue(#{id}, "value") )
232
+ end
233
+
234
+ def checked?
235
+ val = browser.evaluate_script %( ChromeRemoteHelper.onSelfValue(#{id}, "checked") )
236
+ end
237
+
238
+ def selected?
239
+ val = browser.evaluate_script %( ChromeRemoteHelper.onSelfValue(#{id}, "selected") )
240
+ end
241
+
242
+ def disabled?
243
+ val = browser.evaluate_script %( ChromeRemoteHelper.onSelfValue(#{id}, "disabled") )
244
+ end
245
+
246
+ def select_option(*args)
247
+ on_self_value %(
248
+ if (this.disabled)
249
+ return;
250
+
251
+ var selectNode = this.parentNode;
252
+ if (selectNode.tagName == "OPTGROUP")
253
+ selectNode = selectNode.parentNode;
254
+
255
+ ChromeRemoteHelper.dispatchEvent(selectNode, "mousedown");
256
+ selectNode.focus();
257
+ ChromeRemoteHelper.dispatchEvent(selectNode, "input");
258
+
259
+ if (!this.selected) {
260
+ this.selected = true;
261
+ ChromeRemoteHelper.dispatchEvent(selectNode, "change");
262
+ }
263
+
264
+ ChromeRemoteHelper.dispatchEvent(selectNode, "mouseup");
265
+ ChromeRemoteHelper.dispatchEvent(selectNode, "click");
266
+ )
267
+ end
268
+
269
+ def trigger(event_name)
270
+ on_self %( ChromeRemoteHelper.dispatchEvent(this, '#{event_name}') )
271
+ end
272
+
273
+ def method_missing(method, *args)
274
+ debug ["method missing", method, args]
275
+ raise "method missing #{method} #{args.inspect}"
276
+ end
277
+
278
+ def [](attr)
279
+ on_self_value %(return this.getAttribute("#{attr}") )
280
+ end
281
+
282
+ def node_description
283
+ # return @node_description if @node_description
284
+ @node_description = send_cmd("DOM.describeNode", nodeId: id)
285
+ debug ["node_desc", @node_description, id]
286
+ if @node_description.nil?
287
+ raise Capybara::ExpectationNotMet
288
+ end
289
+ @node_description
290
+ end
291
+
292
+ def tag_name
293
+ # node_description["node"]["localName"]
294
+ on_self_value("return this.tagName.toLowerCase()")
295
+ end
296
+ alias local_name tag_name
297
+
298
+ def send_cmd(cmd, args={})
299
+ browser.remote.send_cmd(cmd, args)
300
+ end
301
+ def send_cmd!(cmd, args={})
302
+ browser.remote.send_cmd!(cmd, args)
303
+ end
304
+
305
+ def on_self_value(function_body, options={})
306
+ function_body = function_body.gsub('"', '\"').gsub(/\s+/, " ")
307
+ browser.evaluate_script %(ChromeRemoteHelper.onSelf(#{id}, "#{function_body}"))
308
+ end
309
+
310
+ def on_self(function_body, options={})
311
+ function_body = function_body.gsub('"', '\"').gsub(/\s+/, " ")
312
+ browser.evaluate_script %( window.ChromeRemoteHelper && ChromeRemoteHelper.waitDOMContentLoaded(); )
313
+ browser.evaluate_script %(window.ChromeRemoteHelper && ChromeRemoteHelper.onSelf(#{id}, "#{function_body}"))
314
+ end
315
+
316
+ def on_self!(function_body, options={})
317
+ function_body = function_body.gsub('"', '\"').gsub(/\s+/, " ")
318
+
319
+ browser.evaluate_script %( ChromeRemoteHelper.waitDOMContentLoaded(); )
320
+
321
+ browser.execute_script! %(ChromeRemoteHelper.onSelf(#{id}, "#{function_body}"))
322
+ end
323
+
324
+ def remote_object_id
325
+ remote_object["object"]["objectId"]
326
+ end
327
+
328
+ def remote_object
329
+ return @remote_object if @remote_object
330
+ @remote_object = send_cmd("DOM.resolveNode", nodeId: id)
331
+ debug @remote_object, id
332
+ if @remote_object.nil?
333
+ raise Capybara::ExpectationNotMet
334
+ end
335
+ @remote_object
336
+ end
337
+
338
+ def request_node(remote_object_id)
339
+ @request_node ||= send_cmd("DOM.requestNode", objectId: remote_object_id)
340
+ end
341
+ end
342
+
343
+ end