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.
@@ -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