capybara-chrome 0.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +5 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +110 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/capybara-chrome.gemspec +31 -0
- data/lib/capybara-chrome.rb +1 -0
- data/lib/capybara/chrome.rb +56 -0
- data/lib/capybara/chrome/browser.rb +393 -0
- data/lib/capybara/chrome/configuration.rb +77 -0
- data/lib/capybara/chrome/debug.rb +17 -0
- data/lib/capybara/chrome/driver.rb +38 -0
- data/lib/capybara/chrome/errors.rb +15 -0
- data/lib/capybara/chrome/node.rb +343 -0
- data/lib/capybara/chrome/rdp_client.rb +204 -0
- data/lib/capybara/chrome/rdp_socket.rb +29 -0
- data/lib/capybara/chrome/rdp_web_socket_client.rb +51 -0
- data/lib/capybara/chrome/repeat_timeout.rb +15 -0
- data/lib/capybara/chrome/service.rb +109 -0
- data/lib/capybara/chrome/version.rb +5 -0
- data/lib/chrome_remote_helper.js +340 -0
- metadata +154 -0
| @@ -0,0 +1,204 @@ | |
| 1 | 
            +
            module Capybara::Chrome
         | 
| 2 | 
            +
             | 
| 3 | 
            +
              # Chrome Remote Debugging Protocol (RDP) Client
         | 
| 4 | 
            +
              class RDPClient
         | 
| 5 | 
            +
                require "open-uri"
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                include Debug
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                attr_reader :response_events, :response_messages, :loader_ids, :listen_mutex, :handler_calls, :ws, :handlers, :browser
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                def initialize(chrome_host:, chrome_port:, browser:)
         | 
| 12 | 
            +
                  @chrome_host = chrome_host
         | 
| 13 | 
            +
                  @chrome_port = chrome_port
         | 
| 14 | 
            +
                  @browser = browser
         | 
| 15 | 
            +
                  @last_id = 0
         | 
| 16 | 
            +
                  @ws = nil
         | 
| 17 | 
            +
                  @ws_thread = nil
         | 
| 18 | 
            +
                  @ws_mutex = Mutex.new
         | 
| 19 | 
            +
                  @handlers = Hash.new { |hash, key| hash[key] = [] }
         | 
| 20 | 
            +
                  @listen_mutex = Mutex.new
         | 
| 21 | 
            +
                  @response_messages = {}
         | 
| 22 | 
            +
                  @response_events = []
         | 
| 23 | 
            +
                  @read_mutex = Mutex.new
         | 
| 24 | 
            +
                  @handler_mutex = Mutex.new
         | 
| 25 | 
            +
                  @loader_ids = []
         | 
| 26 | 
            +
                  @handler_calls = []
         | 
| 27 | 
            +
                end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                def reset
         | 
| 30 | 
            +
                  @calling_handlers = false
         | 
| 31 | 
            +
                  response_messages.clear
         | 
| 32 | 
            +
                  response_events.clear
         | 
| 33 | 
            +
                  loader_ids.clear
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                def generate_unique_id
         | 
| 37 | 
            +
                  @last_id += 1
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                def send_cmd!(command, params={})
         | 
| 41 | 
            +
                  debug command, params
         | 
| 42 | 
            +
                  msg_id = generate_unique_id
         | 
| 43 | 
            +
                  send_msg({method: command, params: params, id: msg_id}.to_json)
         | 
| 44 | 
            +
                  msg_id
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                # Errno::EPIPE
         | 
| 48 | 
            +
                def send_cmd(command, params={})
         | 
| 49 | 
            +
                  msg_id = send_cmd!(command, params)
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                  debug "waiting #{command} #{msg_id}"
         | 
| 52 | 
            +
                  msg = nil
         | 
| 53 | 
            +
                  begin
         | 
| 54 | 
            +
                    until msg = @response_messages[msg_id]
         | 
| 55 | 
            +
                      read_and_process(1)
         | 
| 56 | 
            +
                    end
         | 
| 57 | 
            +
                    @response_messages.delete msg_id
         | 
| 58 | 
            +
                  rescue Timeout::Error
         | 
| 59 | 
            +
                    puts "TimeoutError #{command} #{params.inspect} #{msg_id}"
         | 
| 60 | 
            +
                    send_cmd! "Runtime.terminateExecution"
         | 
| 61 | 
            +
                    puts "Recovering"
         | 
| 62 | 
            +
                    recover_chrome_crash
         | 
| 63 | 
            +
                    raise ResponseTimeoutError
         | 
| 64 | 
            +
                  rescue WebSocketError => e
         | 
| 65 | 
            +
                    puts "send_cmd received websocket error #{e.inspect}"
         | 
| 66 | 
            +
                    recover_chrome_crash
         | 
| 67 | 
            +
                    raise e
         | 
| 68 | 
            +
                  rescue Errno::EPIPE, EOFError => e
         | 
| 69 | 
            +
                    puts "send_cmd received EPIPE or EOF error #{e.inspect}"
         | 
| 70 | 
            +
                    recover_chrome_crash
         | 
| 71 | 
            +
                    raise e
         | 
| 72 | 
            +
                  rescue => e
         | 
| 73 | 
            +
                    puts "send_cmd caught error #{e.inspect} when issuing #{command} #{params.inspect}"
         | 
| 74 | 
            +
                    puts caller
         | 
| 75 | 
            +
                    raise e
         | 
| 76 | 
            +
                  end
         | 
| 77 | 
            +
                  return msg["result"]
         | 
| 78 | 
            +
                end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                def recover_chrome_crash
         | 
| 81 | 
            +
                  $stderr.puts "Chrome Crashed... #{Capybara::Chrome.wants_to_quit.inspect} #{::RSpec.wants_to_quit.inspect}" unless Capybara::Chrome.wants_to_quit
         | 
| 82 | 
            +
                  browser.restart_chrome
         | 
| 83 | 
            +
                  browser.start_remote
         | 
| 84 | 
            +
                end
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                def send_msg(msg)
         | 
| 87 | 
            +
                  retries ||= 0
         | 
| 88 | 
            +
                  ws.send_msg(msg)
         | 
| 89 | 
            +
                rescue Errno::EPIPE, EOFError => exception
         | 
| 90 | 
            +
                  retries += 1
         | 
| 91 | 
            +
                  recover_chrome_crash
         | 
| 92 | 
            +
                  if retries < 5 && !::Capybara::Chrome.wants_to_quit
         | 
| 93 | 
            +
                    retry
         | 
| 94 | 
            +
                  else
         | 
| 95 | 
            +
                    raise exception
         | 
| 96 | 
            +
                  end
         | 
| 97 | 
            +
                end
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                def on(event_name, &block)
         | 
| 100 | 
            +
                  handlers[event_name] << block
         | 
| 101 | 
            +
                end
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                def wait_for(event_name, timeout=Capybara.default_max_wait_time)
         | 
| 104 | 
            +
                  @response_events.clear
         | 
| 105 | 
            +
                  msg = nil
         | 
| 106 | 
            +
                  loop do
         | 
| 107 | 
            +
                    msgs = @response_events.select {|v| v["method"] == event_name}
         | 
| 108 | 
            +
                    if msgs.any?
         | 
| 109 | 
            +
                      if block_given?
         | 
| 110 | 
            +
                        do_return = msgs.detect do |m|
         | 
| 111 | 
            +
                          val = yield m["params"]
         | 
| 112 | 
            +
                        end
         | 
| 113 | 
            +
                        if do_return
         | 
| 114 | 
            +
                          msg = do_return.dup
         | 
| 115 | 
            +
                          @response_events.delete do_return
         | 
| 116 | 
            +
                          break
         | 
| 117 | 
            +
                        else
         | 
| 118 | 
            +
                          read_and_process(1)
         | 
| 119 | 
            +
                          next
         | 
| 120 | 
            +
                        end
         | 
| 121 | 
            +
                      else
         | 
| 122 | 
            +
                        msg = msgs.first.dup
         | 
| 123 | 
            +
                        msgs.each {|m| @response_events.delete m}
         | 
| 124 | 
            +
                        break
         | 
| 125 | 
            +
                      end
         | 
| 126 | 
            +
                    else
         | 
| 127 | 
            +
                    end
         | 
| 128 | 
            +
                    read_and_process(1)
         | 
| 129 | 
            +
                  end
         | 
| 130 | 
            +
                  return msg && msg["params"]
         | 
| 131 | 
            +
                rescue Timeout::Error
         | 
| 132 | 
            +
                  nil
         | 
| 133 | 
            +
                end
         | 
| 134 | 
            +
             | 
| 135 | 
            +
                def process_messages
         | 
| 136 | 
            +
                  n = 0
         | 
| 137 | 
            +
                  while @ws.messages.any? do
         | 
| 138 | 
            +
                    n += 1
         | 
| 139 | 
            +
                    msg_raw = @ws.messages.shift
         | 
| 140 | 
            +
                    if msg_raw
         | 
| 141 | 
            +
                      msg = JSON.parse(msg_raw)
         | 
| 142 | 
            +
                      if msg["method"]
         | 
| 143 | 
            +
                        hs = handlers[msg["method"]]
         | 
| 144 | 
            +
                        if hs.any?
         | 
| 145 | 
            +
                          @handler_calls << [msg["method"], msg["params"]]
         | 
| 146 | 
            +
                        end
         | 
| 147 | 
            +
                        @response_events << msg
         | 
| 148 | 
            +
                      else
         | 
| 149 | 
            +
                        @response_messages[msg["id"]] = msg
         | 
| 150 | 
            +
                        if msg["exceptionDetails"]
         | 
| 151 | 
            +
                          puts JSException.new(val["exceptionDetails"]["exception"].inspect)
         | 
| 152 | 
            +
                        end
         | 
| 153 | 
            +
                      end
         | 
| 154 | 
            +
                    else
         | 
| 155 | 
            +
                      p ["no msg_raw", msg_raw]
         | 
| 156 | 
            +
                    end
         | 
| 157 | 
            +
                  end
         | 
| 158 | 
            +
                  n
         | 
| 159 | 
            +
                end
         | 
| 160 | 
            +
             | 
| 161 | 
            +
                def read_and_process(timeout=0)
         | 
| 162 | 
            +
                  return unless Thread.current == Thread.main
         | 
| 163 | 
            +
                  ready = select [@ws.socket.io], [], [], timeout
         | 
| 164 | 
            +
                  if ready
         | 
| 165 | 
            +
                    @ws.parse_input
         | 
| 166 | 
            +
                    process_messages
         | 
| 167 | 
            +
                  end
         | 
| 168 | 
            +
                  if !@calling_handlers
         | 
| 169 | 
            +
                    @calling_handlers = true
         | 
| 170 | 
            +
                    while obj = @handler_calls.shift do
         | 
| 171 | 
            +
                      handlers[obj[0]].each {|h| h.call obj[1]}
         | 
| 172 | 
            +
                    end
         | 
| 173 | 
            +
                    @calling_handlers = false
         | 
| 174 | 
            +
                  end
         | 
| 175 | 
            +
                end
         | 
| 176 | 
            +
             | 
| 177 | 
            +
                def discover_ws_url
         | 
| 178 | 
            +
                  response = open("http://#{@chrome_host}:#{@chrome_port}/json")
         | 
| 179 | 
            +
                  data = JSON.parse(response.read)
         | 
| 180 | 
            +
                  first_page = data.detect {|e| e["type"] == "page"}
         | 
| 181 | 
            +
                  @ws_url = first_page["webSocketDebuggerUrl"]
         | 
| 182 | 
            +
                end
         | 
| 183 | 
            +
             | 
| 184 | 
            +
                def start
         | 
| 185 | 
            +
                  browser.wait_for_chrome
         | 
| 186 | 
            +
                  browser.with_retry do
         | 
| 187 | 
            +
                    discover_ws_url
         | 
| 188 | 
            +
                  end
         | 
| 189 | 
            +
                  @ws = RDPWebSocketClient.new @ws_url
         | 
| 190 | 
            +
                  send_cmd! "Network.enable"
         | 
| 191 | 
            +
                  send_cmd! "Network.clearBrowserCookies"
         | 
| 192 | 
            +
                  send_cmd! "Page.enable"
         | 
| 193 | 
            +
                  send_cmd! "DOM.enable"
         | 
| 194 | 
            +
                  send_cmd! "CSS.enable"
         | 
| 195 | 
            +
                  send_cmd! "Page.setDownloadBehavior", behavior: "allow", downloadPath: Capybara::Chrome.configuration.download_path
         | 
| 196 | 
            +
                  helper_js = File.expand_path(File.join("..", "..", "chrome_remote_helper.js"), File.dirname(__FILE__))
         | 
| 197 | 
            +
                  send_cmd! "Page.addScriptToEvaluateOnNewDocument", source: File.read(helper_js)
         | 
| 198 | 
            +
             | 
| 199 | 
            +
                  Thread.abort_on_exception = true
         | 
| 200 | 
            +
                  return
         | 
| 201 | 
            +
                end
         | 
| 202 | 
            +
              end
         | 
| 203 | 
            +
             | 
| 204 | 
            +
            end
         | 
| @@ -0,0 +1,29 @@ | |
| 1 | 
            +
            module Capybara::Chrome
         | 
| 2 | 
            +
              class RDPSocket
         | 
| 3 | 
            +
                READ_LEN = 4096
         | 
| 4 | 
            +
                attr_reader :url, :io
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                def initialize(url)
         | 
| 7 | 
            +
                  uri = URI.parse(url)
         | 
| 8 | 
            +
                  @url = uri.to_s
         | 
| 9 | 
            +
                  @io = TCPSocket.new(uri.host, uri.port)
         | 
| 10 | 
            +
                end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                def write(data)
         | 
| 13 | 
            +
                  io.sendmsg data
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                def read
         | 
| 17 | 
            +
                  buf = ""
         | 
| 18 | 
            +
                  loop do
         | 
| 19 | 
            +
                    buf << io.recv_nonblock(READ_LEN)
         | 
| 20 | 
            +
                  end
         | 
| 21 | 
            +
                rescue IO::EAGAINWaitReadable
         | 
| 22 | 
            +
                  if buf.size == 0
         | 
| 23 | 
            +
                    puts "buf is #{buf.size}"
         | 
| 24 | 
            +
                    puts caller[0..10]
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
                  buf
         | 
| 27 | 
            +
                end
         | 
| 28 | 
            +
              end
         | 
| 29 | 
            +
            end
         | 
| @@ -0,0 +1,51 @@ | |
| 1 | 
            +
            module Capybara::Chrome
         | 
| 2 | 
            +
              class RDPWebSocketClient
         | 
| 3 | 
            +
                attr_reader :socket, :driver, :messages, :status
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                def initialize(url)
         | 
| 6 | 
            +
                  @socket = RDPSocket.new(url)
         | 
| 7 | 
            +
                  @driver = ::WebSocket::Driver.client(socket)
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                  @messages = []
         | 
| 10 | 
            +
                  @status = :closed
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  setup_driver
         | 
| 13 | 
            +
                  start_driver
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                def send_msg(msg)
         | 
| 17 | 
            +
                  driver.text msg
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                def parse_input
         | 
| 21 | 
            +
                  @driver.parse(@socket.read)
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                private
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                def setup_driver
         | 
| 27 | 
            +
                  driver.on(:message) do |e|
         | 
| 28 | 
            +
                    messages << e.data
         | 
| 29 | 
            +
                  end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  driver.on(:error) do |e|
         | 
| 32 | 
            +
                    raise WebSocketError.new e.message
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                  driver.on(:close) do |e|
         | 
| 36 | 
            +
                    raise "closed"
         | 
| 37 | 
            +
                    @status = :closed
         | 
| 38 | 
            +
                  end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                  driver.on(:open) do |e|
         | 
| 41 | 
            +
                    @status = :open
         | 
| 42 | 
            +
                  end
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                def start_driver
         | 
| 46 | 
            +
                  driver.start
         | 
| 47 | 
            +
                  select [socket.io]
         | 
| 48 | 
            +
                  parse_input until status == :open
         | 
| 49 | 
            +
                end
         | 
| 50 | 
            +
              end
         | 
| 51 | 
            +
            end
         | 
| @@ -0,0 +1,109 @@ | |
| 1 | 
            +
            module Capybara::Chrome
         | 
| 2 | 
            +
              module Service
         | 
| 3 | 
            +
             | 
| 4 | 
            +
                  # "--disable-gpu",
         | 
| 5 | 
            +
                  # '--js-flags="--max-old-space-size=500"',
         | 
| 6 | 
            +
                CHROME_ARGS = [
         | 
| 7 | 
            +
                  "--headless",
         | 
| 8 | 
            +
                  "--enable-automation",
         | 
| 9 | 
            +
                  "--crash-dumps-dir=/tmp",
         | 
| 10 | 
            +
                  "--disable-background-networking",
         | 
| 11 | 
            +
                  "--disable-background-timer-throttling",
         | 
| 12 | 
            +
                  "--disable-breakpad",
         | 
| 13 | 
            +
                  "--disable-client-side-phishing-detection",
         | 
| 14 | 
            +
                  "--disable-default-apps",
         | 
| 15 | 
            +
                  "--disable-dev-shm-usage",
         | 
| 16 | 
            +
                  "--disable-extensions",
         | 
| 17 | 
            +
                  "--disable-features=site-per-process",
         | 
| 18 | 
            +
                  "--disable-hang-monitor",
         | 
| 19 | 
            +
                  "--disable-popup-blocking",
         | 
| 20 | 
            +
                  "--disable-prompt-on-repost",
         | 
| 21 | 
            +
                  "--disable-sync",
         | 
| 22 | 
            +
                  "--disable-translate",
         | 
| 23 | 
            +
                  "--metrics-recording-only",
         | 
| 24 | 
            +
                  "--no-first-run",
         | 
| 25 | 
            +
                  "--no-pings",
         | 
| 26 | 
            +
                  "--safebrowsing-disable-auto-update",
         | 
| 27 | 
            +
                  "--hide-scrollbars",
         | 
| 28 | 
            +
                  "--mute-audio",
         | 
| 29 | 
            +
                ]
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                def chrome_pid
         | 
| 32 | 
            +
                  @chrome_pid
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                def start_chrome
         | 
| 36 | 
            +
                  return if chrome_running?
         | 
| 37 | 
            +
                  debug "Starting Chrome", chrome_path, chrome_args
         | 
| 38 | 
            +
                  @chrome_pid = Process.spawn chrome_path, *chrome_args, :out=>"/dev/null"
         | 
| 39 | 
            +
                  at_exit { stop_chrome }
         | 
| 40 | 
            +
                end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                def stop_chrome
         | 
| 43 | 
            +
                  Process.kill "TERM", chrome_pid rescue nil
         | 
| 44 | 
            +
                end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                def restart_chrome
         | 
| 47 | 
            +
                  stop_chrome
         | 
| 48 | 
            +
                  if chrome_running?
         | 
| 49 | 
            +
                    @chrome_port = find_available_port(@chrome_host)
         | 
| 50 | 
            +
                  end
         | 
| 51 | 
            +
                  start_chrome
         | 
| 52 | 
            +
                end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                def wait_for_chrome
         | 
| 55 | 
            +
                  running = false
         | 
| 56 | 
            +
                  while !running
         | 
| 57 | 
            +
                    running = chrome_running?
         | 
| 58 | 
            +
                    sleep 0.02
         | 
| 59 | 
            +
                  end
         | 
| 60 | 
            +
                end
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                def chrome_running?
         | 
| 63 | 
            +
                  socket = TCPSocket.new(@chrome_host, @chrome_port) rescue false
         | 
| 64 | 
            +
                  socket.close if socket
         | 
| 65 | 
            +
                  !!socket
         | 
| 66 | 
            +
                end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                def chrome_path
         | 
| 69 | 
            +
                  case os
         | 
| 70 | 
            +
                  when :macosx
         | 
| 71 | 
            +
                    "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
         | 
| 72 | 
            +
                  when :linux
         | 
| 73 | 
            +
                    # /opt/google/chrome/chrome
         | 
| 74 | 
            +
                    "google-chrome-stable"
         | 
| 75 | 
            +
                  end
         | 
| 76 | 
            +
                end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                def chrome_args
         | 
| 79 | 
            +
                  CHROME_ARGS + ["--remote-debugging-port=#{@chrome_port}"]
         | 
| 80 | 
            +
                end
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                def os
         | 
| 83 | 
            +
                  @os ||= (
         | 
| 84 | 
            +
                    host_os = RbConfig::CONFIG['host_os']
         | 
| 85 | 
            +
                    case host_os
         | 
| 86 | 
            +
                    when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
         | 
| 87 | 
            +
                      :windows
         | 
| 88 | 
            +
                    when /darwin|mac os/
         | 
| 89 | 
            +
                      :macosx
         | 
| 90 | 
            +
                    when /linux/
         | 
| 91 | 
            +
                      :linux
         | 
| 92 | 
            +
                    when /solaris|bsd/
         | 
| 93 | 
            +
                      :unix
         | 
| 94 | 
            +
                    else
         | 
| 95 | 
            +
                      raise Error::WebDriverError, "unknown os: #{host_os.inspect}"
         | 
| 96 | 
            +
                    end
         | 
| 97 | 
            +
                  )
         | 
| 98 | 
            +
                end
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                def find_available_port(host)
         | 
| 101 | 
            +
                  sleep rand * 0.7 # slight delay to account for concurrent browsers
         | 
| 102 | 
            +
                  server = TCPServer.new(host, 0)
         | 
| 103 | 
            +
                  server.addr[1]
         | 
| 104 | 
            +
                ensure
         | 
| 105 | 
            +
                  server.close if server
         | 
| 106 | 
            +
                end
         | 
| 107 | 
            +
             | 
| 108 | 
            +
              end
         | 
| 109 | 
            +
            end
         | 
| @@ -0,0 +1,340 @@ | |
| 1 | 
            +
            window.ChromeRemotePageLoaded = document.readyState == "complete"
         | 
| 2 | 
            +
            window.ChromeRemoteHelper = {
         | 
| 3 | 
            +
              DOMContentLoaded: document.readyState == "interactive" || document.readyState == "complete",
         | 
| 4 | 
            +
              windowLoaded: document.readyState == "complete",
         | 
| 5 | 
            +
              windowUnloading: false,
         | 
| 6 | 
            +
              nextIndex: 0,
         | 
| 7 | 
            +
              nodes: {},
         | 
| 8 | 
            +
              nodeClicks: {},
         | 
| 9 | 
            +
              TEXT_TYPES: ["date", "email", "number", "password", "search", "tel", "text", "textarea", "url"],
         | 
| 10 | 
            +
              windowWaitPromise: null,
         | 
| 11 | 
            +
             | 
| 12 | 
            +
              registerNode: function(node) {
         | 
| 13 | 
            +
                this.nextIndex++;
         | 
| 14 | 
            +
                this.nodes[this.nextIndex] = node;
         | 
| 15 | 
            +
                return this.nextIndex;
         | 
| 16 | 
            +
              },
         | 
| 17 | 
            +
             | 
| 18 | 
            +
              waitPromise: function(truthFn, delay) {
         | 
| 19 | 
            +
                var ms = 0;
         | 
| 20 | 
            +
                var intId;
         | 
| 21 | 
            +
                var p = new Promise(function(resolve, reject) {
         | 
| 22 | 
            +
                  var truthy = truthFn();
         | 
| 23 | 
            +
                  if (truthy) {
         | 
| 24 | 
            +
                    resolve(truthy);
         | 
| 25 | 
            +
                  } else if (this.windowUnloading) {
         | 
| 26 | 
            +
                    if (intId) {
         | 
| 27 | 
            +
                      window.clearInterval(intId);
         | 
| 28 | 
            +
                    }
         | 
| 29 | 
            +
                    resolve(false);
         | 
| 30 | 
            +
                  } else {
         | 
| 31 | 
            +
                    intId = window.setInterval(function() {
         | 
| 32 | 
            +
                      truthy = truthFn(intId);
         | 
| 33 | 
            +
                      ms += delay;
         | 
| 34 | 
            +
                      if (truthy || ms > 500) {
         | 
| 35 | 
            +
                        window.clearInterval(intId);
         | 
| 36 | 
            +
                        resolve(truthy);
         | 
| 37 | 
            +
                      } else if (this.windowUnloading) {
         | 
| 38 | 
            +
                        resolve(false);
         | 
| 39 | 
            +
                      } else {
         | 
| 40 | 
            +
                        console.log("not truthy", truthy, this);
         | 
| 41 | 
            +
                      }
         | 
| 42 | 
            +
                    }.bind(this), delay);
         | 
| 43 | 
            +
                  }
         | 
| 44 | 
            +
                }.bind(this));
         | 
| 45 | 
            +
                return p;
         | 
| 46 | 
            +
              },
         | 
| 47 | 
            +
             | 
| 48 | 
            +
              waitUnload: function() {
         | 
| 49 | 
            +
                var ms = 0;
         | 
| 50 | 
            +
                var delay = 1;
         | 
| 51 | 
            +
                var intId;
         | 
| 52 | 
            +
                var p = new Promise(function(resolve, reject) {
         | 
| 53 | 
            +
                  if (this.windowUnloading) {
         | 
| 54 | 
            +
                    resolve(true);
         | 
| 55 | 
            +
                  }
         | 
| 56 | 
            +
                  var fn = function() {
         | 
| 57 | 
            +
                    ms += delay;
         | 
| 58 | 
            +
                    if (ms >= 100) {
         | 
| 59 | 
            +
                      //clearInterval(intId);
         | 
| 60 | 
            +
                      return true;
         | 
| 61 | 
            +
                    } else {
         | 
| 62 | 
            +
                      if (this.windowUnloading) {
         | 
| 63 | 
            +
                        // clearInterval(intId);
         | 
| 64 | 
            +
                        return true;
         | 
| 65 | 
            +
                      } else {
         | 
| 66 | 
            +
                        return false;
         | 
| 67 | 
            +
                      }
         | 
| 68 | 
            +
                    }
         | 
| 69 | 
            +
                  }.bind(this);
         | 
| 70 | 
            +
                  var redo = function() {
         | 
| 71 | 
            +
                    if (fn()) {
         | 
| 72 | 
            +
                      return resolve(true);
         | 
| 73 | 
            +
                    } else {
         | 
| 74 | 
            +
                      window.setTimeout(redo, delay);
         | 
| 75 | 
            +
                    }
         | 
| 76 | 
            +
                  }.bind(this);
         | 
| 77 | 
            +
                  redo();
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                  // intId = window.setInterval(, delay);
         | 
| 80 | 
            +
                }.bind(this));
         | 
| 81 | 
            +
                return p;
         | 
| 82 | 
            +
              },
         | 
| 83 | 
            +
             | 
| 84 | 
            +
              waitWindowLoaded: function() {
         | 
| 85 | 
            +
                var ms = 0;
         | 
| 86 | 
            +
                var delay = 0;
         | 
| 87 | 
            +
                this.windowWaitPromise = this.waitPromise(function() {
         | 
| 88 | 
            +
                  return this.windowLoaded;
         | 
| 89 | 
            +
                }.bind(this), delay);
         | 
| 90 | 
            +
                return this.windowWaitPromise;
         | 
| 91 | 
            +
              },
         | 
| 92 | 
            +
             | 
| 93 | 
            +
              waitDOMContentLoaded: function() {
         | 
| 94 | 
            +
                return this.waitPromise(function() {
         | 
| 95 | 
            +
                  return this.DOMContentLoaded;
         | 
| 96 | 
            +
                }.bind(this), 5);
         | 
| 97 | 
            +
              },
         | 
| 98 | 
            +
             | 
| 99 | 
            +
              findCss: function(query) {
         | 
| 100 | 
            +
                return this.findCssRelativeTo(document, query);
         | 
| 101 | 
            +
              },
         | 
| 102 | 
            +
             | 
| 103 | 
            +
              findCssWithin: function (index, query) {
         | 
| 104 | 
            +
                return this.findCssRelativeTo(this.getNode(index), query);
         | 
| 105 | 
            +
              },
         | 
| 106 | 
            +
             | 
| 107 | 
            +
              findCssRelativeTo: function(reference, query) {
         | 
| 108 | 
            +
                return this.waitDOMContentLoaded().then(function() {
         | 
| 109 | 
            +
                  var results = [];
         | 
| 110 | 
            +
                  var list = reference.querySelectorAll(query);
         | 
| 111 | 
            +
                  for (i = 0; i < list.length; i++) {
         | 
| 112 | 
            +
                    results.push(this.registerNode(list[i]));
         | 
| 113 | 
            +
                  }
         | 
| 114 | 
            +
                  return results.join(",");
         | 
| 115 | 
            +
                }.bind(this));
         | 
| 116 | 
            +
              },
         | 
| 117 | 
            +
             | 
| 118 | 
            +
              findXPath: function(query) {
         | 
| 119 | 
            +
                return this.findXpathRelativeTo(document, query);
         | 
| 120 | 
            +
              },
         | 
| 121 | 
            +
             | 
| 122 | 
            +
              findXPathWithin: function(index, query) {
         | 
| 123 | 
            +
                return this.findXpathRelativeTo(this.getNode(index), query);
         | 
| 124 | 
            +
              },
         | 
| 125 | 
            +
             | 
| 126 | 
            +
              findXpathRelativeTo: function(reference, query) {
         | 
| 127 | 
            +
                return this.waitDOMContentLoaded().then(function() {
         | 
| 128 | 
            +
                  var iterator = document.evaluate(query, reference, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
         | 
| 129 | 
            +
                  var node;
         | 
| 130 | 
            +
                  var results = [];
         | 
| 131 | 
            +
                  while (node = iterator.iterateNext()) {
         | 
| 132 | 
            +
                    results.push(this.registerNode(node));
         | 
| 133 | 
            +
                  }
         | 
| 134 | 
            +
                  return results.join(",");
         | 
| 135 | 
            +
                }.bind(this));
         | 
| 136 | 
            +
              },
         | 
| 137 | 
            +
             | 
| 138 | 
            +
              getXPathNode: function(node, path) {
         | 
| 139 | 
            +
                path = path || [];
         | 
| 140 | 
            +
                if (node.parentNode) {
         | 
| 141 | 
            +
                  path = this.getXPathNode(node.parentNode, path);
         | 
| 142 | 
            +
                }
         | 
| 143 | 
            +
             | 
| 144 | 
            +
                var first = node;
         | 
| 145 | 
            +
                while (first.previousSibling)
         | 
| 146 | 
            +
                  first = first.previousSibling;
         | 
| 147 | 
            +
             | 
| 148 | 
            +
                var count = 0;
         | 
| 149 | 
            +
                var index = 0;
         | 
| 150 | 
            +
                var iter = first;
         | 
| 151 | 
            +
                while (iter) {
         | 
| 152 | 
            +
                  if (iter.nodeType == 1 && iter.nodeName == node.nodeName)
         | 
| 153 | 
            +
                    count++;
         | 
| 154 | 
            +
                  if (iter.isSameNode(node))
         | 
| 155 | 
            +
                    index = count;
         | 
| 156 | 
            +
                  iter = iter.nextSibling;
         | 
| 157 | 
            +
                  continue;
         | 
| 158 | 
            +
                }
         | 
| 159 | 
            +
             | 
| 160 | 
            +
                if (node.nodeType == 1)
         | 
| 161 | 
            +
                  path.push(node.nodeName.toLowerCase() + (node.id ? "[@id='"+node.id+"']" : count > 1 ? "["+index+"]" : ''));
         | 
| 162 | 
            +
             | 
| 163 | 
            +
                return path;
         | 
| 164 | 
            +
              },
         | 
| 165 | 
            +
             | 
| 166 | 
            +
              nodePathForNode: function(index) {
         | 
| 167 | 
            +
                return this.pathForNode(this.getNode(index));
         | 
| 168 | 
            +
              },
         | 
| 169 | 
            +
             | 
| 170 | 
            +
              pathForNode: function(node) {
         | 
| 171 | 
            +
                return "/" + this.getXPathNode(node).join("/");
         | 
| 172 | 
            +
              },
         | 
| 173 | 
            +
             | 
| 174 | 
            +
              getNode: function(index) {
         | 
| 175 | 
            +
                var node = this.nodes[index];
         | 
| 176 | 
            +
                if (!node) {
         | 
| 177 | 
            +
                  throw new NodeNotFoundError("No node found with id:"+index+". Registered nodes:"+Object.keys(this.nodes).length);
         | 
| 178 | 
            +
                }
         | 
| 179 | 
            +
                return node;
         | 
| 180 | 
            +
              },
         | 
| 181 | 
            +
             | 
| 182 | 
            +
              onSelf: function(index, script) {
         | 
| 183 | 
            +
                var node = this.getNode(index);
         | 
| 184 | 
            +
                // console.log("onSelf " + index + " " + node.tagName + " " + (node.parentElement && node.parentElement.tagName) + " " + script);
         | 
| 185 | 
            +
                var fn = Function("'use strict';"+script).bind(node)
         | 
| 186 | 
            +
                var pp = new Promise(function(resolve) { resolve(fn()); });
         | 
| 187 | 
            +
                return pp;
         | 
| 188 | 
            +
              },
         | 
| 189 | 
            +
             | 
| 190 | 
            +
              // args should be an array
         | 
| 191 | 
            +
              onSelfValue: function(index, meth, args) {
         | 
| 192 | 
            +
                var node = this.getNode(index);
         | 
| 193 | 
            +
                var val = node[meth];
         | 
| 194 | 
            +
                if (typeof val === "function") {
         | 
| 195 | 
            +
                  val.apply(node, args);
         | 
| 196 | 
            +
                } else {
         | 
| 197 | 
            +
                  return val;
         | 
| 198 | 
            +
                }
         | 
| 199 | 
            +
              },
         | 
| 200 | 
            +
             | 
| 201 | 
            +
              dispatchEvent: function(node, eventName) {
         | 
| 202 | 
            +
                var eventObject;
         | 
| 203 | 
            +
                if (eventName == "click") {
         | 
| 204 | 
            +
                  eventObject = new MouseEvent("click", {bubbles: true, cancelable: true});
         | 
| 205 | 
            +
                } else {
         | 
| 206 | 
            +
                  eventObject = document.createEvent("HTMLEvents");
         | 
| 207 | 
            +
                  eventObject.initEvent(eventName, true, true);
         | 
| 208 | 
            +
                }
         | 
| 209 | 
            +
                return node.dispatchEvent(eventObject);
         | 
| 210 | 
            +
              },
         | 
| 211 | 
            +
             | 
| 212 | 
            +
              nodeSetType: function(index) {
         | 
| 213 | 
            +
                var node = this.getNode(index);
         | 
| 214 | 
            +
                return (node.type || node.tagName).toLowerCase();
         | 
| 215 | 
            +
              },
         | 
| 216 | 
            +
             | 
| 217 | 
            +
              nodeSet: function(index, value, type) {
         | 
| 218 | 
            +
                var node = this.getNode(index);
         | 
| 219 | 
            +
                if (type == "checkbox" || type == "radio") {
         | 
| 220 | 
            +
                  if (value == "true" && !node.checked) {
         | 
| 221 | 
            +
                    return node.click();
         | 
| 222 | 
            +
                  } else if (value == "false" && node.checked) {
         | 
| 223 | 
            +
                    return node.click();
         | 
| 224 | 
            +
                  }
         | 
| 225 | 
            +
                } else {
         | 
| 226 | 
            +
                  return node.value = value;
         | 
| 227 | 
            +
                }
         | 
| 228 | 
            +
              },
         | 
| 229 | 
            +
             | 
| 230 | 
            +
              nodeVisible: function(index) {
         | 
| 231 | 
            +
                return this.visible(this.getNode(index));
         | 
| 232 | 
            +
              },
         | 
| 233 | 
            +
             | 
| 234 | 
            +
              visible: function(node) {
         | 
| 235 | 
            +
                var styles = node.ownerDocument.defaultView.getComputedStyle(node);
         | 
| 236 | 
            +
                if (styles["visibility"] == "hidden" || styles["display"] == "none" || styles["opacity"] == 0) {
         | 
| 237 | 
            +
                  return false;
         | 
| 238 | 
            +
                }
         | 
| 239 | 
            +
                while (node = node.parentElement) {
         | 
| 240 | 
            +
                  styles = node.ownerDocument.defaultView.getComputedStyle(node);
         | 
| 241 | 
            +
                  if (styles["display"] == "none" || styles["opacity"] == 0) {
         | 
| 242 | 
            +
                    return false;
         | 
| 243 | 
            +
                  }
         | 
| 244 | 
            +
                }
         | 
| 245 | 
            +
                return true;
         | 
| 246 | 
            +
              },
         | 
| 247 | 
            +
             | 
| 248 | 
            +
              nodeText: function(index) {
         | 
| 249 | 
            +
                return this.text(this.getNode(index));
         | 
| 250 | 
            +
              },
         | 
| 251 | 
            +
             | 
| 252 | 
            +
              text: function(node) {
         | 
| 253 | 
            +
                var type = node instanceof HTMLFormElement ? 'form' : (node.type || node.tagName).toLowerCase();
         | 
| 254 | 
            +
                if (type == "textarea") {
         | 
| 255 | 
            +
                  return node.innerHTML;
         | 
| 256 | 
            +
                } else {
         | 
| 257 | 
            +
                  var visible_text = node.innerText;
         | 
| 258 | 
            +
                  return typeof visible_text === "string" ? visible_text : node.textContent;
         | 
| 259 | 
            +
                }
         | 
| 260 | 
            +
              },
         | 
| 261 | 
            +
             | 
| 262 | 
            +
              nodeIsNodeAtPosition: function(index, pos) {
         | 
| 263 | 
            +
                return this.isNodeAtPosition(this.getNode(index), pos);
         | 
| 264 | 
            +
              },
         | 
| 265 | 
            +
             | 
| 266 | 
            +
              isNodeAtPosition: function(node, pos) {
         | 
| 267 | 
            +
                var nodeAtPosition =
         | 
| 268 | 
            +
                  document.elementFromPoint(pos.relativeX, pos.relativeY);
         | 
| 269 | 
            +
                var overlappingPath;
         | 
| 270 | 
            +
             | 
| 271 | 
            +
             | 
| 272 | 
            +
                if (nodeAtPosition) {
         | 
| 273 | 
            +
                  // console.log("is node at position" + nodeAtPosition.tagName)
         | 
| 274 | 
            +
                  overlappingPath = this.pathForNode(nodeAtPosition)
         | 
| 275 | 
            +
                }
         | 
| 276 | 
            +
             | 
| 277 | 
            +
                if (!this.isNodeOrChildAtPosition(node, pos, nodeAtPosition)) {
         | 
| 278 | 
            +
                  // console.log("Would throw " + overlappingPath + " " + this.pathForNode(node))
         | 
| 279 | 
            +
                  return false;
         | 
| 280 | 
            +
                }
         | 
| 281 | 
            +
             | 
| 282 | 
            +
                return true;
         | 
| 283 | 
            +
              },
         | 
| 284 | 
            +
             | 
| 285 | 
            +
              isNodeOrChildAtPosition: function(expectedNode, pos, currentNode) {
         | 
| 286 | 
            +
                if (currentNode == expectedNode) {
         | 
| 287 | 
            +
                  return true;
         | 
| 288 | 
            +
                } else if (currentNode) {
         | 
| 289 | 
            +
                  return this.isNodeOrChildAtPosition(
         | 
| 290 | 
            +
                    expectedNode,
         | 
| 291 | 
            +
                    pos,
         | 
| 292 | 
            +
                    currentNode.parentNode
         | 
| 293 | 
            +
                  );
         | 
| 294 | 
            +
                } else {
         | 
| 295 | 
            +
                  return false;
         | 
| 296 | 
            +
                }
         | 
| 297 | 
            +
              },
         | 
| 298 | 
            +
             | 
| 299 | 
            +
              nodeGetDimensions: function(index) {
         | 
| 300 | 
            +
                return this.getDimensions(this.getNode(index));
         | 
| 301 | 
            +
              },
         | 
| 302 | 
            +
             | 
| 303 | 
            +
              getDimensions: function(node) {
         | 
| 304 | 
            +
                return JSON.stringify(node.getBoundingClientRect());
         | 
| 305 | 
            +
              },
         | 
| 306 | 
            +
             | 
| 307 | 
            +
              // don't attach state to the node because the node can go away after click
         | 
| 308 | 
            +
              attachClickListener: function(index) {
         | 
| 309 | 
            +
                var node = this.getNode(index);
         | 
| 310 | 
            +
                this.nodeClicks[index] = false;
         | 
| 311 | 
            +
                var fn = function() {
         | 
| 312 | 
            +
                  this.nodeClicks[index] = true;
         | 
| 313 | 
            +
                  node.removeEventListener("click", fn);
         | 
| 314 | 
            +
                }.bind(this);
         | 
| 315 | 
            +
                node.addEventListener("click", fn);
         | 
| 316 | 
            +
              },
         | 
| 317 | 
            +
             | 
| 318 | 
            +
              nodeVerifyClicked: function(index) {
         | 
| 319 | 
            +
                return this.nodeClicks[index];
         | 
| 320 | 
            +
              }
         | 
| 321 | 
            +
            }
         | 
| 322 | 
            +
             | 
| 323 | 
            +
            document.addEventListener("DOMContentLoaded", function() {
         | 
| 324 | 
            +
              ChromeRemoteHelper.DOMContentLoaded = true;
         | 
| 325 | 
            +
            });
         | 
| 326 | 
            +
             | 
| 327 | 
            +
            window.addEventListener("load", function() {
         | 
| 328 | 
            +
              ChromeRemoteHelper.windowLoaded = true;
         | 
| 329 | 
            +
              ChromeRemotePageLoaded = true;
         | 
| 330 | 
            +
            });
         | 
| 331 | 
            +
             | 
| 332 | 
            +
            window.addEventListener("beforeunload", function() {
         | 
| 333 | 
            +
              ChromeRemoteHelper.windowUnloading = true;
         | 
| 334 | 
            +
              ChromeRemoteHelper.windowLoaded = true;
         | 
| 335 | 
            +
            });
         | 
| 336 | 
            +
             | 
| 337 | 
            +
            window.addEventListener("unload", function() {
         | 
| 338 | 
            +
            });
         | 
| 339 | 
            +
             | 
| 340 | 
            +
            class NodeNotFoundError extends Error{}
         |