poltergeistFork 0.0.1

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