poltergeist 0.4.0 → 0.5.0
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.
- data/README.md +48 -2
- data/lib/capybara/poltergeist.rb +3 -0
- data/lib/capybara/poltergeist/browser.rb +48 -55
- data/lib/capybara/poltergeist/client.rb +42 -12
- data/lib/capybara/poltergeist/client/agent.coffee +48 -5
- data/lib/capybara/poltergeist/client/browser.coffee +64 -42
- data/lib/capybara/poltergeist/client/compiled/agent.js +71 -8
- data/lib/capybara/poltergeist/client/compiled/browser.js +68 -41
- data/lib/capybara/poltergeist/client/compiled/main.js +34 -5
- data/lib/capybara/poltergeist/client/compiled/node.js +34 -25
- data/lib/capybara/poltergeist/client/compiled/web_page.js +30 -7
- data/lib/capybara/poltergeist/client/main.coffee +26 -3
- data/lib/capybara/poltergeist/client/node.coffee +30 -23
- data/lib/capybara/poltergeist/client/web_page.coffee +31 -7
- data/lib/capybara/poltergeist/driver.rb +54 -19
- data/lib/capybara/poltergeist/errors.rb +63 -6
- data/lib/capybara/poltergeist/inspector.rb +35 -0
- data/lib/capybara/poltergeist/node.rb +15 -5
- data/lib/capybara/poltergeist/server.rb +7 -12
- data/lib/capybara/poltergeist/spawn.rb +17 -0
- data/lib/capybara/poltergeist/util.rb +12 -0
- data/lib/capybara/poltergeist/version.rb +1 -1
- data/lib/capybara/poltergeist/web_socket_server.rb +25 -6
- metadata +31 -25
    
        data/README.md
    CHANGED
    
    | @@ -1,8 +1,9 @@ | |
| 1 1 | 
             
            # Poltergeist - A PhantomJS driver for Capybara #
         | 
| 2 2 |  | 
| 3 | 
            -
            Version: 0. | 
| 3 | 
            +
            Version: 0.5.0
         | 
| 4 4 |  | 
| 5 5 | 
             
            [](http://travis-ci.org/jonleighton/poltergeist)
         | 
| 6 | 
            +
            [](https://gemnasium.com/jonleighton/poltergeist)
         | 
| 6 7 |  | 
| 7 8 | 
             
            Poltergeist is a driver for [Capybara](https://github.com/jnicklas/capybara). It allows you to
         | 
| 8 9 | 
             
            run your Capybara tests on a headless [WebKit](http://webkit.org) browser,
         | 
| @@ -10,7 +11,7 @@ provided by [PhantomJS](http://www.phantomjs.org/). | |
| 10 11 |  | 
| 11 12 | 
             
            ## Installation ##
         | 
| 12 13 |  | 
| 13 | 
            -
            Add `poltergeist` to your Gemfile, and  | 
| 14 | 
            +
            Add `poltergeist` to your Gemfile, and in your test setup add:
         | 
| 14 15 |  | 
| 15 16 | 
             
            ``` ruby
         | 
| 16 17 | 
             
            require 'capybara/poltergeist'
         | 
| @@ -172,6 +173,51 @@ makes debugging easier). Running `rake autocompile` will watch the | |
| 172 173 |  | 
| 173 174 | 
             
            ## Changes ##
         | 
| 174 175 |  | 
| 176 | 
            +
            ### 0.5.0 ###
         | 
| 177 | 
            +
             | 
| 178 | 
            +
            #### Features ####
         | 
| 179 | 
            +
             | 
| 180 | 
            +
            *   Detect if clicking an element will fail. If the click will actually
         | 
| 181 | 
            +
                hit another element (because that element is in front of the one we
         | 
| 182 | 
            +
                want to click), the user will now see an exception explaining what
         | 
| 183 | 
            +
                happened and which element would actually be targeted by the click. This
         | 
| 184 | 
            +
                should aid debugging. [Issue #25]
         | 
| 185 | 
            +
             | 
| 186 | 
            +
            *   Click elements at their middle position rather than the top-left.
         | 
| 187 | 
            +
                This is presumed to be more likely to succeed because the top-left
         | 
| 188 | 
            +
                may be obscured by overlapping elements, negative margins, etc. [Issue #26]
         | 
| 189 | 
            +
             | 
| 190 | 
            +
            *   Add experimental support for using the remote WebKit web inspector.
         | 
| 191 | 
            +
                This will only work with PhantomJS 1.5, which is not yet released,
         | 
| 192 | 
            +
                so it won't be officially supported by Poltergeist until 1.5 is
         | 
| 193 | 
            +
                released. [Issue #31]
         | 
| 194 | 
            +
             | 
| 195 | 
            +
            *   Add `page.driver.quit` method. If you spawn additional Capybara
         | 
| 196 | 
            +
                sessions, you might want to use this to reap the child phantomjs
         | 
| 197 | 
            +
                process. [Issue #24]
         | 
| 198 | 
            +
             | 
| 199 | 
            +
            *   Errors produced by Javascript on the page will now generate an
         | 
| 200 | 
            +
                exception within Ruby. [Issue #27]
         | 
| 201 | 
            +
             | 
| 202 | 
            +
            #### Bug fixes ####
         | 
| 203 | 
            +
             | 
| 204 | 
            +
            *   Fix bug where we could end up interacting with an obsolete element. [Issue #30]
         | 
| 205 | 
            +
             | 
| 206 | 
            +
            *   Raise an suitable error if PhantomJS returns a non-zero exit status.
         | 
| 207 | 
            +
                Previously a version error would be raised, indicating that the
         | 
| 208 | 
            +
                PhantomJS version was too old when in fact it did not start at all. [Issue #23]
         | 
| 209 | 
            +
             | 
| 210 | 
            +
            *   Ensure the `:timeout` option is actually used. [Issue #36]
         | 
| 211 | 
            +
             | 
| 212 | 
            +
            *   Nodes need to know which page they are associated with. Before this,
         | 
| 213 | 
            +
                if Javascript caused a new page to load, existing node references
         | 
| 214 | 
            +
                would be wrong, but wouldn't raise an ObsoleteNode error. [Issue #39]
         | 
| 215 | 
            +
             | 
| 216 | 
            +
            *   In some circumstances, we could end up missing an inline element
         | 
| 217 | 
            +
                when attempting to click it. This is due to the use of
         | 
| 218 | 
            +
                `getBoundingClientRect()`. We're now using `getClientRects()` to
         | 
| 219 | 
            +
                address this.
         | 
| 220 | 
            +
             | 
| 175 221 | 
             
            ### 0.4.0 ###
         | 
| 176 222 |  | 
| 177 223 | 
             
            *   Element click position is now calculated using the native
         | 
    
        data/lib/capybara/poltergeist.rb
    CHANGED
    
    | @@ -9,6 +9,9 @@ module Capybara | |
| 9 9 | 
             
                autoload :Server,          'capybara/poltergeist/server'
         | 
| 10 10 | 
             
                autoload :WebSocketServer, 'capybara/poltergeist/web_socket_server'
         | 
| 11 11 | 
             
                autoload :Client,          'capybara/poltergeist/client'
         | 
| 12 | 
            +
                autoload :Util,            'capybara/poltergeist/util'
         | 
| 13 | 
            +
                autoload :Inspector,       'capybara/poltergeist/inspector'
         | 
| 14 | 
            +
                autoload :Spawn,           'capybara/poltergeist/spawn'
         | 
| 12 15 |  | 
| 13 16 | 
             
                require 'capybara/poltergeist/errors'
         | 
| 14 17 | 
             
              end
         | 
| @@ -1,23 +1,13 @@ | |
| 1 | 
            -
            require ' | 
| 1 | 
            +
            require 'multi_json'
         | 
| 2 2 |  | 
| 3 3 | 
             
            module Capybara::Poltergeist
         | 
| 4 4 | 
             
              class Browser
         | 
| 5 | 
            -
                attr_reader : | 
| 5 | 
            +
                attr_reader :server, :client, :logger
         | 
| 6 6 |  | 
| 7 | 
            -
                 | 
| 8 | 
            -
             | 
| 9 | 
            -
             | 
| 10 | 
            -
                  @ | 
| 11 | 
            -
                  @server  = Server.new(options.fetch(:timeout, DEFAULT_TIMEOUT))
         | 
| 12 | 
            -
                  @client  = Client.start(server.port, options[:phantomjs])
         | 
| 13 | 
            -
                end
         | 
| 14 | 
            -
             | 
| 15 | 
            -
                def timeout
         | 
| 16 | 
            -
                  server.timeout
         | 
| 17 | 
            -
                end
         | 
| 18 | 
            -
             | 
| 19 | 
            -
                def timeout=(sec)
         | 
| 20 | 
            -
                  server.timeout = sec
         | 
| 7 | 
            +
                def initialize(server, client, logger = nil)
         | 
| 8 | 
            +
                  @server = server
         | 
| 9 | 
            +
                  @client = client
         | 
| 10 | 
            +
                  @logger = logger
         | 
| 21 11 | 
             
                end
         | 
| 22 12 |  | 
| 23 13 | 
             
                def restart
         | 
| @@ -25,7 +15,7 @@ module Capybara::Poltergeist | |
| 25 15 | 
             
                  client.restart
         | 
| 26 16 | 
             
                end
         | 
| 27 17 |  | 
| 28 | 
            -
                def visit(url | 
| 18 | 
            +
                def visit(url)
         | 
| 29 19 | 
             
                  command 'visit', url
         | 
| 30 20 | 
             
                end
         | 
| 31 21 |  | 
| @@ -41,36 +31,41 @@ module Capybara::Poltergeist | |
| 41 31 | 
             
                  command 'source'
         | 
| 42 32 | 
             
                end
         | 
| 43 33 |  | 
| 44 | 
            -
                def find(selector | 
| 45 | 
            -
                  command | 
| 34 | 
            +
                def find(selector)
         | 
| 35 | 
            +
                  result = command('find', selector)
         | 
| 36 | 
            +
                  result['ids'].map { |id| [result['page_id'], id] }
         | 
| 46 37 | 
             
                end
         | 
| 47 38 |  | 
| 48 | 
            -
                def  | 
| 49 | 
            -
                  command ' | 
| 39 | 
            +
                def find_within(page_id, id, selector)
         | 
| 40 | 
            +
                  command 'find_within', page_id, id, selector
         | 
| 50 41 | 
             
                end
         | 
| 51 42 |  | 
| 52 | 
            -
                def  | 
| 53 | 
            -
                  command ' | 
| 43 | 
            +
                def text(page_id, id)
         | 
| 44 | 
            +
                  command 'text', page_id, id
         | 
| 54 45 | 
             
                end
         | 
| 55 46 |  | 
| 56 | 
            -
                def  | 
| 57 | 
            -
                  command ' | 
| 47 | 
            +
                def attribute(page_id, id, name)
         | 
| 48 | 
            +
                  command 'attribute', page_id, id, name.to_s
         | 
| 58 49 | 
             
                end
         | 
| 59 50 |  | 
| 60 | 
            -
                def  | 
| 61 | 
            -
                  command ' | 
| 51 | 
            +
                def value(page_id, id)
         | 
| 52 | 
            +
                  command 'value', page_id, id
         | 
| 62 53 | 
             
                end
         | 
| 63 54 |  | 
| 64 | 
            -
                def  | 
| 65 | 
            -
                  command ' | 
| 55 | 
            +
                def set(page_id, id, value)
         | 
| 56 | 
            +
                  command 'set', page_id, id, value
         | 
| 66 57 | 
             
                end
         | 
| 67 58 |  | 
| 68 | 
            -
                def  | 
| 69 | 
            -
                  command | 
| 59 | 
            +
                def select_file(page_id, id, value)
         | 
| 60 | 
            +
                  command 'select_file', page_id, id, value
         | 
| 70 61 | 
             
                end
         | 
| 71 62 |  | 
| 72 | 
            -
                def  | 
| 73 | 
            -
                  command | 
| 63 | 
            +
                def tag_name(page_id, id)
         | 
| 64 | 
            +
                  command('tag_name', page_id, id).downcase
         | 
| 65 | 
            +
                end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                def visible?(page_id, id)
         | 
| 68 | 
            +
                  command 'visible', page_id, id
         | 
| 74 69 | 
             
                end
         | 
| 75 70 |  | 
| 76 71 | 
             
                def evaluate(script)
         | 
| @@ -87,24 +82,20 @@ module Capybara::Poltergeist | |
| 87 82 | 
             
                  command 'pop_frame'
         | 
| 88 83 | 
             
                end
         | 
| 89 84 |  | 
| 90 | 
            -
                def  | 
| 91 | 
            -
                   | 
| 85 | 
            +
                def click(page_id, id)
         | 
| 86 | 
            +
                  command 'click', page_id, id
         | 
| 92 87 | 
             
                end
         | 
| 93 88 |  | 
| 94 | 
            -
                def  | 
| 95 | 
            -
                  command ' | 
| 89 | 
            +
                def drag(page_id, id, other_id)
         | 
| 90 | 
            +
                  command 'drag', page_id, id, other_id
         | 
| 96 91 | 
             
                end
         | 
| 97 92 |  | 
| 98 | 
            -
                def  | 
| 99 | 
            -
                  command ' | 
| 93 | 
            +
                def select(page_id, id, value)
         | 
| 94 | 
            +
                  command 'select', page_id, id, value
         | 
| 100 95 | 
             
                end
         | 
| 101 96 |  | 
| 102 | 
            -
                def  | 
| 103 | 
            -
                  command ' | 
| 104 | 
            -
                end
         | 
| 105 | 
            -
             | 
| 106 | 
            -
                def trigger(id, event)
         | 
| 107 | 
            -
                  command 'trigger', id, event
         | 
| 97 | 
            +
                def trigger(page_id, id, event)
         | 
| 98 | 
            +
                  command 'trigger', page_id, id, event.to_s
         | 
| 108 99 | 
             
                end
         | 
| 109 100 |  | 
| 110 101 | 
             
                def reset
         | 
| @@ -119,23 +110,19 @@ module Capybara::Poltergeist | |
| 119 110 | 
             
                  command 'resize', width, height
         | 
| 120 111 | 
             
                end
         | 
| 121 112 |  | 
| 122 | 
            -
                def logger
         | 
| 123 | 
            -
                  options[:logger]
         | 
| 124 | 
            -
                end
         | 
| 125 | 
            -
             | 
| 126 | 
            -
                def log(message)
         | 
| 127 | 
            -
                  logger.puts message if logger
         | 
| 128 | 
            -
                end
         | 
| 129 | 
            -
             | 
| 130 113 | 
             
                def command(name, *args)
         | 
| 131 114 | 
             
                  message = { 'name' => name, 'args' => args }
         | 
| 132 115 | 
             
                  log message.inspect
         | 
| 133 116 |  | 
| 134 | 
            -
                  json =  | 
| 117 | 
            +
                  json = MultiJson.decode(server.send(MultiJson.encode(message)))
         | 
| 135 118 | 
             
                  log json.inspect
         | 
| 136 119 |  | 
| 137 120 | 
             
                  if json['error']
         | 
| 138 | 
            -
                     | 
| 121 | 
            +
                    if json['error']['name'] == 'Poltergeist.JavascriptError'
         | 
| 122 | 
            +
                      raise JavascriptError.new(json['error'])
         | 
| 123 | 
            +
                    else
         | 
| 124 | 
            +
                      raise BrowserError.new(json['error'])
         | 
| 125 | 
            +
                    end
         | 
| 139 126 | 
             
                  else
         | 
| 140 127 | 
             
                    json['response']
         | 
| 141 128 | 
             
                  end
         | 
| @@ -144,5 +131,11 @@ module Capybara::Poltergeist | |
| 144 131 | 
             
                  restart
         | 
| 145 132 | 
             
                  raise
         | 
| 146 133 | 
             
                end
         | 
| 134 | 
            +
             | 
| 135 | 
            +
                private
         | 
| 136 | 
            +
             | 
| 137 | 
            +
                def log(message)
         | 
| 138 | 
            +
                  logger.puts message if logger
         | 
| 139 | 
            +
                end
         | 
| 147 140 | 
             
              end
         | 
| 148 141 | 
             
            end
         | 
| @@ -1,11 +1,8 @@ | |
| 1 | 
            -
            require 'sfl'
         | 
| 2 | 
            -
             | 
| 3 1 | 
             
            module Capybara::Poltergeist
         | 
| 4 2 | 
             
              class Client
         | 
| 5 3 | 
             
                PHANTOMJS_SCRIPT  = File.expand_path('../client/compiled/main.js', __FILE__)
         | 
| 6 | 
            -
                PHANTOMJS_VERSION =  | 
| 7 | 
            -
             | 
| 8 | 
            -
                attr_reader :pid, :port, :path
         | 
| 4 | 
            +
                PHANTOMJS_VERSION = '1.4.1'
         | 
| 5 | 
            +
                PHANTOMJS_NAME    = 'phantomjs'
         | 
| 9 6 |  | 
| 10 7 | 
             
                def self.start(*args)
         | 
| 11 8 | 
             
                  client = new(*args)
         | 
| @@ -13,19 +10,33 @@ module Capybara::Poltergeist | |
| 13 10 | 
             
                  client
         | 
| 14 11 | 
             
                end
         | 
| 15 12 |  | 
| 16 | 
            -
                 | 
| 17 | 
            -
             | 
| 18 | 
            -
             | 
| 19 | 
            -
                   | 
| 13 | 
            +
                attr_reader :pid, :port, :path, :inspector
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                def initialize(port, inspector = nil, path = nil)
         | 
| 16 | 
            +
                  @port      = port
         | 
| 17 | 
            +
                  @inspector = inspector
         | 
| 18 | 
            +
                  @path      = path || PHANTOMJS_NAME
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  pid = Process.pid
         | 
| 21 | 
            +
                  at_exit { stop if Process.pid == pid }
         | 
| 20 22 | 
             
                end
         | 
| 21 23 |  | 
| 22 24 | 
             
                def start
         | 
| 23 25 | 
             
                  check_phantomjs_version
         | 
| 24 | 
            -
                  @pid =  | 
| 26 | 
            +
                  @pid = Spawn.spawn(*command)
         | 
| 25 27 | 
             
                end
         | 
| 26 28 |  | 
| 27 29 | 
             
                def stop
         | 
| 28 | 
            -
                   | 
| 30 | 
            +
                  if pid
         | 
| 31 | 
            +
                    begin
         | 
| 32 | 
            +
                      Process.kill('TERM', pid)
         | 
| 33 | 
            +
                      Process.wait(pid)
         | 
| 34 | 
            +
                    rescue Errno::ESRCH, Errno::ECHILD
         | 
| 35 | 
            +
                      # Zed's dead, baby
         | 
| 36 | 
            +
                    end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                    @pid = nil
         | 
| 39 | 
            +
                  end
         | 
| 29 40 | 
             
                end
         | 
| 30 41 |  | 
| 31 42 | 
             
                def restart
         | 
| @@ -33,15 +44,34 @@ module Capybara::Poltergeist | |
| 33 44 | 
             
                  start
         | 
| 34 45 | 
             
                end
         | 
| 35 46 |  | 
| 47 | 
            +
                def command
         | 
| 48 | 
            +
                  @command ||= begin
         | 
| 49 | 
            +
                    parts = [path]
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                    if inspector
         | 
| 52 | 
            +
                      parts << "--remote-debugger-port=#{inspector.port}"
         | 
| 53 | 
            +
                      parts << "--remote-debugger-autorun=yes"
         | 
| 54 | 
            +
                    end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                    parts << PHANTOMJS_SCRIPT
         | 
| 57 | 
            +
                    parts << port
         | 
| 58 | 
            +
                    parts
         | 
| 59 | 
            +
                  end
         | 
| 60 | 
            +
                end
         | 
| 61 | 
            +
             | 
| 36 62 | 
             
                private
         | 
| 37 63 |  | 
| 38 64 | 
             
                def check_phantomjs_version
         | 
| 39 65 | 
             
                  return if @phantomjs_version_checked
         | 
| 40 66 |  | 
| 41 67 | 
             
                  version = `#{path} --version`.chomp
         | 
| 42 | 
            -
             | 
| 68 | 
            +
             | 
| 69 | 
            +
                  if $? != 0
         | 
| 70 | 
            +
                    raise PhantomJSFailed.new($?)
         | 
| 71 | 
            +
                  elsif version < PHANTOMJS_VERSION
         | 
| 43 72 | 
             
                    raise PhantomJSTooOld.new(version)
         | 
| 44 73 | 
             
                  end
         | 
| 74 | 
            +
             | 
| 45 75 | 
             
                  @phantomjs_version_checked = true
         | 
| 46 76 | 
             
                end
         | 
| 47 77 | 
             
              end
         | 
| @@ -7,18 +7,28 @@ class PoltergeistAgent | |
| 7 7 | 
             
                @windows  = []
         | 
| 8 8 | 
             
                this.pushWindow(window)
         | 
| 9 9 |  | 
| 10 | 
            +
              externalCall: (name, arguments) ->
         | 
| 11 | 
            +
                try
         | 
| 12 | 
            +
                  { value: this[name].apply(this, arguments) }
         | 
| 13 | 
            +
                catch error
         | 
| 14 | 
            +
                  { error: error.toString() }
         | 
| 15 | 
            +
             | 
| 10 16 | 
             
              pushWindow: (new_window) ->
         | 
| 11 17 | 
             
                @windows.push(new_window)
         | 
| 12 18 |  | 
| 13 19 | 
             
                @window   = new_window
         | 
| 14 20 | 
             
                @document = @window.document
         | 
| 15 21 |  | 
| 22 | 
            +
                null
         | 
| 23 | 
            +
             | 
| 16 24 | 
             
              popWindow: ->
         | 
| 17 25 | 
             
                @windows.pop()
         | 
| 18 26 |  | 
| 19 27 | 
             
                @window   = @windows[@windows.length - 1]
         | 
| 20 28 | 
             
                @document = @window.document
         | 
| 21 29 |  | 
| 30 | 
            +
                null
         | 
| 31 | 
            +
             | 
| 22 32 | 
             
              pushFrame: (id) ->
         | 
| 23 33 | 
             
                this.pushWindow @document.getElementById(id).contentWindow
         | 
| 24 34 |  | 
| @@ -28,9 +38,8 @@ class PoltergeistAgent | |
| 28 38 | 
             
              currentUrl: ->
         | 
| 29 39 | 
             
                window.location.toString()
         | 
| 30 40 |  | 
| 31 | 
            -
              find: (selector,  | 
| 32 | 
            -
                 | 
| 33 | 
            -
                results = @document.evaluate(selector, context, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null)
         | 
| 41 | 
            +
              find: (selector, within = @document) ->
         | 
| 42 | 
            +
                results = @document.evaluate(selector, within, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null)
         | 
| 34 43 | 
             
                ids     = []
         | 
| 35 44 |  | 
| 36 45 | 
             
                for i in [0...results.snapshotLength]
         | 
| @@ -51,8 +60,12 @@ class PoltergeistAgent | |
| 51 60 |  | 
| 52 61 | 
             
              nodeCall: (id, name, arguments) ->
         | 
| 53 62 | 
             
                node = this.get(id)
         | 
| 63 | 
            +
                throw new PoltergeistAgent.ObsoleteNode if node.isObsolete()
         | 
| 54 64 | 
             
                node[name].apply(node, arguments)
         | 
| 55 65 |  | 
| 66 | 
            +
            class PoltergeistAgent.ObsoleteNode
         | 
| 67 | 
            +
              toString: -> "PoltergeistAgent.ObsoleteNode"
         | 
| 68 | 
            +
             | 
| 56 69 | 
             
            class PoltergeistAgent.Node
         | 
| 57 70 | 
             
              @EVENTS = {
         | 
| 58 71 | 
             
                FOCUS: ['blur', 'focus', 'focusin', 'focusout'],
         | 
| @@ -65,6 +78,9 @@ class PoltergeistAgent.Node | |
| 65 78 | 
             
              parentId: ->
         | 
| 66 79 | 
             
                @agent.register(@element.parentNode)
         | 
| 67 80 |  | 
| 81 | 
            +
              find: (selector) ->
         | 
| 82 | 
            +
                @agent.find(selector, @element)
         | 
| 83 | 
            +
             | 
| 68 84 | 
             
              isObsolete: ->
         | 
| 69 85 | 
             
                obsolete = (element) =>
         | 
| 70 86 | 
             
                  if element.parentNode?
         | 
| @@ -151,8 +167,16 @@ class PoltergeistAgent.Node | |
| 151 167 | 
             
                  true
         | 
| 152 168 |  | 
| 153 169 | 
             
              position: ->
         | 
| 154 | 
            -
                rect = @element. | 
| 155 | 
            -
             | 
| 170 | 
            +
                rect = @element.getClientRects()[0]
         | 
| 171 | 
            +
             | 
| 172 | 
            +
                {
         | 
| 173 | 
            +
                  top:    rect.top,
         | 
| 174 | 
            +
                  right:  rect.right,
         | 
| 175 | 
            +
                  left:   rect.left,
         | 
| 176 | 
            +
                  bottom: rect.bottom,
         | 
| 177 | 
            +
                  width:  rect.width,
         | 
| 178 | 
            +
                  height: rect.height
         | 
| 179 | 
            +
                }
         | 
| 156 180 |  | 
| 157 181 | 
             
              trigger: (name) ->
         | 
| 158 182 | 
             
                if Node.EVENTS.MOUSE.indexOf(name) != -1
         | 
| @@ -169,6 +193,25 @@ class PoltergeistAgent.Node | |
| 169 193 |  | 
| 170 194 | 
             
                @element.dispatchEvent(event)
         | 
| 171 195 |  | 
| 196 | 
            +
              clickTest: (x, y) ->
         | 
| 197 | 
            +
                el = origEl = document.elementFromPoint(x, y)
         | 
| 198 | 
            +
             | 
| 199 | 
            +
                while el
         | 
| 200 | 
            +
                  if el == @element
         | 
| 201 | 
            +
                    return { status: 'success' }
         | 
| 202 | 
            +
                  else
         | 
| 203 | 
            +
                    el = el.parentNode
         | 
| 204 | 
            +
             | 
| 205 | 
            +
                { status: 'failure', selector: origEl && this.getSelector(origEl) }
         | 
| 206 | 
            +
             | 
| 207 | 
            +
              getSelector: (el) ->
         | 
| 208 | 
            +
                selector = if el.tagName != 'HTML' then this.getSelector(el.parentNode) + ' ' else ''
         | 
| 209 | 
            +
                selector += el.tagName.toLowerCase()
         | 
| 210 | 
            +
                selector += "##{el.id}" if el.id
         | 
| 211 | 
            +
                for className in el.classList
         | 
| 212 | 
            +
                  selector += ".#{className}"
         | 
| 213 | 
            +
                selector
         | 
| 214 | 
            +
             | 
| 172 215 | 
             
            window.__poltergeist = new PoltergeistAgent
         | 
| 173 216 |  | 
| 174 217 | 
             
            document.addEventListener(
         | 
| @@ -1,11 +1,12 @@ | |
| 1 1 | 
             
            class Poltergeist.Browser
         | 
| 2 2 | 
             
              constructor: (@owner) ->
         | 
| 3 | 
            -
                @state | 
| 3 | 
            +
                @state   = 'default'
         | 
| 4 | 
            +
                @page_id = 0
         | 
| 5 | 
            +
             | 
| 4 6 | 
             
                this.resetPage()
         | 
| 5 7 |  | 
| 6 8 | 
             
              resetPage: ->
         | 
| 7 9 | 
             
                @page.release() if @page?
         | 
| 8 | 
            -
             | 
| 9 10 | 
             
                @page = new Poltergeist.WebPage
         | 
| 10 11 |  | 
| 11 12 | 
             
                @page.onLoadStarted = =>
         | 
| @@ -13,37 +14,58 @@ class Poltergeist.Browser | |
| 13 14 |  | 
| 14 15 | 
             
                @page.onLoadFinished = (status) =>
         | 
| 15 16 | 
             
                  if @state == 'loading'
         | 
| 16 | 
            -
                     | 
| 17 | 
            +
                    this.sendResponse(status)
         | 
| 17 18 | 
             
                    @state = 'default'
         | 
| 18 19 |  | 
| 20 | 
            +
                @page.onInitialized = =>
         | 
| 21 | 
            +
                  @page_id += 1
         | 
| 22 | 
            +
             | 
| 23 | 
            +
              sendResponse: (response) ->
         | 
| 24 | 
            +
                errors = @page.errors()
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                if errors.length > 0
         | 
| 27 | 
            +
                  @page.clearErrors()
         | 
| 28 | 
            +
                  @owner.sendError(new Poltergeist.JavascriptError(errors))
         | 
| 29 | 
            +
                else
         | 
| 30 | 
            +
                  @owner.sendResponse(response)
         | 
| 31 | 
            +
             | 
| 32 | 
            +
              node: (page_id, id) ->
         | 
| 33 | 
            +
                if page_id == @page_id
         | 
| 34 | 
            +
                  @page.get(id)
         | 
| 35 | 
            +
                else
         | 
| 36 | 
            +
                  throw new Poltergeist.ObsoleteNode
         | 
| 37 | 
            +
             | 
| 19 38 | 
             
              visit: (url) ->
         | 
| 20 39 | 
             
                @state = 'loading'
         | 
| 21 40 | 
             
                @page.open(url)
         | 
| 22 41 |  | 
| 23 42 | 
             
              current_url: ->
         | 
| 24 | 
            -
                 | 
| 43 | 
            +
                this.sendResponse @page.currentUrl()
         | 
| 25 44 |  | 
| 26 45 | 
             
              body: ->
         | 
| 27 | 
            -
                 | 
| 46 | 
            +
                this.sendResponse @page.content()
         | 
| 28 47 |  | 
| 29 48 | 
             
              source: ->
         | 
| 30 | 
            -
                 | 
| 49 | 
            +
                this.sendResponse @page.source()
         | 
| 50 | 
            +
             | 
| 51 | 
            +
              find: (selector) ->
         | 
| 52 | 
            +
                this.sendResponse(page_id: @page_id, ids: @page.find(selector))
         | 
| 31 53 |  | 
| 32 | 
            -
               | 
| 33 | 
            -
                 | 
| 54 | 
            +
              find_within: (page_id, id, selector) ->
         | 
| 55 | 
            +
                this.sendResponse this.node(page_id, id).find(selector)
         | 
| 34 56 |  | 
| 35 | 
            -
              text: (id) ->
         | 
| 36 | 
            -
                 | 
| 57 | 
            +
              text: (page_id, id) ->
         | 
| 58 | 
            +
                this.sendResponse this.node(page_id, id).text()
         | 
| 37 59 |  | 
| 38 | 
            -
              attribute: (id, name) ->
         | 
| 39 | 
            -
                 | 
| 60 | 
            +
              attribute: (page_id, id, name) ->
         | 
| 61 | 
            +
                this.sendResponse this.node(page_id, id).getAttribute(name)
         | 
| 40 62 |  | 
| 41 | 
            -
              value: (id) ->
         | 
| 42 | 
            -
                 | 
| 63 | 
            +
              value: (page_id, id) ->
         | 
| 64 | 
            +
                this.sendResponse this.node(page_id, id).value()
         | 
| 43 65 |  | 
| 44 | 
            -
              set: (id, value) ->
         | 
| 45 | 
            -
                 | 
| 46 | 
            -
                 | 
| 66 | 
            +
              set: (page_id, id, value) ->
         | 
| 67 | 
            +
                this.node(page_id, id).set(value)
         | 
| 68 | 
            +
                this.sendResponse(true)
         | 
| 47 69 |  | 
| 48 70 | 
             
              # PhantomJS only allows us to reference the element by CSS selector, not XPath,
         | 
| 49 71 | 
             
              # so we have to add an attribute to the element to identify it, then remove it
         | 
| @@ -52,8 +74,8 @@ class Poltergeist.Browser | |
| 52 74 | 
             
              # PhantomJS does not support multiple-file inputs, so we have to blatently cheat
         | 
| 53 75 | 
             
              # by temporarily changing it to a single-file input. This obviously could break
         | 
| 54 76 | 
             
              # things in various ways, which is not ideal, but it works in the simplest case.
         | 
| 55 | 
            -
              select_file: (id, value) ->
         | 
| 56 | 
            -
                element =  | 
| 77 | 
            +
              select_file: (page_id, id, value) ->
         | 
| 78 | 
            +
                element = this.node(page_id, id)
         | 
| 57 79 |  | 
| 58 80 | 
             
                multiple = element.isMultiple()
         | 
| 59 81 |  | 
| @@ -65,38 +87,38 @@ class Poltergeist.Browser | |
| 65 87 | 
             
                element.removeAttribute('_poltergeist_selected')
         | 
| 66 88 | 
             
                element.setAttribute('multiple', 'multiple') if multiple
         | 
| 67 89 |  | 
| 68 | 
            -
                 | 
| 90 | 
            +
                this.sendResponse(true)
         | 
| 69 91 |  | 
| 70 | 
            -
              select: (id, value) ->
         | 
| 71 | 
            -
                 | 
| 92 | 
            +
              select: (page_id, id, value) ->
         | 
| 93 | 
            +
                this.sendResponse this.node(page_id, id).select(value)
         | 
| 72 94 |  | 
| 73 | 
            -
              tag_name: (id) ->
         | 
| 74 | 
            -
                 | 
| 95 | 
            +
              tag_name: (page_id, id) ->
         | 
| 96 | 
            +
                this.sendResponse this.node(page_id, id).tagName()
         | 
| 75 97 |  | 
| 76 | 
            -
              visible: (id) ->
         | 
| 77 | 
            -
                 | 
| 98 | 
            +
              visible: (page_id, id) ->
         | 
| 99 | 
            +
                this.sendResponse this.node(page_id, id).isVisible()
         | 
| 78 100 |  | 
| 79 101 | 
             
              evaluate: (script) ->
         | 
| 80 | 
            -
                 | 
| 102 | 
            +
                this.sendResponse JSON.parse(@page.evaluate("function() { return JSON.stringify(#{script}) }"))
         | 
| 81 103 |  | 
| 82 104 | 
             
              execute: (script) ->
         | 
| 83 105 | 
             
                @page.execute("function() { #{script} }")
         | 
| 84 | 
            -
                 | 
| 106 | 
            +
                this.sendResponse(true)
         | 
| 85 107 |  | 
| 86 108 | 
             
              push_frame: (id) ->
         | 
| 87 109 | 
             
                @page.pushFrame(id)
         | 
| 88 | 
            -
                 | 
| 110 | 
            +
                this.sendResponse(true)
         | 
| 89 111 |  | 
| 90 112 | 
             
              pop_frame: ->
         | 
| 91 113 | 
             
                @page.popFrame()
         | 
| 92 | 
            -
                 | 
| 114 | 
            +
                this.sendResponse(true)
         | 
| 93 115 |  | 
| 94 | 
            -
              click: (id) ->
         | 
| 116 | 
            +
              click: (page_id, id) ->
         | 
| 95 117 | 
             
                # If the click event triggers onLoadStarted, we will transition to the 'loading'
         | 
| 96 118 | 
             
                # state and wait for onLoadFinished before sending a response.
         | 
| 97 119 | 
             
                @state = 'clicked'
         | 
| 98 120 |  | 
| 99 | 
            -
                 | 
| 121 | 
            +
                this.node(page_id, id).click()
         | 
| 100 122 |  | 
| 101 123 | 
             
                # Use a timeout in order to let the stack clear, so that the @page.onLoadStarted
         | 
| 102 124 | 
             
                # callback can (possibly) fire, before we decide whether to send a response.
         | 
| @@ -104,22 +126,22 @@ class Poltergeist.Browser | |
| 104 126 | 
             
                  =>
         | 
| 105 127 | 
             
                    if @state == 'clicked'
         | 
| 106 128 | 
             
                      @state = 'default'
         | 
| 107 | 
            -
                       | 
| 129 | 
            +
                      this.sendResponse(true)
         | 
| 108 130 | 
             
                  ,
         | 
| 109 131 | 
             
                  10
         | 
| 110 132 | 
             
                )
         | 
| 111 133 |  | 
| 112 | 
            -
              drag: (id, other_id) ->
         | 
| 113 | 
            -
                 | 
| 114 | 
            -
                 | 
| 134 | 
            +
              drag: (page_id, id, other_id) ->
         | 
| 135 | 
            +
                this.node(page_id, id).dragTo(@page.get(other_id))
         | 
| 136 | 
            +
                this.sendResponse(true)
         | 
| 115 137 |  | 
| 116 | 
            -
              trigger: (id, event) ->
         | 
| 117 | 
            -
                 | 
| 118 | 
            -
                 | 
| 138 | 
            +
              trigger: (page_id, id, event) ->
         | 
| 139 | 
            +
                this.node(page_id, id).trigger(event)
         | 
| 140 | 
            +
                this.sendResponse(event)
         | 
| 119 141 |  | 
| 120 142 | 
             
              reset: ->
         | 
| 121 143 | 
             
                this.resetPage()
         | 
| 122 | 
            -
                 | 
| 144 | 
            +
                this.sendResponse(true)
         | 
| 123 145 |  | 
| 124 146 | 
             
              render: (path, full) ->
         | 
| 125 147 | 
             
                dimensions = @page.validatedDimensions()
         | 
| @@ -135,11 +157,11 @@ class Poltergeist.Browser | |
| 135 157 | 
             
                  @page.setClipRect(left: 0, top: 0, width: viewport.width, height: viewport.height)
         | 
| 136 158 | 
             
                  @page.render(path)
         | 
| 137 159 |  | 
| 138 | 
            -
                 | 
| 160 | 
            +
                this.sendResponse(true)
         | 
| 139 161 |  | 
| 140 162 | 
             
              resize: (width, height) ->
         | 
| 141 163 | 
             
                @page.setViewportSize(width: width, height: height)
         | 
| 142 | 
            -
                 | 
| 164 | 
            +
                this.sendResponse(true)
         | 
| 143 165 |  | 
| 144 166 | 
             
              exit: ->
         | 
| 145 167 | 
             
                phantom.exit()
         |