poltergeistFork 0.0.1

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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +22 -0
  3. data/README.md +425 -0
  4. data/lib/capybara/poltergeist/browser.rb +426 -0
  5. data/lib/capybara/poltergeist/client.rb +151 -0
  6. data/lib/capybara/poltergeist/client/agent.coffee +423 -0
  7. data/lib/capybara/poltergeist/client/browser.coffee +497 -0
  8. data/lib/capybara/poltergeist/client/cmd.coffee +17 -0
  9. data/lib/capybara/poltergeist/client/compiled/agent.js +587 -0
  10. data/lib/capybara/poltergeist/client/compiled/browser.js +687 -0
  11. data/lib/capybara/poltergeist/client/compiled/cmd.js +31 -0
  12. data/lib/capybara/poltergeist/client/compiled/connection.js +25 -0
  13. data/lib/capybara/poltergeist/client/compiled/main.js +228 -0
  14. data/lib/capybara/poltergeist/client/compiled/node.js +88 -0
  15. data/lib/capybara/poltergeist/client/compiled/web_page.js +539 -0
  16. data/lib/capybara/poltergeist/client/connection.coffee +11 -0
  17. data/lib/capybara/poltergeist/client/main.coffee +99 -0
  18. data/lib/capybara/poltergeist/client/node.coffee +70 -0
  19. data/lib/capybara/poltergeist/client/pre/agent.js +587 -0
  20. data/lib/capybara/poltergeist/client/pre/browser.js +688 -0
  21. data/lib/capybara/poltergeist/client/pre/cmd.js +31 -0
  22. data/lib/capybara/poltergeist/client/pre/connection.js +25 -0
  23. data/lib/capybara/poltergeist/client/pre/main.js +228 -0
  24. data/lib/capybara/poltergeist/client/pre/node.js +88 -0
  25. data/lib/capybara/poltergeist/client/pre/web_page.js +540 -0
  26. data/lib/capybara/poltergeist/client/web_page.coffee +372 -0
  27. data/lib/capybara/poltergeist/command.rb +17 -0
  28. data/lib/capybara/poltergeist/cookie.rb +35 -0
  29. data/lib/capybara/poltergeist/driver.rb +394 -0
  30. data/lib/capybara/poltergeist/errors.rb +183 -0
  31. data/lib/capybara/poltergeist/inspector.rb +46 -0
  32. data/lib/capybara/poltergeist/json.rb +25 -0
  33. data/lib/capybara/poltergeist/network_traffic.rb +7 -0
  34. data/lib/capybara/poltergeist/network_traffic/error.rb +19 -0
  35. data/lib/capybara/poltergeist/network_traffic/request.rb +27 -0
  36. data/lib/capybara/poltergeist/network_traffic/response.rb +40 -0
  37. data/lib/capybara/poltergeist/node.rb +177 -0
  38. data/lib/capybara/poltergeist/server.rb +36 -0
  39. data/lib/capybara/poltergeist/utility.rb +9 -0
  40. data/lib/capybara/poltergeist/version.rb +5 -0
  41. data/lib/capybara/poltergeist/web_socket_server.rb +107 -0
  42. data/lib/capybara/poltergeistFork.rb +27 -0
  43. metadata +268 -0
@@ -0,0 +1,426 @@
1
+ require "capybara/poltergeist/errors"
2
+ require "capybara/poltergeist/command"
3
+ require 'multi_json'
4
+ require 'time'
5
+
6
+ module Capybara::Poltergeist
7
+ class Browser
8
+ ERROR_MAPPINGS = {
9
+ 'Poltergeist.JavascriptError' => JavascriptError,
10
+ 'Poltergeist.FrameNotFound' => FrameNotFound,
11
+ 'Poltergeist.InvalidSelector' => InvalidSelector,
12
+ 'Poltergeist.StatusFailError' => StatusFailError,
13
+ 'Poltergeist.NoSuchWindowError' => NoSuchWindowError
14
+ }
15
+
16
+ attr_reader :server, :client, :logger
17
+
18
+ def initialize(server, client, logger = nil)
19
+ @server = server
20
+ @client = client
21
+ @logger = logger
22
+ end
23
+
24
+ def restart
25
+ server.restart
26
+ client.restart
27
+
28
+ self.debug = @debug if defined?(@debug)
29
+ self.js_errors = @js_errors if defined?(@js_errors)
30
+ self.extensions = @extensions if @extensions
31
+ end
32
+
33
+ def visit(url)
34
+ command 'visit', url
35
+ end
36
+
37
+ def current_url
38
+ command 'current_url'
39
+ end
40
+
41
+ def status_code
42
+ command 'status_code'
43
+ end
44
+
45
+ def body
46
+ command 'body'
47
+ end
48
+
49
+ def source
50
+ command 'source'
51
+ end
52
+
53
+ def title
54
+ command 'title'
55
+ end
56
+
57
+ def parents(page_id, id)
58
+ command 'parents', page_id, id
59
+ end
60
+
61
+ def find(method, selector)
62
+ result = command('find', method, selector)
63
+ result['ids'].map { |id| [result['page_id'], id] }
64
+ end
65
+
66
+ def find_within(page_id, id, method, selector)
67
+ command 'find_within', page_id, id, method, selector
68
+ end
69
+
70
+ def all_text(page_id, id)
71
+ command 'all_text', page_id, id
72
+ end
73
+
74
+ def visible_text(page_id, id)
75
+ command 'visible_text', page_id, id
76
+ end
77
+
78
+ def delete_text(page_id, id)
79
+ command 'delete_text', page_id, id
80
+ end
81
+
82
+ def property(page_id, id, name)
83
+ command 'property', page_id, id, name.to_s
84
+ end
85
+
86
+ def attributes(page_id, id)
87
+ command 'attributes', page_id, id
88
+ end
89
+
90
+ def attribute(page_id, id, name)
91
+ command 'attribute', page_id, id, name.to_s
92
+ end
93
+
94
+ def value(page_id, id)
95
+ command 'value', page_id, id
96
+ end
97
+
98
+ def set(page_id, id, value)
99
+ command 'set', page_id, id, value
100
+ end
101
+
102
+ def select_file(page_id, id, value)
103
+ command 'select_file', page_id, id, value
104
+ end
105
+
106
+ def tag_name(page_id, id)
107
+ command('tag_name', page_id, id).downcase
108
+ end
109
+
110
+ def visible?(page_id, id)
111
+ command 'visible', page_id, id
112
+ end
113
+
114
+ def disabled?(page_id, id)
115
+ command 'disabled', page_id, id
116
+ end
117
+
118
+ def click_coordinates(x, y)
119
+ command 'click_coordinates', x, y
120
+ end
121
+
122
+ def evaluate(script)
123
+ command 'evaluate', script
124
+ end
125
+
126
+ def execute(script)
127
+ command 'execute', script
128
+ end
129
+
130
+ def within_frame(handle, &block)
131
+ if handle.is_a?(Capybara::Node::Base)
132
+ command 'push_frame', [handle.native.page_id, handle.native.id]
133
+ else
134
+ command 'push_frame', handle
135
+ end
136
+
137
+ yield
138
+ ensure
139
+ command 'pop_frame'
140
+ end
141
+
142
+ def window_handle
143
+ command 'window_handle'
144
+ end
145
+
146
+ def window_handles
147
+ command 'window_handles'
148
+ end
149
+
150
+ def switch_to_window(handle)
151
+ command 'switch_to_window', handle
152
+ end
153
+
154
+ def open_new_window
155
+ command 'open_new_window'
156
+ end
157
+
158
+ def close_window(handle)
159
+ command 'close_window', handle
160
+ end
161
+
162
+ def find_window_handle(locator)
163
+ return locator if window_handles.include? locator
164
+
165
+ handle = command 'window_handle', locator
166
+ raise noSuchWindowError unless handle
167
+ return handle
168
+ end
169
+
170
+ def within_window(locator, &block)
171
+ original = window_handle
172
+ handle = find_window_handle(locator)
173
+ switch_to_window(handle)
174
+ yield
175
+ ensure
176
+ switch_to_window(original)
177
+ end
178
+
179
+ def click(page_id, id)
180
+ command 'click', page_id, id
181
+ end
182
+
183
+ def right_click(page_id, id)
184
+ command 'right_click', page_id, id
185
+ end
186
+
187
+ def double_click(page_id, id)
188
+ command 'double_click', page_id, id
189
+ end
190
+
191
+ def hover(page_id, id)
192
+ command 'hover', page_id, id
193
+ end
194
+
195
+ def drag(page_id, id, other_id)
196
+ command 'drag', page_id, id, other_id
197
+ end
198
+
199
+ def drag_by(page_id, id, x, y)
200
+ command 'drag_by', page_id, id, x, y
201
+ end
202
+
203
+ def select(page_id, id, value)
204
+ command 'select', page_id, id, value
205
+ end
206
+
207
+ def trigger(page_id, id, event)
208
+ command 'trigger', page_id, id, event.to_s
209
+ end
210
+
211
+ def reset
212
+ command 'reset'
213
+ end
214
+
215
+ def scroll_to(left, top)
216
+ command 'scroll_to', left, top
217
+ end
218
+
219
+ def render(path, options = {})
220
+ check_render_options!(options)
221
+ command 'render', path.to_s, !!options[:full], options[:selector]
222
+ end
223
+
224
+ def render_base64(format, options = {})
225
+ check_render_options!(options)
226
+ command 'render_base64', format.to_s, !!options[:full], options[:selector]
227
+ end
228
+
229
+ def set_zoom_factor(zoom_factor)
230
+ command 'set_zoom_factor', zoom_factor
231
+ end
232
+
233
+ def set_screen_size(s_width,s_height)
234
+ command 'set_screen_size', s_width, s_height
235
+ end
236
+
237
+ def set_paper_size(size)
238
+ command 'set_paper_size', size
239
+ end
240
+
241
+ def resize(width, height)
242
+ command 'resize', width, height
243
+ end
244
+
245
+ def send_keys(page_id, id, keys)
246
+ command 'send_keys', page_id, id, normalize_keys(keys)
247
+ end
248
+
249
+ def path(page_id, id)
250
+ command 'path', page_id, id
251
+ end
252
+
253
+ def network_traffic
254
+ command('network_traffic').values.map do |event|
255
+ NetworkTraffic::Request.new(
256
+ event['request'],
257
+ event['responseParts'].map { |response| NetworkTraffic::Response.new(response) },
258
+ event['error'] ? NetworkTraffic::Error.new(event['error']) : nil
259
+ )
260
+ end
261
+ end
262
+
263
+ def clear_network_traffic
264
+ command('clear_network_traffic')
265
+ end
266
+
267
+ def equals(page_id, id, other_id)
268
+ command('equals', page_id, id, other_id)
269
+ end
270
+
271
+ def get_headers
272
+ command 'get_headers'
273
+ end
274
+
275
+ def set_headers(headers)
276
+ command 'set_headers', headers
277
+ end
278
+
279
+ def add_headers(headers)
280
+ command 'add_headers', headers
281
+ end
282
+
283
+ def add_header(header, permanent)
284
+ command 'add_header', header, permanent
285
+ end
286
+
287
+ def response_headers
288
+ command 'response_headers'
289
+ end
290
+
291
+ def cookies
292
+ Hash[command('cookies').map { |cookie| [cookie['name'], Cookie.new(cookie)] }]
293
+ end
294
+
295
+ def set_cookie(cookie)
296
+ if cookie[:expires]
297
+ cookie[:expires] = cookie[:expires].to_i * 1000
298
+ end
299
+
300
+ command 'set_cookie', cookie
301
+ end
302
+
303
+ def remove_cookie(name)
304
+ command 'remove_cookie', name
305
+ end
306
+
307
+ def clear_cookies
308
+ command 'clear_cookies'
309
+ end
310
+
311
+ def cookies_enabled=(flag)
312
+ command 'cookies_enabled', !!flag
313
+ end
314
+
315
+ def set_http_auth(user, password)
316
+ command 'set_http_auth', user, password
317
+ end
318
+
319
+ def js_errors=(val)
320
+ @js_errors = val
321
+ command 'set_js_errors', !!val
322
+ end
323
+
324
+ def extensions=(names)
325
+ @extensions = names
326
+ Array(names).each do |name|
327
+ command 'add_extension', name
328
+ end
329
+ end
330
+
331
+ def url_blacklist=(blacklist)
332
+ command 'set_url_blacklist', *blacklist
333
+ end
334
+
335
+ def debug=(val)
336
+ @debug = val
337
+ command 'set_debug', !!val
338
+ end
339
+
340
+ def command(name, *args)
341
+ cmd = Command.new(name, *args)
342
+ log cmd.message
343
+
344
+ response = server.send(cmd)
345
+ log response
346
+
347
+ json = JSON.load(response)
348
+
349
+ if json['error']
350
+ klass = ERROR_MAPPINGS[json['error']['name']] || BrowserError
351
+ raise klass.new(json['error'])
352
+ else
353
+ json['response']
354
+ end
355
+ rescue DeadClient
356
+ restart
357
+ raise
358
+ end
359
+
360
+ def go_back
361
+ command 'go_back'
362
+ end
363
+
364
+ def go_forward
365
+ command 'go_forward'
366
+ end
367
+
368
+ def accept_confirm
369
+ command 'set_confirm_process', true
370
+ end
371
+
372
+ def dismiss_confirm
373
+ command 'set_confirm_process', false
374
+ end
375
+
376
+ #
377
+ # press "OK" with text (response) or default value
378
+ #
379
+ def accept_prompt(response)
380
+ command 'set_prompt_response', response || false
381
+ end
382
+
383
+ #
384
+ # press "Cancel"
385
+ #
386
+ def dismiss_prompt
387
+ command 'set_prompt_response', nil
388
+ end
389
+
390
+ def modal_message
391
+ command 'modal_message'
392
+ end
393
+
394
+ private
395
+
396
+ def log(message)
397
+ logger.puts message if logger
398
+ end
399
+
400
+ def check_render_options!(options)
401
+ if !!options[:full] && options.has_key?(:selector)
402
+ warn "Ignoring :selector in #render since :full => true was given at #{caller.first}"
403
+ options.delete(:selector)
404
+ end
405
+ end
406
+
407
+ def normalize_keys(keys)
408
+ keys.map do |key|
409
+ case key
410
+ when Array
411
+ # [:Shift, "s"] => { modifier: "shift", key: "S" }
412
+ # [:Ctrl, :Left] => { modifier: "ctrl", key: :Left }
413
+ # [:Ctrl, :Shift, :Left] => { modifier: "ctrl,shift", key: :Left }
414
+ letter = key.pop
415
+ symbol = key.map { |k| k.to_s.downcase }.join(',')
416
+
417
+ { modifier: symbol.to_s.downcase, key: letter.capitalize }
418
+ when Symbol
419
+ { key: key.capitalize } # Return a known sequence for PhantomJS
420
+ when String
421
+ key # Plain string, nothing to do
422
+ end
423
+ end
424
+ end
425
+ end
426
+ end
@@ -0,0 +1,151 @@
1
+ require "timeout"
2
+ require "capybara/poltergeist/utility"
3
+ require 'cliver'
4
+
5
+ module Capybara::Poltergeist
6
+ class Client
7
+ PHANTOMJS_SCRIPT = File.expand_path('../client/compiled/main.js', __FILE__)
8
+ PHANTOMJS_VERSION = ['>= 1.8.1', '< 3.0']
9
+ PHANTOMJS_NAME = 'phantomjs'
10
+
11
+ KILL_TIMEOUT = 2 # seconds
12
+
13
+ def self.start(*args)
14
+ client = new(*args)
15
+ client.start
16
+ client
17
+ end
18
+
19
+ # Returns a proc, that when called will attempt to kill the given process.
20
+ # This is because implementing ObjectSpace.define_finalizer is tricky.
21
+ # Hat-Tip to @mperham for describing in detail:
22
+ # http://www.mikeperham.com/2010/02/24/the-trouble-with-ruby-finalizers/
23
+ def self.process_killer(pid)
24
+ proc do
25
+ begin
26
+ Process.kill('KILL', pid)
27
+ rescue Errno::ESRCH, Errno::ECHILD
28
+ end
29
+ end
30
+ end
31
+
32
+ attr_reader :pid, :server, :path, :window_size, :phantomjs_options
33
+
34
+ def initialize(server, options = {})
35
+ @server = server
36
+ @path = Cliver::detect!((options[:path] || PHANTOMJS_NAME),
37
+ *PHANTOMJS_VERSION)
38
+
39
+ @window_size = options[:window_size] || [1024, 768]
40
+ @phantomjs_options = options[:phantomjs_options] || []
41
+ @phantomjs_logger = options[:phantomjs_logger] || $stdout
42
+
43
+ pid = Process.pid
44
+ at_exit do
45
+ # do the work in a separate thread, to avoid stomping on $!,
46
+ # since other libraries depend on it directly.
47
+ Thread.new do
48
+ stop if Process.pid == pid
49
+ end.join
50
+ end
51
+ end
52
+
53
+ def start
54
+ @read_io, @write_io = IO.pipe
55
+ @out_thread = Thread.new {
56
+ while !@read_io.eof? && data = @read_io.readpartial(1024)
57
+ @phantomjs_logger.write(data)
58
+ end
59
+ }
60
+
61
+ process_options = {}
62
+ process_options[:pgroup] = true unless Capybara::Poltergeist.windows?
63
+
64
+ redirect_stdout do
65
+ @pid = Process.spawn(*command.map(&:to_s), process_options)
66
+ ObjectSpace.define_finalizer(self, self.class.process_killer(@pid))
67
+ end
68
+ end
69
+
70
+ def stop
71
+ if pid
72
+ kill_phantomjs
73
+ @out_thread.kill
74
+ close_io
75
+ ObjectSpace.undefine_finalizer(self)
76
+ end
77
+ end
78
+
79
+ def restart
80
+ stop
81
+ start
82
+ end
83
+
84
+ def command
85
+ parts = [path]
86
+ parts.concat phantomjs_options
87
+ parts << PHANTOMJS_SCRIPT
88
+ parts << server.port
89
+ parts.concat window_size
90
+ parts
91
+ end
92
+
93
+ private
94
+
95
+ # This abomination is because JRuby doesn't support the :out option of
96
+ # Process.spawn. To be honest it works pretty bad with pipes too, because
97
+ # we ought close writing end in parent process immediately but JRuby will
98
+ # lose all the output from child. Process.popen can be used here and seems
99
+ # it works with JRuby but I've experienced strange mistakes on Rubinius.
100
+ def redirect_stdout
101
+ prev = STDOUT.dup
102
+ $stdout = @write_io
103
+ STDOUT.reopen(@write_io)
104
+ yield
105
+ ensure
106
+ STDOUT.reopen(prev)
107
+ $stdout = STDOUT
108
+ prev.close
109
+ end
110
+
111
+ def kill_phantomjs
112
+ begin
113
+ if Capybara::Poltergeist.windows?
114
+ Process.kill('KILL', pid)
115
+ else
116
+ Process.kill('TERM', pid)
117
+ begin
118
+ Timeout.timeout(KILL_TIMEOUT) { Process.wait(pid) }
119
+ rescue Timeout::Error
120
+ Process.kill('KILL', pid)
121
+ Process.wait(pid)
122
+ end
123
+ end
124
+ rescue Errno::ESRCH, Errno::ECHILD
125
+ # Zed's dead, baby
126
+ end
127
+ @pid = nil
128
+ end
129
+
130
+ # We grab all the output from PhantomJS like console.log in another thread
131
+ # and when PhantomJS crashes we try to restart it. In order to do it we stop
132
+ # server and client and on JRuby see this error `IOError: Stream closed`.
133
+ # It happens because JRuby tries to close pipe and it is blocked on `eof?`
134
+ # or `readpartial` call. The error is raised in the related thread and it's
135
+ # not actually main thread but the thread that listens to the output. That's
136
+ # why if you put some debug code after `rescue IOError` it won't be shown.
137
+ # In fact the main thread will continue working after the error even if we
138
+ # don't use `rescue`. The first attempt to fix it was a try not to block on
139
+ # IO, but looks like similar issue appers after JRuby upgrade. Perhaps the
140
+ # only way to fix it is catching the exception what this method overall does.
141
+ def close_io
142
+ [@write_io, @read_io].each do |io|
143
+ begin
144
+ io.close unless io.closed?
145
+ rescue IOError
146
+ raise unless RUBY_ENGINE == 'jruby'
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end