poltergeist 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,102 @@
1
+ class Poltergeist.WebPage
2
+ @CALLBACKS = ['onAlert', 'onConsoleMessage', 'onLoadFinished', 'onInitialized',
3
+ 'onLoadStarted', 'onResourceRequested', 'onResourceReceived']
4
+ @DELEGATES = ['open', 'sendEvent', 'uploadFile', 'release', 'render']
5
+ @COMMANDS = ['currentUrl', 'find', 'nodeCall', 'pushFrame', 'popFrame', 'documentSize']
6
+
7
+ constructor: ->
8
+ @native = require('webpage').create()
9
+ @nodes = {}
10
+ @_source = ""
11
+
12
+ for callback in WebPage.CALLBACKS
13
+ this.bindCallback(callback)
14
+
15
+ this.injectAgent()
16
+
17
+ for command in @COMMANDS
18
+ do (command) =>
19
+ this.prototype[command] =
20
+ (arguments...) -> this.runCommand(command, arguments)
21
+
22
+ for delegate in @DELEGATES
23
+ do (delegate) =>
24
+ this.prototype[delegate] =
25
+ -> @native[delegate].apply(@native, arguments)
26
+
27
+ onInitializedNative: ->
28
+ @_source = null
29
+ this.injectAgent()
30
+ this.setScrollPosition({ left: 0, top: 0})
31
+
32
+ injectAgent: ->
33
+ if this.evaluate(-> typeof __poltergeist) == "undefined"
34
+ @native.injectJs('agent.js')
35
+
36
+ onConsoleMessageNative: (message) ->
37
+ if message == '__DOMContentLoaded'
38
+ @_source = @native.content
39
+ false
40
+
41
+ onLoadFinishedNative: ->
42
+ @_source or= @native.content
43
+
44
+ onConsoleMessage: (message) ->
45
+ console.log(message)
46
+
47
+ content: ->
48
+ @native.content
49
+
50
+ source: ->
51
+ @_source
52
+
53
+ viewportSize: ->
54
+ @native.viewportSize
55
+
56
+ scrollPosition: ->
57
+ @native.scrollPosition
58
+
59
+ setScrollPosition: (pos) ->
60
+ @native.scrollPosition = pos
61
+
62
+ viewport: ->
63
+ scroll = this.scrollPosition()
64
+ size = this.viewportSize()
65
+
66
+ top: scroll.top, bottom: scroll.top + size.height,
67
+ left: scroll.left, right: scroll.left + size.width,
68
+ width: size.width, height: size.height
69
+
70
+ get: (id) ->
71
+ @nodes[id] or= new Poltergeist.Node(this, id)
72
+
73
+ evaluate: (fn, args...) ->
74
+ @native.evaluate("function() { return #{this.stringifyCall(fn, args)} }")
75
+
76
+ execute: (fn, args...) ->
77
+ @native.evaluate("function() { #{this.stringifyCall(fn, args)} }")
78
+
79
+ stringifyCall: (fn, args) ->
80
+ if args.length == 0
81
+ "(#{fn.toString()})()"
82
+ else
83
+ # The JSON.stringify happens twice because the second time we are essentially
84
+ # escaping the string.
85
+ "(#{fn.toString()}).apply(this, JSON.parse(#{JSON.stringify(JSON.stringify(args))}))"
86
+
87
+ # For some reason phantomjs seems to have trouble with doing 'fat arrow' binding here,
88
+ # hence the 'that' closure.
89
+ bindCallback: (name) ->
90
+ that = this
91
+ @native[name] = ->
92
+ if that[name + 'Native']? # For internal callbacks
93
+ result = that[name + 'Native'].apply(that, arguments)
94
+
95
+ if result != false && that[name]? # For externally set callbacks
96
+ that[name].apply(that, arguments)
97
+
98
+ runCommand: (name, arguments) ->
99
+ this.evaluate(
100
+ (name, arguments) -> __poltergeist[name].apply(__poltergeist, arguments),
101
+ name, arguments
102
+ )
@@ -0,0 +1,57 @@
1
+ require 'open3'
2
+
3
+ module Capybara::Poltergeist
4
+ class Client
5
+ PHANTOM_SCRIPT = File.expand_path('../client/compiled/main.js', __FILE__)
6
+
7
+ attr_reader :pid, :port, :path
8
+
9
+ def initialize(port, path = nil)
10
+ @port = port
11
+ @path = path || 'phantomjs'
12
+
13
+ start
14
+ at_exit { stop }
15
+ end
16
+
17
+ def start
18
+ @pid = Process.fork do
19
+ Open3.popen3("#{path} #{PHANTOM_SCRIPT} #{port}") do |stdin, stdout, stderr|
20
+ loop do
21
+ select = IO.select([stdout, stderr])
22
+ stream = select.first.first
23
+
24
+ break if stream.eof?
25
+
26
+ if stream == stdout
27
+ STDOUT.puts stdout.readline
28
+ elsif stream == stderr
29
+ line = stderr.readline
30
+
31
+ # QtWebkit seems to throw this error all the time when using WebSockets, but
32
+ # it doesn't appear to actually stop anything working, so filter it out.
33
+ #
34
+ # This isn't the nicest solution I know :( Hopefully it will be fixed in
35
+ # QtWebkit (if you search for this string, you'll see it's been reported in
36
+ # various places).
37
+ unless line.include?('WebCore::SocketStreamHandlePrivate::socketSentData()')
38
+ STDERR.puts line
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ def stop
47
+ Process.kill('TERM', pid)
48
+ rescue Errno::ESRCH
49
+ # Bovvered, I ain't
50
+ end
51
+
52
+ def restart
53
+ stop
54
+ start
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,85 @@
1
+ module Capybara::Poltergeist
2
+ class Driver < Capybara::Driver::Base
3
+ attr_reader :app, :server, :browser, :options
4
+
5
+ def initialize(app, options = {})
6
+ @app = app
7
+ @options = options
8
+ @server = Capybara::Server.new(app)
9
+ @browser = nil
10
+
11
+ @server.boot if Capybara.run_server
12
+ end
13
+
14
+ def browser
15
+ @browser ||= Browser.new(
16
+ :logger => logger,
17
+ :phantomjs => options[:phantomjs]
18
+ )
19
+ end
20
+
21
+ def restart
22
+ browser.restart
23
+ end
24
+
25
+ # logger should be an object that responds to puts, or nil
26
+ def logger
27
+ options[:logger] || (options[:debug] && STDERR)
28
+ end
29
+
30
+ def visit(path, attributes = {})
31
+ browser.visit(url(path), attributes)
32
+ end
33
+
34
+ def current_url
35
+ browser.current_url
36
+ end
37
+
38
+ def body
39
+ browser.body
40
+ end
41
+
42
+ def source
43
+ browser.source
44
+ end
45
+
46
+ def find(selector)
47
+ browser.find(selector).map { |node| Capybara::Poltergeist::Node.new(self, node) }
48
+ end
49
+
50
+ def evaluate_script(script)
51
+ browser.evaluate(script)
52
+ end
53
+
54
+ def execute_script(script)
55
+ browser.execute(script)
56
+ nil
57
+ end
58
+
59
+ def within_frame(id, &block)
60
+ browser.within_frame(id, &block)
61
+ end
62
+
63
+ def reset!
64
+ browser.reset
65
+ end
66
+
67
+ def render(path)
68
+ browser.render(path)
69
+ end
70
+
71
+ def wait?
72
+ true
73
+ end
74
+
75
+ def invalid_element_errors
76
+ [Capybara::Poltergeist::ObsoleteNode]
77
+ end
78
+
79
+ private
80
+
81
+ def url(path)
82
+ server.url(path)
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,26 @@
1
+ module Capybara
2
+ module Poltergeist
3
+ class Error < StandardError
4
+ end
5
+
6
+ class BrowserError < Error
7
+ attr_reader :text
8
+
9
+ def initialize(text)
10
+ @text = text
11
+ end
12
+
13
+ def message
14
+ "Received error from PhantomJS client: #{text}"
15
+ end
16
+ end
17
+
18
+ class ObsoleteNode < Error
19
+ attr_reader :node
20
+
21
+ def initialize(node)
22
+ @node = node
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,92 @@
1
+ module Capybara::Poltergeist
2
+ class Node < Capybara::Driver::Node
3
+ alias id native
4
+
5
+ def browser
6
+ driver.browser
7
+ end
8
+
9
+ def command(name, *args)
10
+ browser.send(name, id, *args)
11
+ rescue BrowserError => error
12
+ if error.text == 'Poltergeist.ObsoleteNode'
13
+ raise ObsoleteNode.new(self)
14
+ else
15
+ raise error
16
+ end
17
+ end
18
+
19
+ def find(selector)
20
+ browser.find(selector, id).map { |node| self.class.new(driver, node) }
21
+ end
22
+
23
+ def text
24
+ command :text
25
+ end
26
+
27
+ def [](name)
28
+ command :attribute, name
29
+ end
30
+
31
+ def value
32
+ command :value
33
+ end
34
+
35
+ def set(value)
36
+ if tag_name == 'input'
37
+ type = self[:type]
38
+
39
+ if type == 'radio'
40
+ click
41
+ elsif type == 'checkbox'
42
+ if value && !checked? || !value && checked?
43
+ click
44
+ end
45
+ elsif type == 'file'
46
+ command :select_file, value
47
+ else
48
+ command :set, value
49
+ end
50
+ elsif tag_name == 'textarea'
51
+ command :set, value
52
+ end
53
+ end
54
+
55
+ def select_option
56
+ command :select, true
57
+ end
58
+
59
+ def unselect_option
60
+ command(:select, false) or
61
+ raise(Capybara::UnselectNotAllowed, "Cannot unselect option from single select box.")
62
+ end
63
+
64
+ def tag_name
65
+ @tag_name ||= command(:tag_name)
66
+ end
67
+
68
+ def visible?
69
+ command :visible?
70
+ end
71
+
72
+ def checked?
73
+ self[:checked]
74
+ end
75
+
76
+ def selected?
77
+ self[:selected]
78
+ end
79
+
80
+ def click
81
+ command :click
82
+ end
83
+
84
+ def drag_to(other)
85
+ command :drag, other.id
86
+ end
87
+
88
+ def trigger(event)
89
+ command :trigger, event
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,35 @@
1
+ module Capybara::Poltergeist
2
+ class Server
3
+ attr_reader :port
4
+
5
+ def initialize
6
+ @port = find_available_port
7
+ start
8
+ end
9
+
10
+ def start
11
+ server_manager.start(port)
12
+ end
13
+
14
+ def restart
15
+ server_manager.restart(port)
16
+ end
17
+
18
+ def send(message)
19
+ server_manager.send(port, message)
20
+ end
21
+
22
+ private
23
+
24
+ def server_manager
25
+ ServerManager.instance
26
+ end
27
+
28
+ def find_available_port
29
+ server = TCPServer.new('127.0.0.1', 0)
30
+ server.addr[1]
31
+ ensure
32
+ server.close if server
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,118 @@
1
+ require 'em-websocket'
2
+ require 'timeout'
3
+ require 'singleton'
4
+
5
+ module Capybara::Poltergeist
6
+ # The reason for the lolzy thread code is because the EM reactor blocks the thread, so
7
+ # we have to put it in its own thread.
8
+ #
9
+ # The reason we are using EM, is because it has a WebSocket library. If there's a decent
10
+ # WebSocket library that doesn't require an event loop, we can use that.
11
+ class ServerManager
12
+ include Singleton
13
+
14
+ TIMEOUT = 30
15
+
16
+ class TimeoutError < StandardError
17
+ def initialize(message)
18
+ super "Server timed out waiting for response to #{@message}"
19
+ end
20
+ end
21
+
22
+ attr_reader :sockets
23
+
24
+ def initialize
25
+ @instruction = nil
26
+ @response = nil
27
+ @sockets = {}
28
+ @waiting = false
29
+
30
+ @main = Thread.current
31
+ @thread = Thread.new { start_event_loop }
32
+ @thread.abort_on_exception = true
33
+ end
34
+
35
+ def start(port)
36
+ thread_execute { start_websocket_server(port) }
37
+ end
38
+
39
+ # This isn't a 'proper' restart. It's more like 'wait for the client to connect again'.
40
+ def restart(port)
41
+ sockets[port] = nil
42
+ @thread.run
43
+ end
44
+
45
+ def send(port, message)
46
+ @message = nil
47
+
48
+ Timeout.timeout(TIMEOUT, TimeoutError.new(message)) do
49
+ # Ensure there is a socket before trying to send a message on it.
50
+ Thread.pass until sockets[port]
51
+
52
+ # Send the message
53
+ thread_execute { sockets[port].send(message) }
54
+
55
+ # Wait for the response message
56
+ Thread.pass until @message
57
+ end
58
+
59
+ @message
60
+ end
61
+
62
+ def thread_execute(&instruction)
63
+ # Ensure that the thread is waiting for an instruction before we wake it up
64
+ # to receive the instruction
65
+ Thread.pass until @waiting
66
+
67
+ @instruction = instruction
68
+ @waiting = false
69
+
70
+ # Bring the EM thread out of its sleep so that it can execute the instruction.
71
+ @thread.run
72
+ end
73
+
74
+ def start_event_loop
75
+ EM.run { await_instruction }
76
+ end
77
+
78
+ def start_websocket_server(port)
79
+ EventMachine.start_server('127.0.0.1', port, EventMachine::WebSocket::Connection, {}) do |socket|
80
+ socket.onopen do
81
+ connection_opened(port, socket)
82
+ end
83
+
84
+ socket.onmessage do |message|
85
+ message_received(message)
86
+ end
87
+ end
88
+ end
89
+
90
+ def connection_opened(port, socket)
91
+ sockets[port] = socket
92
+ await_instruction
93
+ end
94
+
95
+ def message_received(message)
96
+ @message = message
97
+ await_instruction
98
+ end
99
+
100
+ # Stop the thread so that it can be manually scheduled by the parent once there is
101
+ # something to do
102
+ def await_instruction
103
+ # Sleep this thread. The main thread will wake us up when there is an instruction
104
+ # to perform.
105
+ @waiting = true
106
+ Thread.stop
107
+
108
+ # Main thread has woken us up, so execute the current instruction.
109
+ if @instruction
110
+ @instruction.call
111
+ @instruction = nil
112
+ end
113
+
114
+ # Continue execution of the thread until a socket callback fires, which will
115
+ # trigger then method again and send us back to sleep.
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,5 @@
1
+ module Capybara
2
+ module Poltergeist
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,20 @@
1
+ require 'capybara'
2
+
3
+ module Capybara
4
+ module Poltergeist
5
+ autoload :Driver, 'capybara/poltergeist/driver'
6
+ autoload :Browser, 'capybara/poltergeist/browser'
7
+ autoload :Node, 'capybara/poltergeist/node'
8
+ autoload :ServerManager, 'capybara/poltergeist/server_manager'
9
+ autoload :Server, 'capybara/poltergeist/server'
10
+ autoload :Client, 'capybara/poltergeist/client'
11
+
12
+ autoload :Error, 'capybara/poltergeist/errors'
13
+ autoload :BrowserError, 'capybara/poltergeist/errors'
14
+ autoload :ObsoleteNode, 'capybara/poltergeist/errors'
15
+ end
16
+ end
17
+
18
+ Capybara.register_driver :poltergeist do |app|
19
+ Capybara::Poltergeist::Driver.new(app)
20
+ end
metadata ADDED
@@ -0,0 +1,102 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: poltergeist
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jon Leighton
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-10-27 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: capybara
16
+ requirement: &20250700 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 1.1.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *20250700
25
+ - !ruby/object:Gem::Dependency
26
+ name: em-websocket
27
+ requirement: &20220020 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: 0.3.1
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *20220020
36
+ - !ruby/object:Gem::Dependency
37
+ name: json
38
+ requirement: &20216800 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: '1.6'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *20216800
47
+ description: PhantomJS driver for Capybara
48
+ email:
49
+ - j@jonathanleighton.com
50
+ executables: []
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - lib/capybara/poltergeist.rb
55
+ - lib/capybara/poltergeist/client.rb
56
+ - lib/capybara/poltergeist/server.rb
57
+ - lib/capybara/poltergeist/browser.rb
58
+ - lib/capybara/poltergeist/errors.rb
59
+ - lib/capybara/poltergeist/version.rb
60
+ - lib/capybara/poltergeist/driver.rb
61
+ - lib/capybara/poltergeist/client/connection.coffee
62
+ - lib/capybara/poltergeist/client/node.coffee
63
+ - lib/capybara/poltergeist/client/compiled/agent.js
64
+ - lib/capybara/poltergeist/client/compiled/browser.js
65
+ - lib/capybara/poltergeist/client/compiled/main.js
66
+ - lib/capybara/poltergeist/client/compiled/node.js
67
+ - lib/capybara/poltergeist/client/compiled/connection.js
68
+ - lib/capybara/poltergeist/client/compiled/web_page.js
69
+ - lib/capybara/poltergeist/client/main.coffee
70
+ - lib/capybara/poltergeist/client/web_page.coffee
71
+ - lib/capybara/poltergeist/client/browser.coffee
72
+ - lib/capybara/poltergeist/client/agent.coffee
73
+ - lib/capybara/poltergeist/server_manager.rb
74
+ - lib/capybara/poltergeist/node.rb
75
+ - LICENSE
76
+ - README.md
77
+ - CHANGELOG.md
78
+ homepage: http://github.com/jonleighton/poltergeist
79
+ licenses: []
80
+ post_install_message:
81
+ rdoc_options: []
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ! '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ none: false
92
+ requirements:
93
+ - - ! '>='
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ requirements: []
97
+ rubyforge_project:
98
+ rubygems_version: 1.8.6
99
+ signing_key:
100
+ specification_version: 3
101
+ summary: PhantomJS driver for Capybara
102
+ test_files: []