├── .document ├── .gitignore ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── VERSION ├── lib ├── webkit_remote.rb └── webkit_remote │ ├── browser.rb │ ├── client.rb │ ├── client │ ├── console.rb │ ├── console_events.rb │ ├── dom.rb │ ├── dom_events.rb │ ├── dom_runtime.rb │ ├── input.rb │ ├── network.rb │ ├── network_events.rb │ ├── page.rb │ ├── page_events.rb │ └── runtime.rb │ ├── event.rb │ ├── process.rb │ ├── rpc.rb │ └── top_level.rb ├── test ├── fixtures │ ├── config.ru │ ├── html │ │ ├── console.html │ │ ├── dom.html │ │ ├── input.html │ │ ├── load.html │ │ ├── network.html │ │ ├── popup.html │ │ ├── popup_user.html │ │ └── runtime.html │ ├── js │ │ └── network.js │ └── png │ │ └── network.png ├── helper.rb ├── webkit_remote │ ├── browser_test.rb │ ├── client │ │ ├── console_test.rb │ │ ├── dom_test.rb │ │ ├── input_test.rb │ │ ├── js_object_group_test.rb │ │ ├── js_object_test.rb │ │ ├── network_test.rb │ │ ├── page_test.rb │ │ └── runtime_test.rb │ ├── client_test.rb │ ├── event_test.rb │ ├── process_flags_test.rb │ ├── process_test.rb │ └── rpc_test.rb └── webkit_remote_test.rb └── webkit_remote.gemspec /.document: -------------------------------------------------------------------------------- 1 | lib/**/*.rb 2 | bin/* 3 | - 4 | features/**/*.feature 5 | LICENSE.txt 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # rcov generated 2 | coverage 3 | coverage.data 4 | 5 | # rdoc generated 6 | rdoc 7 | 8 | # yard generated 9 | doc 10 | .yardoc 11 | 12 | # bundler 13 | .bundle 14 | 15 | # jeweler generated 16 | pkg 17 | 18 | # Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore: 19 | # 20 | # * Create a file at ~/.gitignore 21 | # * Include files you want ignored 22 | # * Run: git config --global core.excludesfile ~/.gitignore 23 | # 24 | # After doing this, these files will be ignored in all your git projects, 25 | # saving you from having to 'pollute' every project you touch with them 26 | # 27 | # Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line) 28 | # 29 | # For MacOS: 30 | .DS_Store 31 | 32 | # For TextMate 33 | #*.tmproj 34 | #tmtags 35 | 36 | # For emacs: 37 | *~ 38 | \#* 39 | .\#* 40 | 41 | # For vim: 42 | *.swp 43 | 44 | # For redcar: 45 | #.redcar 46 | 47 | # For rubinius: 48 | #*.rbc 49 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | dist: trusty 3 | language: ruby 4 | addons: 5 | apt: 6 | sources: 7 | - google-chrome 8 | packages: 9 | - google-chrome-stable 10 | rvm: 11 | - 2.3.4 12 | - 2.4.1 13 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem 'ws_sync_client', '>= 0.1.2' 3 | 4 | group :development do 5 | gem 'bundler', '>= 1.5.3' 6 | gem 'byebug', '>= 9.0.6' 7 | gem 'jeweler', '>= 2.0.1' 8 | gem 'minitest', '>= 5.3.0' 9 | gem 'puma', '>= 2.8.0' 10 | gem 'rack', '>= 1.6.8' 11 | gem 'rack-contrib', '>= 1.2.0' 12 | gem 'rdoc', '>= 4.1.1' 13 | gem 'simplecov', '>= 0.9.1' 14 | gem 'yard', '>= 0.8.7.3' 15 | end 16 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.4.0) 5 | builder (3.2.3) 6 | byebug (9.0.6) 7 | descendants_tracker (0.0.4) 8 | thread_safe (~> 0.3, >= 0.3.1) 9 | docile (1.1.5) 10 | faraday (0.9.2) 11 | multipart-post (>= 1.2, < 3) 12 | git (1.3.0) 13 | git-version-bump (0.15.1) 14 | github_api (0.16.0) 15 | addressable (~> 2.4.0) 16 | descendants_tracker (~> 0.0.4) 17 | faraday (~> 0.8, < 0.10) 18 | hashie (>= 3.4) 19 | mime-types (>= 1.16, < 3.0) 20 | oauth2 (~> 1.0) 21 | hashie (3.5.5) 22 | highline (1.7.8) 23 | jeweler (2.3.7) 24 | builder 25 | bundler (>= 1) 26 | git (>= 1.2.5) 27 | github_api (~> 0.16.0) 28 | highline (>= 1.6.15) 29 | nokogiri (>= 1.5.10) 30 | psych (~> 2.2) 31 | rake 32 | rdoc 33 | semver2 34 | json (2.1.0) 35 | jwt (1.5.6) 36 | mime-types (2.99.3) 37 | mini_portile2 (2.2.0) 38 | minitest (5.10.2) 39 | multi_json (1.12.1) 40 | multi_xml (0.6.0) 41 | multipart-post (2.0.0) 42 | nokogiri (1.8.0) 43 | mini_portile2 (~> 2.2.0) 44 | oauth2 (1.4.0) 45 | faraday (>= 0.8, < 0.13) 46 | jwt (~> 1.0) 47 | multi_json (~> 1.3) 48 | multi_xml (~> 0.5) 49 | rack (>= 1.2, < 3) 50 | psych (2.2.4) 51 | puma (3.9.1) 52 | rack (1.6.8) 53 | rack-contrib (1.4.0) 54 | git-version-bump (~> 0.15) 55 | rack (~> 1.4) 56 | rake (12.0.0) 57 | rdoc (5.1.0) 58 | semver2 (3.4.2) 59 | simplecov (0.14.1) 60 | docile (~> 1.1.0) 61 | json (>= 1.8, < 3) 62 | simplecov-html (~> 0.10.0) 63 | simplecov-html (0.10.1) 64 | thread_safe (0.3.6) 65 | websocket (1.2.4) 66 | websocket-native (1.0.0) 67 | ws_sync_client (0.1.2) 68 | websocket (>= 1.2.4) 69 | websocket-native (>= 1.0.0) 70 | yard (0.9.9) 71 | 72 | PLATFORMS 73 | ruby 74 | 75 | DEPENDENCIES 76 | bundler (>= 1.5.3) 77 | byebug (>= 9.0.6) 78 | jeweler (>= 2.0.1) 79 | minitest (>= 5.3.0) 80 | puma (>= 2.8.0) 81 | rack (>= 1.6.8) 82 | rack-contrib (>= 1.2.0) 83 | rdoc (>= 4.1.1) 84 | simplecov (>= 0.9.1) 85 | ws_sync_client (>= 0.1.2) 86 | yard (>= 0.8.7.3) 87 | 88 | BUNDLED WITH 89 | 1.14.6 90 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Victor Costan 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webkit_remote 2 | 3 | Ruby gem for driving 4 | [Google Chrome](https://www.google.com/chrome/) and possibly other 5 | WebKit-based browsers via the 6 | [WebKit remote debugging protocol](https://www.webkit.org/blog/1875/announcing-remote-debugging-protocol-v1-0/). 7 | 8 | 9 | ## Features 10 | 11 | This gem can be used to test Web pages in real browsers with minimal intrusion. 12 | 13 | Compared to [PhantomJS](http://phantomjs.org/), `webkit_remote` tests will take 14 | longer, but provide assurance that the code will run as intended on desktop and 15 | mobile browsers, and can exercise HTML5 features that are not yet 16 | [supported by Phantom](http://code.google.com/p/phantomjs/wiki/SupportedFeatures). 17 | 18 | Compared to [Selenium](http://seleniumhq.org/), `webkit_remote` is less mature, 19 | and only supports WebKit-based browsers. In return, the gem can support 20 | (either directly or via extensions) features that have not made their way into 21 | Selenium's [WebDriver](http://www.w3.org/TR/webdriver/). 22 | 23 | Currently, the following sections of the 24 | [WebKit remote debugging protocol](https://developers.google.com/chrome-developer-tools/docs/protocol/1.0/) 25 | have been implemented: 26 | 27 | * Console 28 | * DOM (incomplete) 29 | * Input 30 | * Network 31 | * Page 32 | * Remote 33 | 34 | This gem will only support officially released remote debugging protocol 35 | features. If you need to use an unsupported feature, such as CSS debugging, 36 | take a look at the 37 | [webkit_remote_unstable](https:://github.com/pwnall/webkit_remote_unstable) 38 | gem. 39 | 40 | 41 | ## Requirements 42 | 43 | The gem is tested against the OSX and Linux builds of Google Chrome. The only 44 | platform-dependent functionality is launching and shutting down the browser 45 | process, everything else should work for any WebKit-based browser that 46 | implements the remote debugging protocol. 47 | 48 | Google Chrome 60 and above 49 | [can be used in headless mode](https://developers.google.com/web/updates/2017/04/headless-chrome). 50 | 51 | 52 | ## Installation 53 | 54 | Use RubyGems. 55 | 56 | ```bash 57 | gem install webkit_remote 58 | ``` 59 | 60 | 61 | ## Usage 62 | 63 | This section only showcases a few features. Read the 64 | [YARD docs](http://rdoc.info/github/pwnall/webkit_remote) 65 | to see everything this gem has to offer. 66 | 67 | ### Session Setup 68 | 69 | ```ruby 70 | client = WebkitRemote.local 71 | ``` 72 | 73 | launches a separate instance of Google Chrome that is not connected to your 74 | profile, and sets up a connection to it. Alternatively, 75 | 76 | ```ruby 77 | client = WebkitRemote.remote host: 'phone-ip-here', port: 9222 78 | ``` 79 | 80 | connects to a remote WebKit instance 81 | [running on a phone](https://developers.google.com/chrome/mobile/docs/debugging). 82 | 83 | ### Load a Page 84 | 85 | ```ruby 86 | client.page_events = true 87 | client.navigate_to 'http://translate.google.com' 88 | client.wait_for(type: WebkitRemote::Event::PageLoaded).last 89 | ``` 90 | 91 | ### Run JavaScript 92 | 93 | Evaluate some JavaScript. 94 | 95 | ```ruby 96 | element = client.remote_eval 'document.querySelector("[name=text]")' 97 | ``` 98 | 99 | Take a look at the result. 100 | 101 | ```ruby 102 | element.js_class_name 103 | element.description 104 | element.properties[:tagName].value 105 | element.properties[:tagName].writable? 106 | ``` 107 | 108 | Pass an object to some JavaScript code. 109 | 110 | ```ruby 111 | js_code = < e 8 | $stderr.puts e.message 9 | $stderr.puts "Run `bundle install` to install missing gems" 10 | exit e.status_code 11 | end 12 | require 'rake' 13 | 14 | require 'jeweler' 15 | Jeweler::Tasks.new do |gem| 16 | # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options 17 | gem.name = "webkit_remote" 18 | gem.homepage = "http://github.com/pwnall/webkit_remote" 19 | gem.license = "MIT" 20 | gem.summary = %Q{Client for the Webkit Remote Debugging server} 21 | gem.description = %Q{Launches Google Chrome instances and controls them via the Remote Debugging server} 22 | gem.email = "victor@costan.us" 23 | gem.authors = ["Victor Costan"] 24 | # dependencies defined in Gemfile 25 | end 26 | Jeweler::RubygemsDotOrgTasks.new 27 | 28 | require 'rake/testtask' 29 | Rake::TestTask.new(:test) do |test| 30 | test.libs << 'lib' << 'test' 31 | test.pattern = 'test/**/*_test.rb' 32 | test.verbose = true 33 | end 34 | 35 | task :default => :test 36 | 37 | require 'yard' 38 | YARD::Rake::YardocTask.new 39 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.6.0 -------------------------------------------------------------------------------- /lib/webkit_remote.rb: -------------------------------------------------------------------------------- 1 | module WebkitRemote; end 2 | require 'webkit_remote/top_level.rb' 3 | 4 | require 'webkit_remote/browser.rb' 5 | require 'webkit_remote/process.rb' 6 | require 'webkit_remote/rpc.rb' 7 | 8 | require 'webkit_remote/client.rb' 9 | require 'webkit_remote/event.rb' 10 | require 'webkit_remote/client/console.rb' 11 | require 'webkit_remote/client/console_events.rb' 12 | require 'webkit_remote/client/dom.rb' 13 | require 'webkit_remote/client/dom_events.rb' 14 | require 'webkit_remote/client/input.rb' 15 | require 'webkit_remote/client/network.rb' 16 | require 'webkit_remote/client/network_events.rb' 17 | require 'webkit_remote/client/page.rb' 18 | require 'webkit_remote/client/page_events.rb' 19 | require 'webkit_remote/client/runtime.rb' 20 | require 'webkit_remote/client/dom_runtime.rb' 21 | -------------------------------------------------------------------------------- /lib/webkit_remote/browser.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'net/http' 3 | 4 | module WebkitRemote 5 | 6 | # The master connection to the remote debugging server in a Webkit process. 7 | class Browser 8 | # Sets up a debugging connection to a Webkit process. 9 | # 10 | # @param [Hash] opts info on the browser to connect to 11 | # @option opts [String] host the hostname / IP address of the Webkit remote 12 | # debugging server 13 | # @option opts [Integer] port the port that the Webkit remote debugging 14 | # server listens to 15 | # @option opts [WebkitRemote::Process] process a process on the local machine 16 | # to connect to; the process will automatically be stopped when the 17 | # debugging connection is closed; the host and port will be configured 18 | # automatically 19 | # @option opts [Boolean] stop_process if true, the WebkitRemote::Process 20 | # passed to the constructor will be automatically stopped 21 | # @raise [SystemCallError] if the connection could not be established; this 22 | # most likely means that there is no remote debugging server at the given 23 | # host / port 24 | def initialize(opts = {}) 25 | if opts[:process] 26 | @process = opts[:process] 27 | @stop_process = opts.fetch :stop_process, false 28 | @host = 'localhost' 29 | @port = process.port 30 | else 31 | @process = nil 32 | @stop_process = false 33 | @host = opts[:host] || 'localhost' 34 | @port = opts[:port] || 9292 35 | end 36 | @closed = false 37 | 38 | @http = Net::HTTP.start @host, @port 39 | end 40 | 41 | # Closes the connection the browser. 42 | # 43 | # If the Browser instance was given a WebkitRemote::Process, the process will 44 | # also be stopped. This instance becomes useless after closing. 45 | # 46 | # @return [WebkitRemote::Browser] self 47 | def close 48 | return self if @closed 49 | @closed = true 50 | @http.finish 51 | @http = nil 52 | @process.stop if @stop_process 53 | self 54 | end 55 | 56 | # Retrieves the tabs that are currently open in the browser. 57 | # 58 | # These tabs can be used to start debugging. 59 | # 60 | # @return [Array] the open tabs 61 | def tabs 62 | http_response = @http.request Net::HTTP::Get.new('/json') 63 | tabs = JSON.parse(http_response.body).map do |json_tab| 64 | title = json_tab['title'] 65 | url = json_tab['url'] 66 | debug_url = json_tab['webSocketDebuggerUrl'] 67 | Tab.new self, debug_url, title: title, url: url 68 | end 69 | # HACK(pwnall): work around the nasty Google Hangouts integration 70 | tabs.select do |tab| 71 | tab.url != 'chrome-extension://nkeimhogjdpnpccoofpliimaahmaaome/background.html' 72 | end 73 | end 74 | 75 | # @return [Boolean] if true, a WebkitRemote::Process will be stopped when 76 | # this browser connection is closed 77 | attr_reader :stop_process 78 | alias_method :stop_process?, :stop_process 79 | 80 | # Changes the automated WebkitRemote::Process stopping behavior. 81 | # 82 | # This should only be set to true if this Browser instance was given a 83 | # WebkitRemote::Process at creation time. 84 | # 85 | # @param [Boolean] new_stop_process if true, the WebkitRemote::Process 86 | # passed to this instance's constructor will be stopped when this 87 | # connection is closed 88 | # @return [Boolean] new_stop_process 89 | def stop_process=(new_stop_process) 90 | if new_stop_process 91 | unless @process 92 | raise ArgumentError, "Browser instance not backed by a Webkit process" 93 | end 94 | @stop_process = true 95 | else 96 | @stop_process = false 97 | end 98 | new_stop_process 99 | end 100 | 101 | # @return [WebkitRemote::Process, nil] Process instance passed to this 102 | # connection's constructor 103 | attr_reader :process 104 | 105 | # @return [String] hostname or IP of the Webkit remote debugging server 106 | attr_reader :host 107 | 108 | # @return [Integer] port that the Webkit remote debugging server listens on 109 | attr_reader :port 110 | 111 | # @return [Boolean] if true, the connection to the remote debugging server 112 | # has been closed, and this instance is mostly useless 113 | attr_reader :closed 114 | alias_method :closed?, :closed 115 | 116 | # Clean up when garbage collected. 117 | def finalize 118 | close unless @closed 119 | end 120 | 121 | # References a tab open in a Webkit process with a remote debugging server. 122 | class Tab 123 | # @return [Webkit::Remote] connection to the browser that this tab belongs to 124 | attr_reader :browser 125 | 126 | # @return [String] URL of the tab's remote debugging endpoint 127 | attr_reader :debug_url 128 | 129 | # @return [String, nil] title of the Web page open in the browser tab 130 | attr_reader :title 131 | 132 | # @return [String, nil] URL of the Web page open in the browser tab 133 | attr_reader :url 134 | 135 | # Creates a tab reference. 136 | # 137 | # @param [WebkitRemote::Browser] browser the master debugging connection to 138 | # the Webkit process 139 | # @param [String] debug_url URL of the tab's remote debugging endpoint 140 | # @param [Hash] metadata non-essential information about the tab 141 | # @option metadata [String, nil] title title of the page open in the browser 142 | # tab 143 | # @option metadata [String, nil] url URL of the page open in the browser tab 144 | def initialize(browser, debug_url, metadata) 145 | @browser = browser 146 | @debug_url = debug_url 147 | @title = metadata[:title] 148 | @url = metadata[:url] 149 | end 150 | end # class WebkitRemote::Browser::Tab 151 | 152 | end # class WebkitRemote::Browser 153 | 154 | end # namespace WebkitRemote 155 | -------------------------------------------------------------------------------- /lib/webkit_remote/client.rb: -------------------------------------------------------------------------------- 1 | module WebkitRemote 2 | 3 | # Client for the Webkit remote debugging protocol 4 | # 5 | # A client manages a single tab. 6 | class Client 7 | # Connects to the remote debugging server in a Webkit tab. 8 | # 9 | # @param [Hash] opts info on the tab to connect to 10 | # @option opts [WebkitRemote::Tab] tab reference to the tab whose debugger 11 | # server this RPC client connects to 12 | # @option opts [Boolean] close_browser if true, the session to the brower 13 | # that the tab belongs to will be closed when this RPC client's connection 14 | # is closed 15 | def initialize(opts = {}) 16 | unless tab = opts[:tab] 17 | raise ArgumentError, 'Target tab not specified' 18 | end 19 | @rpc = WebkitRemote::Rpc.new opts 20 | @debug_url = @rpc.debug_url 21 | @browser = tab.browser 22 | @close_browser = opts.fetch :close_browser, false 23 | @closed = false 24 | initialize_modules 25 | end 26 | 27 | # Closes the remote debugging connection. 28 | # 29 | # Call this method to avoid leaking resources. 30 | # 31 | # @return [WebkitRemote::Rpc] self 32 | def close 33 | return if @closed 34 | @closed = true 35 | @rpc.close 36 | @rpc = nil 37 | @browser.close if @close_browser 38 | self 39 | end 40 | 41 | # @return [Boolean] if true, the connection to the remote debugging server 42 | # has been closed, and this instance is mostly useless 43 | attr_reader :closed 44 | alias_method :closed?, :closed 45 | 46 | # @return [Boolean] if true, the master debugging connection to the browser 47 | # associated with the client's tab will be automatically closed when this 48 | # RPC client's connection is closed; in turn, this might stop the browser 49 | # process 50 | attr_accessor :close_browser 51 | alias_method :close_browser?, :close_browser 52 | 53 | # Continuously reports events sent by the remote debugging server. 54 | # 55 | # @yield once for each RPC event received from the remote debugger; break to 56 | # stop the event listening loop 57 | # @yieldparam [WebkitRemote::Event] event an instance of an Event sub-class 58 | # that best represents the received event 59 | # @return [WebkitRemote::Client] self 60 | def each_event 61 | @rpc.each_event do |rpc_event| 62 | yield WebkitRemote::Event.for(rpc_event, self) 63 | end 64 | self 65 | end 66 | 67 | # Waits for the remote debugging server to send a specific event. 68 | # 69 | # @param (see WebkitRemote::Event#matches?) 70 | # @return [Array] all the events received, including the 71 | # event that matches the class requirement 72 | def wait_for(conditions) 73 | unless WebkitRemote::Event.can_receive? self, conditions 74 | raise ArgumentError, "Cannot receive event with #{conditions.inspect}" 75 | end 76 | 77 | events = [] 78 | each_event do |event| 79 | events << event 80 | break if event.matches?(conditions) 81 | end 82 | events 83 | end 84 | 85 | # Removes all the remote debugging data cached by this client. 86 | # 87 | # Some modules accumulate data throughout the debigging process. For example, 88 | # WebkitRemote::Client::Remote#remote_eval and 89 | # WebkitRemote::Events#ConsoleMessage build Ruby equivalents of the returned 90 | # JavaScript objects, and prevent Chrome from garbage-collecting the 91 | # returned JavaScript objects. 92 | # 93 | # Although modules have individual methods for releasing this data, such as 94 | # WebkitRemote::Client::RemoteGroup#release, keeping track of individual data 95 | # items is very inconvenient. Therefore, if you need to run a WebkitRemote 96 | # client for an extended period of time, you might find it easier to 97 | # periodically call this method. 98 | def clear_all 99 | clear_modules 100 | end 101 | 102 | # @return [WebkitRemote::Rpc] the WebSocket RPC client; useful for making raw 103 | # RPC calls to unsupported methods 104 | attr_reader :rpc 105 | 106 | # @return [WebkitRemote::Browser] master session to the browser that owns the 107 | # tab debugged by this client 108 | attr_reader :browser 109 | 110 | # Registers a module initializer. 111 | def self.initializer(name) 112 | before_name = :"initialize_modules_before_#{name}" 113 | alias_method before_name, :initialize_modules 114 | private before_name 115 | remove_method :initialize_modules 116 | eval <] 34 | attr_reader :console_messages 35 | 36 | # @private Called by the ConsoleMessage event constructor 37 | def console_add_message(message) 38 | @console_messages << message 39 | end 40 | 41 | # @private Called by the Client constructor to set up Console data. 42 | def initialize_console 43 | @console_events = false 44 | @console_messages = [] 45 | end 46 | end # module WebkitRemote::Client::Console 47 | 48 | initializer :initialize_console 49 | clearer :clear_console 50 | include WebkitRemote::Client::Console 51 | 52 | # Data about an entry in the debugger console. 53 | class ConsoleMessage 54 | # @return [String] the message text 55 | attr_reader :text 56 | 57 | # @return [Array] extra arguments given 58 | # to the message 59 | attr_reader :params 60 | 61 | # @return [Symbol] message severity 62 | # 63 | # The documented values are :debug, :error, :log, :tip, and :warning. 64 | attr_reader :level 65 | 66 | # @return [Symbol] the component that produced this message 67 | # 68 | # The documented values are :console_api, :html, :javascript, :network, 69 | # :other, :wml, and :xml. 70 | attr_reader :reason 71 | 72 | # @return [WebkitRemote::Client::NetworkResource] resource associated with 73 | # this message 74 | # 75 | # This is set for console messages that indicate network errors. 76 | attr_reader :network_resource 77 | 78 | # @return [String] the URL of the file that caused this message 79 | attr_reader :source_url 80 | 81 | # @return [Integer] the line number of the statement that caused this message 82 | attr_reader :source_line 83 | 84 | # @return [Array>] JavaScript stack trace to the 85 | # statement that caused this message 86 | attr_reader :stack_trace 87 | 88 | # @private Use Event#for instead of calling this constructor directly. 89 | # 90 | # @param [Hash] the raw JSON for a Message object in the 91 | # Console domain, returned by a RPC call to a Webkit debugging server 92 | # @ 93 | def initialize(raw_message, client) 94 | @level = (raw_message['level'] || 'error').to_sym 95 | @source_line = raw_message['line'] ? raw_message['line'].to_i : nil 96 | if raw_message['networkRequestId'] 97 | @network_resource = 98 | client.network_resource raw_message['networkRequestId'] 99 | else 100 | @network_resource = nil 101 | end 102 | if raw_message['parameters'] 103 | @params = raw_message['parameters'].map do |raw_object| 104 | WebkitRemote::Client::JsObject.for raw_object, client, nil 105 | end 106 | else 107 | @params = [] 108 | end 109 | @params.freeze 110 | if raw_message['source'] 111 | @reason = raw_message['source'].gsub('-', '_').to_sym 112 | else 113 | @reason = :other 114 | end 115 | @stack_trace = self.class.parse_stack_trace raw_message['stackTrace'] 116 | @text = raw_message['text'] 117 | @source_url = raw_message['url'] 118 | end 119 | 120 | # Releases the JavaScript objects referenced by this message's parameters. 121 | def release_params 122 | @params.each do |param| 123 | if param.kind_of?(WebkitRemote::Client::JsObject) 124 | param.release 125 | end 126 | end 127 | end 128 | 129 | # Parses a StackTrace object returned by a RPC request. 130 | # 131 | # @param [Array] raw_stack_trace the raw StackTrace object 132 | # in the Console domain returned by a RPC request 133 | # @return [Array] Ruby-friendly stack trace 134 | def self.parse_stack_trace(raw_stack_trace) 135 | return nil unless raw_stack_trace 136 | 137 | raw_stack_trace.map do |raw_frame| 138 | frame = {} 139 | if raw_frame['columnNumber'] 140 | frame[:column] = raw_frame['columnNumber'].to_i 141 | end 142 | if raw_frame['lineNumber'] 143 | frame[:line] = raw_frame['lineNumber'].to_i 144 | end 145 | if raw_frame['functionName'] 146 | frame[:function] = raw_frame['functionName'] 147 | end 148 | if raw_frame['url'] 149 | frame[:url] = raw_frame['url'] 150 | end 151 | frame 152 | end 153 | end 154 | end # class WebkitRemote::Client::ConsoleMessage 155 | 156 | end # namespace WebkitRemote::Client 157 | 158 | end # namespace WebkitRemote 159 | -------------------------------------------------------------------------------- /lib/webkit_remote/client/console_events.rb: -------------------------------------------------------------------------------- 1 | module WebkitRemote 2 | 3 | class Event 4 | 5 | # Emitted when a console message is produced. 6 | class ConsoleMessage < WebkitRemote::Event 7 | register 'Console.messageAdded' 8 | 9 | # @return [WebkitRemote::Client::ConsoleMessage] the new message 10 | attr_reader :message 11 | 12 | # @return [String] the message text 13 | def text 14 | @message.text 15 | end 16 | 17 | # @return [Symbol] message severity 18 | # 19 | # The documented values are :debug, :error, :log, :tip, and :warning. 20 | def level 21 | @message.level 22 | end 23 | 24 | # @return [Symbol] the component that produced this message 25 | # 26 | # The documented values are :console_api, :html, :javascript, :network, 27 | # :other, :wml, and :xml. 28 | def reason 29 | @message.reason 30 | end 31 | 32 | # @private Use Event#for instead of calling this constructor directly. 33 | def initialize(rpc_event, client) 34 | super 35 | 36 | if raw_message = raw_data['message'] 37 | @message = WebkitRemote::Client::ConsoleMessage.new raw_data['message'], 38 | client 39 | client.console_add_message @message 40 | else 41 | @message = nil 42 | end 43 | end 44 | 45 | # @private Use Event#can_receive instead of calling this directly. 46 | def self.can_reach?(client) 47 | client.console_events 48 | end 49 | end # class WebkitRemote::Event::ConsoleMessage 50 | 51 | # Emitted when the console is cleared. 52 | class ConsoleCleared < WebkitRemote::Event 53 | register 'Console.messagesCleared' 54 | 55 | # @private Use Event#for instead of calling this constructor directly. 56 | def initialize(rpc_event, client) 57 | super 58 | client.console_cleared 59 | end 60 | 61 | # @private Use Event#can_receive instead of calling this directly. 62 | def self.can_reach?(client) 63 | client.console_events 64 | end 65 | end # class WebkitRemote::Event::ConsoleCleared 66 | 67 | end # namespace WebkitRemote::Event 68 | 69 | end # namepspace WebkitRemote 70 | -------------------------------------------------------------------------------- /lib/webkit_remote/client/dom.rb: -------------------------------------------------------------------------------- 1 | module WebkitRemote 2 | 3 | class Client 4 | 5 | # API for the DOM domain. 6 | module Dom 7 | # @return [WebkitRemote::Client::DomNode] the root DOM node 8 | def dom_root 9 | @dom_root ||= dom_root! 10 | end 11 | 12 | # Obtains the root DOM node, bypassing the cache. 13 | # 14 | # @return [WebkitRemote::Client::DomNode] the root DOM node 15 | def dom_root! 16 | result = @rpc.call 'DOM.getDocument' 17 | @dom_root = dom_update_node result['root'] 18 | end 19 | 20 | # Removes all the cached DOM information. 21 | # 22 | # @return [WebkitRemote::Client] self 23 | def clear_dom 24 | @dom_root = nil 25 | @dom_nodes.clear 26 | self 27 | end 28 | 29 | # Looks up cached information about a DOM node. 30 | # 31 | # @private Use WebkitRemote::Client::Dom#query_selector or the other public 32 | # APIs instead of calling this directly 33 | # 34 | # @param [String] remote_id value of the nodeId attribute in the JSON 35 | # returned by a Webkit remote debugging server 36 | # @return [WebkitRemote::Client::DomNode] cached information about the given 37 | # DOM node 38 | def dom_node(remote_id) 39 | @dom_nodes[remote_id] ||= WebkitRemote::Client::DomNode.new remote_id, self 40 | end 41 | 42 | # @private Called by the Client constructor to set up Dom data. 43 | def initialize_dom 44 | @dom_nodes = {} 45 | end 46 | 47 | # Updates cached information about a DOM node. 48 | # 49 | # @param [Hash] raw_node a Node data structure in the DOM 50 | # domain, as returned by a raw JSON RPC call to a Webkit remote debugging 51 | # server 52 | # @return [WebkitRemote::Client::DomNode] the updated cached information 53 | def dom_update_node(raw_node) 54 | remote_id = raw_node['nodeId'] 55 | dom_node(remote_id).update_all raw_node 56 | end 57 | end # module WebkitRemote::Client::Dom 58 | 59 | initializer :initialize_dom 60 | clearer :clear_dom 61 | include WebkitRemote::Client::Dom 62 | 63 | # Cached information about a DOM node. 64 | class DomNode 65 | # @return [Array] children nodes 66 | attr_reader :children 67 | 68 | # @return [String] the node's local name 69 | attr_reader :local_name 70 | # @return [String] the node's name 71 | attr_reader :name 72 | # @return [String] the node's value 73 | attr_reader :value 74 | # @return [Symbol] the DOM node type (such as :element, :text, :attribute) 75 | attr_reader :node_type 76 | 77 | # @return [String] name, for attribute nodes 78 | attr_reader :attr_name 79 | # @return [String] value, for attribute nodes 80 | attr_reader :attr_value 81 | 82 | # @return [String] internal subset, for doctype nodes 83 | attr_reader :internal_subset 84 | # @return [String] public ID, for doctype nodes 85 | attr_reader :public_id 86 | # @return [String] system ID, for doctype nodes 87 | attr_reader :system_id 88 | 89 | # @return [WebkitRemote::Client::DomNode] content document, for frameowner 90 | # nodes 91 | # @return [String] the document URL, for document and frameowner nodes 92 | attr_reader :document_url 93 | # @return [String] the XML version, for document nodes 94 | attr_reader :xml_version 95 | 96 | # @return [Hash] the node's attributes 97 | def attributes 98 | @attributes ||= attributes! 99 | end 100 | 101 | # Retrieves this node's attributes, bypassing its cache. 102 | # 103 | # @return [Hash] the node's attributes 104 | def attributes! 105 | result = @client.rpc.call 'DOM.getAttributes', nodeId: @remote_id 106 | @attributes = Hash[result['attributes'].each_slice(2).to_a] 107 | end 108 | 109 | # @return [WebkitRemote::Client::JsObject] this node's JavaScript object 110 | def js_object 111 | @js_object ||= js_object! 112 | end 113 | 114 | # Retrieves this node's JavaScript object, bypassing the node's cache. 115 | # 116 | # @param [String] group the name of an object group (think memory pools); the 117 | # objects in a group can be released together by one call to 118 | # WebkitRemote::Client::JsObjectGroup#release 119 | # @return [WebkitRemote::Client::JsObject] this node's JavaScript object 120 | def js_object!(group = nil) 121 | group ||= @client.object_group_auto_name 122 | result = @client.rpc.call 'DOM.resolveNode', nodeId: @remote_id, 123 | groupName: group 124 | WebkitRemote::Client::JsObject.for result['object'], @client, group 125 | end 126 | 127 | # @return [String] HTML markup for the node and all its contents 128 | def outer_html 129 | @outer_html ||= outer_html! 130 | end 131 | 132 | # @return [String] HTML markup for the node and all its contents 133 | def outer_html! 134 | result = @client.rpc.call 'DOM.getOuterHTML', nodeId: @remote_id 135 | @outer_html = result['outerHTML'] 136 | end 137 | 138 | # Retrieves the first descendant of this node that matches a CSS selector. 139 | # 140 | # @param [String] css_selector the CSS selector that must be matched by the 141 | # returned node 142 | # @return [WebkitRemote::Client::DomNode] the first DOM node in this node's 143 | # subtree that matches the given selector; if no such node exists, nil is 144 | # returned 145 | def query_selector(css_selector) 146 | result = @client.rpc.call 'DOM.querySelector', nodeId: @remote_id, 147 | selector: css_selector 148 | node_id = result['nodeId'] 149 | return nil if node_id == 0 150 | @client.dom_node result['nodeId'] 151 | end 152 | 153 | # Retrieves all this node's descendants that match a CSS selector. 154 | # 155 | # @param [String] css_selector the CSS selector used to filter this node's 156 | # subtree 157 | # @return [Array] DOM nodes in this node's 158 | # subtree that match the given selector 159 | def query_selector_all(css_selector) 160 | result = @client.rpc.call 'DOM.querySelectorAll', nodeId: @remote_id, 161 | selector: css_selector 162 | result['nodeIds'].map { |remote_id| @client.dom_node remote_id } 163 | end 164 | 165 | # Deletes one of the node (element)'s attributes. 166 | # 167 | # @param [String] attr_name name of the attribute that will be deleted 168 | # @return [WebkitRemote::Client::DomNode] self 169 | def remove_attribute(attr_name) 170 | @attributes.delete attr_name if @attributes 171 | @client.rpc.call 'DOM.removeAttribute', nodeId: @remote_id, name: attr_name 172 | self 173 | end 174 | 175 | # Removes this node from the document. 176 | # 177 | # @return [WebkitRemote::Client::DomNode] self 178 | def remove 179 | @client.rpc.call 'DOM.removeNode', nodeId: @remote_id 180 | self 181 | end 182 | 183 | # Highlights this DOM node. 184 | # 185 | # @param [Hash] options colors to be used for highlighting 186 | # @option options [Hash] margin color used for highlighting 187 | # the element's border 188 | # @option options [Hash] border color used for highlighting 189 | # the element's border 190 | # @option options [Hash] padding color used for highlighting 191 | # the element's padding 192 | # @option options [Hash] content color used for highlighting 193 | # the element's content 194 | # @option options [Boolean] tooltip if true, a tooltip containing node 195 | # information is also shown 196 | def highlight!(options) 197 | config = {} 198 | config[:marginColor] = options[:margin] if options[:margin] 199 | config[:borderColor] = options[:border] if options[:border] 200 | config[:paddingColor] = options[:padding] if options[:padding] 201 | config[:contentColor] = options[:content] if options[:content] 202 | config[:showInfo] = true if options[:tooltip] 203 | @client.rpc.call 'DOM.highlightNode', nodeId: @remote_id, 204 | highlightConfig: config 205 | end 206 | 207 | # @private Use WebkitRemote::Client::Dom#dom_node instead of calling this 208 | def initialize(remote_id, client) 209 | @remote_id = remote_id 210 | @client = client 211 | 212 | @attributes = nil 213 | @attr_name = nil 214 | @attr_value = nil 215 | @children = nil 216 | @content_document = nil 217 | @document_url = nil 218 | @internal_subset = nil 219 | @js_object = nil 220 | @local_name = nil 221 | @name = nil 222 | @node_type = nil 223 | @outer_html = nil 224 | @public_id = nil 225 | @system_id = nil 226 | @value = nil 227 | @xml_version = nil 228 | 229 | initialize_modules 230 | end 231 | 232 | def initialize_modules 233 | end 234 | private :initialize_modules 235 | 236 | # Registers a module initializer. 237 | def self.initializer(name) 238 | before_name = :"initialize_modules_before_#{name}" 239 | alias_method before_name, :initialize_modules 240 | private before_name 241 | remove_method :initialize_modules 242 | eval <] raw_node a Node data structure in the DOM 256 | # domain, as returned by a raw JSON RPC call to a Webkit remote debugging 257 | # server 258 | # @return [WebkitRemote::Client::DomNode] self 259 | def update_all(raw_node) 260 | if raw_node['attributes'] 261 | @attributes = Hash[raw_node['attributes'].each_slice(2).to_a] 262 | end 263 | if raw_node['children'] 264 | @children = raw_node['children'].map do |child_node| 265 | @client.dom_update_node child_node 266 | end 267 | end 268 | if raw_node['contentDocument'] 269 | @content_document = @client.dom_update_node raw_node['contentDocument'] 270 | end 271 | @document_url = raw_node['documentURL'] if raw_node['documentURL'] 272 | @internal_subset = raw_node['internalSubset'] if raw_node['internalSubset'] 273 | @node_local_name = raw_node['localName'] if raw_node['localName'] 274 | @attr_name = raw_node['name'] if raw_node['name'] 275 | @name = raw_node['nodeName'] if raw_node['nodeName'] 276 | if raw_node['nodeType'] 277 | @node_type = NODE_TYPES[raw_node['nodeType'].to_i] || raw_node['nodeType'] 278 | end 279 | @value = raw_node['nodeValue'] if raw_node['nodeValue'] 280 | @public_id = raw_node['publicId'] if raw_node['publicId'] 281 | @system_id = raw_node['systemId'] if raw_node['systemId'] 282 | @attr_value = raw_node['value'] if raw_node['value'] 283 | @xml_version = raw_node['xmlVersion'] if raw_node['xmlVersion'] 284 | 285 | self 286 | end 287 | 288 | # Maps numeric DOM types to their symbolic representation. 289 | NODE_TYPES = { 290 | 1 => :element, 2 => :attribute, 3 => :text, 4 => :cdata_section, 291 | 5 => :entity_reference, 6 => :entity, 7 => :processing_instruction, 292 | 8 => :comment, 9 => :document, 10 => :document_type, 293 | 11 => :document_fragment, 12 => :notation 294 | }.freeze 295 | end # class WebkitRemote::Client::DomNode 296 | 297 | end # namespace WebkitRemote::Client 298 | 299 | end # namespace WebkitRemote 300 | 301 | -------------------------------------------------------------------------------- /lib/webkit_remote/client/dom_events.rb: -------------------------------------------------------------------------------- 1 | module WebkitRemote 2 | 3 | class Event 4 | 5 | # Emitted when the entire document has changed, and all DOM structure is lost. 6 | class DomReset < WebkitRemote::Event 7 | register 'Dom.documentUpdated' 8 | 9 | # @private Use Event#for instead of calling this constructor directly. 10 | def initialize(rpc_event, client) 11 | super 12 | client.clear_dom 13 | end 14 | end # class WebkitRemote::Event::DomReset 15 | 16 | end # namespace WebkitRemote::Event 17 | 18 | end # namepspace WebkitRemote 19 | -------------------------------------------------------------------------------- /lib/webkit_remote/client/dom_runtime.rb: -------------------------------------------------------------------------------- 1 | module WebkitRemote 2 | 3 | class Client 4 | 5 | class JsObject 6 | # @return [WebkitRemote::Client::DomNode] the DOM node wrapped by this 7 | # JavaScript object 8 | def dom_node 9 | @dom_node ||= dom_node! 10 | end 11 | 12 | # Fetches the wrapped DOM node, bypassing the object's cache. 13 | # 14 | # @return [WebkitRemote::Client::DomNode] the DOM domain object wrapped by 15 | # this JavaScript object 16 | def dom_node! 17 | result = @client.rpc.call 'DOM.requestNode', objectId: @remote_id 18 | @dom_node = if result['nodeId'] 19 | @client.dom_node result['nodeId'] 20 | else 21 | nil 22 | end 23 | end 24 | 25 | # @private Called by the JsObject constructor. 26 | def initialize_dom 27 | @dom_node = nil 28 | end 29 | initializer :initialize_dom 30 | end # class WebkitRemote::Client::JsObject 31 | 32 | end # namespace WebkitRemote::Client 33 | 34 | end # namespace WebkitRemote 35 | -------------------------------------------------------------------------------- /lib/webkit_remote/client/input.rb: -------------------------------------------------------------------------------- 1 | module WebkitRemote 2 | 3 | class Client 4 | 5 | # API for the Input domain. 6 | module Input 7 | # Dispatches a mouse event. 8 | # 9 | # @param [Symbol] type the event type (:move, :down, :up) 10 | # @param [Integer] x the X coordinate, relative to the main frame's viewport 11 | # @param [Integer] y the Y coordinate, relative to the main frame's viewport 12 | # @param [Hash] opts optional information 13 | # @option opts [Symbol] button :left, :right, :middle, or nil (none); nil by 14 | # default 15 | # @option opts [Array] modifiers combination of :alt, :ctrl, :shift, 16 | # and :command / :meta (empty by default) 17 | # @option opts [Number] time the event's time, as a JavaScript timestamp 18 | # @option opts [Number] clicks number of times the mouse button was clicked 19 | # (0 by default) 20 | # @return [WebkitRemote::Client] self 21 | def mouse_event(type, x, y, opts = {}) 22 | options = { x: x, y: y } 23 | options[:type] = case type 24 | when :move 25 | 'mouseMoved' 26 | when :down 27 | 'mousePressed' 28 | when :up 29 | 'mouseReleased' 30 | else 31 | raise RuntimeError, "Unsupported mouse event type #{type}" 32 | end 33 | 34 | options[:timestamp] = opts[:time] if opts[:time] 35 | options[:clickCount] = opts[:clicks] if opts[:clicks] 36 | if opts[:button] 37 | options[:button] = opts[:button].to_s 38 | else 39 | options[:button] = 'none' 40 | end 41 | if opts[:modifiers] 42 | flags = 0 43 | opts[:modifiers].each do |modifier| 44 | flags |= case modifier 45 | when :alt 46 | 1 47 | when :ctrl 48 | 2 49 | when :command, :meta 50 | 4 51 | when :shift 52 | 8 53 | end 54 | end 55 | options[:modifiers] = flags 56 | end 57 | 58 | @rpc.call 'Input.dispatchMouseEvent', options 59 | self 60 | end 61 | 62 | # Dispatches a keyboard event. 63 | # 64 | # @param [Symbol] type the event type (:char, :down, :up, :raw_down) 65 | # @param [Hash] opts optional information 66 | # @option opts [Array] modifiers combination of :alt, :ctrl, :shift, 67 | # and :command / :meta (empty by default) 68 | # @option opts [Number] time the event's time, as a JavaScript timestamp 69 | # @option opts [Number] clicks number of times the mouse button was clicked 70 | # (0 by default) 71 | # @option opts [String] text as generated by processing a virtual key code 72 | # with a keyboard layout; not needed for :up and :raw_down events; 73 | # ('' by default) 74 | # @option opts [String] unmodified_text text that would have been generated 75 | # by the keyboard if no modifiers were pressed (except for shift); 76 | # useful for shortcut (accelerator) key handling; ('' by default) 77 | # @option opts [Number] vkey the Windows virtual key code for the key; 78 | # see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent#Virtual_key_codes 79 | # @option opts [Number] key the unique key identifier (e.g., 'U+0041'); 80 | # ('' by default) 81 | # @option opts [Boolean] auto_repeat true if the event was generated by an 82 | # auto-repeat while the key was being held down 83 | # @option opts [Boolean] keypad true if the event was generated from the 84 | # keypad 85 | # @option opts [Boolean] system_key true if the event was a system key event 86 | # @return [WebkitRemote::Client] self 87 | def key_event(type, opts) 88 | options = {} 89 | options[:type] = case type 90 | when :char 91 | 'char' 92 | when :down 93 | 'keyDown' 94 | when :up 95 | 'keyUp' 96 | when :raw_down 97 | 'rawKeyDown' 98 | else 99 | raise RuntimeError, "Unsupported keyboard event type #{type}" 100 | end 101 | 102 | options[:timestamp] = opts[:time] if opts[:time] 103 | if opts[:modifiers] 104 | flags = 0 105 | opts[:modifiers].each do |modifier| 106 | flags |= case modifier 107 | when :alt 108 | 1 109 | when :ctrl 110 | 2 111 | when :command, :meta 112 | 4 113 | when :shift 114 | 8 115 | end 116 | end 117 | options[:modifiers] = flags 118 | end 119 | 120 | options[:key] = opts[:key] if opts[:key] 121 | options[:windowsVirtualKeyCode] = opts[:vkey] if opts[:vkey] 122 | options[:unmodifiedText] = opts[:unmodified_text] if opts[:unmodified_text] 123 | if opts[:text] 124 | options[:text] = opts[:text] 125 | options[:unmodifiedText] ||= opts[:text] 126 | end 127 | options[:autoRepeat] = true if opts[:auto_repeat] 128 | options[:isKeypad] = true if opts[:keypad] 129 | options[:isSystemKey] = true if opts[:system_key] 130 | 131 | @rpc.call 'Input.dispatchKeyEvent', options 132 | self 133 | end 134 | end # module WebkitRemote::Client::Input 135 | 136 | include WebkitRemote::Client::Input 137 | 138 | end # namespace WebkitRemote::Client 139 | 140 | end # namespace WebkitRemote 141 | -------------------------------------------------------------------------------- /lib/webkit_remote/client/network.rb: -------------------------------------------------------------------------------- 1 | module WebkitRemote 2 | 3 | class Client 4 | 5 | # API for the Network domain. 6 | module Network 7 | # Enables or disables the generation of events in the Network domain. 8 | # 9 | # @param [Boolean] new_network_events if true, the browser debugger will 10 | # generate Network.* events 11 | def network_events=(new_network_events) 12 | new_network_events = !!new_network_events 13 | if new_network_events != network_events 14 | @rpc.call(new_network_events ? 'Network.enable' : 'Network.disable') 15 | @network_events = new_network_events 16 | end 17 | new_network_events 18 | end 19 | 20 | # Enables or disables the use of the network cache. 21 | # 22 | # @param [Boolean] new_disable_cache if true, the browser will not use its 23 | # network cache, and will always generate HTTP requests 24 | def disable_cache=(new_disable_cache) 25 | new_disable_cache = !!new_disable_cache 26 | if new_disable_cache != disable_cache 27 | @rpc.call 'Network.setCacheDisabled', cacheDisabled: new_disable_cache 28 | @disable_cache = new_disable_cache 29 | end 30 | new_disable_cache 31 | end 32 | 33 | # Sets the User-Agent header that the browser will use to identify itself. 34 | # 35 | # @param [String] new_user_agent will be used instead of the browser's 36 | # hard-coded User-Agent header 37 | def user_agent=(new_user_agent) 38 | if new_user_agent != user_agent 39 | @rpc.call 'Network.setUserAgentOverride', userAgent: new_user_agent 40 | @user_agent = new_user_agent 41 | end 42 | new_user_agent 43 | end 44 | 45 | # Sets extra headers to be sent with every HTTP request. 46 | # 47 | # @param [Hash] new_extra_headers HTTP headers to be added to 48 | # every HTTP request sent by the browser 49 | def http_headers=(new_http_headers) 50 | new_http_headers = Hash[new_http_headers.map { |k, v| 51 | [k.to_s, v.to_s] 52 | }].freeze 53 | if new_http_headers != http_headers 54 | @rpc.call 'Network.setExtraHTTPHeaders', headers: new_http_headers 55 | @http_headers = new_http_headers 56 | end 57 | new_http_headers 58 | end 59 | 60 | # Checks if the debugger can clear the browser's cookies. 61 | # 62 | # @return [Boolean] true if WebkitRemote::Client::Network#clear_cookies can 63 | # be succesfully called 64 | def can_clear_cookies? 65 | response = @rpc.call 'Network.canClearBrowserCookies' 66 | !!response['result'] 67 | end 68 | 69 | # Removes all the cookies in the debugged browser. 70 | # 71 | # @return [WebkitRemote::Client] self 72 | def clear_cookies 73 | @rpc.call 'Network.clearBrowserCookies' 74 | self 75 | end 76 | 77 | # Checks if the debugger can clear the browser's cache. 78 | # 79 | # @return [Boolean] true if WebkitRemote::Client::Network#clear_network_cache 80 | # can be succesfully called 81 | def can_clear_network_cache? 82 | response = @rpc.call 'Network.canClearBrowserCache' 83 | !!response['result'] 84 | end 85 | 86 | # Removes all the cached data in the debugged browser. 87 | # 88 | # @return [WebkitRemote::Client] self 89 | def clear_network_cache 90 | @rpc.call 'Network.clearBrowserCache' 91 | self 92 | end 93 | 94 | # @return [Boolean] true if the debugger generates Network.* events 95 | attr_reader :network_events 96 | 97 | # @return [Array] the resources 98 | # fetched during the debugging session 99 | # 100 | # This is only populated when Network events are received. 101 | attr_reader :network_resources 102 | 103 | # @return [Boolean] true if the browser's network cache is disabled, so every 104 | # resource load generates an HTTP request 105 | attr_reader :disable_cache 106 | 107 | # @return [String] replaces the brower's built-in User-Agent string 108 | attr_reader :user_agent 109 | 110 | # @return [Hash] 111 | attr_reader :http_headers 112 | 113 | # Looks up network resources by IDs assigned by the WebKit remote debugger. 114 | # 115 | # @private Use the #resource property of Network events instead of calling 116 | # this directly. 117 | # 118 | # @param [String] remote_id the WebKit-assigned request_id 119 | # @return [WebkitRemote::Client::NetworkResource] the cached information 120 | # about the resource with the given ID 121 | def network_resource(remote_id) 122 | if @network_resource_hash[remote_id] 123 | return @network_resource_hash[remote_id] 124 | end 125 | resource = WebkitRemote::Client::NetworkResource.new remote_id, self 126 | 127 | @network_resources << resource 128 | @network_resource_hash[remote_id] = resource 129 | end 130 | 131 | # Removes the cached network request information. 132 | # 133 | # @return [WebkitRemote::Client] self 134 | def clear_network 135 | @network_resource_hash.clear 136 | @network_resources.clear 137 | self 138 | end 139 | 140 | # @private Called by the Client constructor to set up Network data. 141 | def initialize_network 142 | @disable_cache = false 143 | @network_events = false 144 | @network_resources = [] 145 | @network_resource_hash = {} 146 | @user_agent = nil 147 | end 148 | end # module WebkitRemote::Client::Network 149 | 150 | initializer :initialize_network 151 | include WebkitRemote::Client::Network 152 | 153 | end # namespace WebkitRemote::Client 154 | 155 | end # namespace WebkitRemote 156 | -------------------------------------------------------------------------------- /lib/webkit_remote/client/network_events.rb: -------------------------------------------------------------------------------- 1 | require 'base64' 2 | 3 | module WebkitRemote 4 | 5 | class Event 6 | 7 | # Emitted when a chunk of data is received over the network. 8 | class NetworkData < WebkitRemote::Event 9 | register 'Network.dataReceived' 10 | 11 | # @return [WebkitRemote::Client::NetworkResource] information about the 12 | # resource fetched by this network operation 13 | attr_reader :resource 14 | 15 | # @return [Number] the event timestamp 16 | attr_reader :timestamp 17 | 18 | # @return [Number] number of bytes actually received 19 | attr_reader :bytes_received 20 | 21 | # @return [Number] number of data bytes received (after decompression) 22 | attr_reader :data_length 23 | 24 | # @private Use Event#for instead of calling this constructor directly. 25 | def initialize(rpc_event, client) 26 | super 27 | @data_length = raw_data['dataLength'] 28 | @bytes_received = raw_data['encodedDataLength'] 29 | @timestamp = raw_data['timestamp'] 30 | 31 | @resource = client.network_resource raw_data['requestId'] 32 | @resource.add_event self 33 | end 34 | end # class WebkitRemote::Event::NetworkData 35 | 36 | # Emitted when a resource fails to load. 37 | class NetworkFailure < WebkitRemote::Event 38 | register 'Network.loadingFailed' 39 | 40 | # @return [WebkitRemote::Client::NetworkResource] information about the 41 | # resource fetched by this network operation 42 | attr_reader :resource 43 | 44 | # @return [Number] the event timestamp 45 | attr_reader :timestamp 46 | 47 | # @return [String] the error message 48 | attr_reader :error 49 | 50 | # @return [Boolean] true if the request was canceled 51 | # 52 | # For example, CORS violations cause requests to be canceled. 53 | attr_reader :canceled 54 | 55 | # @private Use Event#for instead of calling this constructor directly. 56 | def initialize(rpc_event, client) 57 | super 58 | @canceled = !!raw_data['canceled'] 59 | @error = raw_data['errorText'] 60 | @timestamp = raw_data['timestamp'] 61 | 62 | @resource = client.network_resource raw_data['requestId'] 63 | @resource.set_canceled @canceled 64 | @resource.set_error @error 65 | @resource.add_event self 66 | end 67 | end # class WebkitRemote::Event::NetworkFailure 68 | 69 | # Emitted when a resource finishes loading from the network. 70 | class NetworkLoad < WebkitRemote::Event 71 | register 'Network.loadingFinished' 72 | 73 | # @return [WebkitRemote::Client::NetworkResource] information about the 74 | # resource fetched by this network operation 75 | attr_reader :resource 76 | 77 | # @return [Number] the event timestamp 78 | attr_reader :timestamp 79 | 80 | # @private Use Event#for instead of calling this constructor directly. 81 | def initialize(rpc_event, client) 82 | super 83 | @timestamp = raw_data['timestamp'] 84 | 85 | @resource = client.network_resource raw_data['requestId'] 86 | @resource.add_event self 87 | end 88 | end # class WebkitRemote::Event::NetworkLoad 89 | 90 | # Emitted when a resource is served from the local cache. 91 | class NetworkCacheHit < WebkitRemote::Event 92 | register 'Network.requestServedFromCache' 93 | 94 | # @return [WebkitRemote::Client::NetworkResource] information about the 95 | # resource fetched by this network operation 96 | attr_reader :resource 97 | 98 | # @private Use Event#for instead of calling this constructor directly. 99 | def initialize(rpc_event, client) 100 | super 101 | 102 | @resource = client.network_resource raw_data['requestId'] 103 | @resource.add_event self 104 | end 105 | end # class WebkitRemote::Event::NetworkCacheHit 106 | 107 | # Emitted right before a network request. 108 | class NetworkRequest < WebkitRemote::Event 109 | register 'Network.requestWillBeSent' 110 | 111 | # @return [WebkitRemote::Client::NetworkResource] information about the 112 | # resource fetched by this network operation 113 | attr_reader :resource 114 | 115 | # @return [WebkitRemote::Client::NetworkRequest] information about this 116 | # network request 117 | attr_reader :request 118 | 119 | # @return [String] the URL of the document that caused this network request 120 | attr_reader :document_url 121 | 122 | # @return [WebkitRemote::Client::NetworkRequestInitiator] cause for this 123 | # network request 124 | attr_reader :initiator 125 | 126 | # @return [WebkitRemote::Client::NetworkResponse] the HTTP redirect that 127 | # caused this request; can be nil 128 | attr_reader :redirect_response 129 | 130 | # @return [String] used to correlate events 131 | attr_reader :loader_id 132 | 133 | # @return [Number] the event timestamp 134 | attr_reader :timestamp 135 | 136 | # @private Use Event#for instead of calling this constructor directly. 137 | def initialize(rpc_event, client) 138 | super 139 | @document_url = raw_data['documentURL'] 140 | if raw_data['initiator'] 141 | @initiator = WebkitRemote::Client::NetworkRequestInitiator.new( 142 | raw_data['initiator']) 143 | end 144 | @loader_id = raw_data['loaderId'] 145 | if raw_data['request'] 146 | @request = WebkitRemote::Client::NetworkRequest.new( 147 | raw_data['request']) 148 | end 149 | if raw_data['redirectResponse'] 150 | @redirect_response = WebkitRemote::Client::NetworkResponse.new( 151 | raw_data['redirectResponse']) 152 | end 153 | @timestamp = raw_data['timestamp'] 154 | 155 | @resource = client.network_resource raw_data['requestId'] 156 | @resource.set_document_url @document_url 157 | @resource.set_initiator @initiator 158 | @resource.set_request @request 159 | # TODO(pwnall): consider tracking redirects 160 | @resource.add_event self 161 | end 162 | 163 | # @private Use Event#can_receive instead of calling this directly. 164 | def self.can_reach?(client) 165 | client.network_events 166 | end 167 | end # class WebkitRemote::Event::NetworkRequest 168 | 169 | # Emitted right after receiving a response to a network request. 170 | class NetworkResponse < WebkitRemote::Event 171 | register 'Network.responseReceived' 172 | 173 | # @return [WebkitRemote::Client::NetworkResource] information about the 174 | # resource fetched by this network operation 175 | attr_reader :resource 176 | 177 | # @return [WebkitRemote::Client::NetworkResponse] information about the HTTP 178 | # response behind this event 179 | attr_reader :response 180 | 181 | # @return [Symbol] the type of resource returned by this response; documented 182 | # values are :document, :font, :image, :other, :script, :stylesheet, 183 | # :websocket and :xhr 184 | attr_reader :type 185 | 186 | # @return [Number] the event timestamp 187 | attr_reader :timestamp 188 | 189 | # @return [String] used to correlate events 190 | attr_reader :loader_id 191 | 192 | # @private Use Event#for instead of calling this constructor directly. 193 | def initialize(rpc_event, client) 194 | super 195 | @loader_id = raw_data['loaderId'] 196 | if raw_data['response'] 197 | @response = WebkitRemote::Client::NetworkResponse.new( 198 | raw_data['response']) 199 | end 200 | @type = (raw_data['type'] || 'other').downcase.to_sym 201 | @timestamp = raw_data['timestamp'] 202 | 203 | @resource = client.network_resource raw_data['requestId'] 204 | @resource.set_response @response 205 | @resource.set_type @type 206 | @resource.add_event self 207 | end 208 | 209 | # @private Use Event#can_receive instead of calling this directly. 210 | def self.can_reach?(client) 211 | client.network_events 212 | end 213 | end # class WebkitRemote::Event::NetworkResponse 214 | 215 | end # namespace WebkitRemote::Event 216 | 217 | 218 | class Client 219 | 220 | # Wraps information about the network operations for retrieving a resource. 221 | class NetworkResource 222 | # @return [WebkitRemote::Client::NetworkRequest] network request (most likely 223 | # HTTP) used to fetch this resource 224 | attr_reader :request 225 | 226 | # @return [WebkitRemote::Client::NetworkRequest] network response that 227 | # contains this resource 228 | attr_reader :response 229 | 230 | # @return [Symbol] the type of this resource; documented values are 231 | # :document, :font, :image, :other, :script, :stylesheet, :websocket and 232 | # :xhr 233 | attr_reader :type 234 | 235 | # @return [String] the URL of the document that referenced this resource 236 | attr_reader :document_url 237 | 238 | # @return [WebkitRemote::Client::NetworkRequestInitiator] cause for this 239 | # resource to be fetched from the network 240 | attr_reader :initiator 241 | 242 | # @return [Boolean] true if the request fetch was canceled 243 | attr_reader :canceled 244 | 245 | # @return [String] error message, if the resource fetching failed 246 | attr_reader :error 247 | 248 | # @return [WebkitRemote::Event] last event connected to this resource; can be 249 | # used to determine the resource's status 250 | attr_reader :last_event 251 | 252 | # @return [WebkitRemote::Client] remote debugging client that reported this 253 | attr_reader :client 254 | 255 | # @return [String] request_id assigned by the remote WebKit debugger 256 | attr_reader :remote_id 257 | 258 | # Creates an empty network operation wrapper. 259 | # 260 | # @private Use WebkitRemote::Client::Network#network_op instead of calling 261 | # this directly. 262 | # @param [String] remote_id the request_id used by the remote debugging 263 | # server to identify this network operation 264 | # @param [WebkitRemote::Client] 265 | def initialize(remote_id, client) 266 | @remote_id = remote_id 267 | @client = client 268 | @request = nil 269 | @response = nil 270 | @type = nil 271 | @document_url = nil 272 | @initiator = nil 273 | @canceled = false 274 | @last_event = nil 275 | @body = false 276 | end 277 | 278 | # @return [String] the contents of the resource 279 | def body 280 | @body ||= body! 281 | end 282 | 283 | # Re-fetches the resource from the Webkit remote debugging server. 284 | # 285 | # @return [String] the contents of the resource 286 | def body! 287 | result = @client.rpc.call 'Network.getResponseBody', requestId: @remote_id 288 | if result['base64Encoded'] 289 | @body = Base64.decode64 result['body'] 290 | else 291 | @body = result['body'] 292 | end 293 | end 294 | 295 | # @private Rely on the event processing code to set this property. 296 | def set_canceled(new_canceled) 297 | @canceled ||= new_canceled 298 | end 299 | 300 | # @private Rely on the event processing code to set this property. 301 | def set_document_url(new_document_url) 302 | return if new_document_url == nil 303 | @document_url = new_document_url 304 | end 305 | 306 | # @private Rely on the event processing code to set this property. 307 | def set_error(new_error) 308 | return if new_error == nil 309 | @error = new_error 310 | end 311 | 312 | # @private Rely on the event processing code to set this property. 313 | def set_initiator(new_initiator) 314 | return if new_initiator == nil 315 | @initiator = new_initiator 316 | end 317 | 318 | # @private Rely on the event processing code to set this property. 319 | def set_request(new_request) 320 | return if new_request == nil 321 | # TODO(pwnall): consider handling multiple requests 322 | @request = new_request 323 | end 324 | 325 | # @private Rely on the event processing code to set this property. 326 | def set_response(new_response) 327 | return if new_response == nil 328 | @response = new_response 329 | end 330 | 331 | # @private Rely on the event processing code to set this property. 332 | def set_type(new_type) 333 | return if new_type == nil 334 | @type = new_type 335 | end 336 | 337 | # @private Rely on the event processing code to set this property. 338 | def add_event(event) 339 | @last_event = event 340 | # TODO(pwnall): consider keeping track of all events 341 | end 342 | end # namespace WebkitRemote::Event 343 | 344 | # Wraps information about HTTP requests. 345 | class NetworkRequest 346 | # @return [String] the URL of the request 347 | attr_reader :url 348 | 349 | # @return [Symbol, nil] HTTP request method, e.g. :get 350 | attr_reader :method 351 | 352 | # @return [Hash] the HTTP headers of the request 353 | attr_reader :headers 354 | 355 | # @return [String] the body of a POST request 356 | attr_reader :body 357 | 358 | # @private use Event#for instead of calling this constructor directly 359 | # 360 | # @param [Hash] the raw RPC data for a Response object 361 | # in the Network domain 362 | def initialize(raw_response) 363 | @headers = raw_response['headers'] || {} 364 | @method = raw_response['method'] ? raw_response['method'].downcase.to_sym : 365 | nil 366 | @body = raw_response['postData'] 367 | @url = raw_response['url'] 368 | end 369 | end # class WebkitRemote::Client::NetworkRequest 370 | 371 | # Wraps information about responses to network requests. 372 | class NetworkResponse 373 | # @return [String] the URL of the response 374 | attr_reader :url 375 | 376 | # @return [Number] HTTP status code 377 | attr_reader :status 378 | 379 | # @return [String] HTTP status message 380 | attr_reader :status_text 381 | 382 | # @return [Hash] HTTP response headers 383 | attr_reader :headers 384 | 385 | # @return [String] the browser-determined response MIME type 386 | attr_reader :mime_type 387 | 388 | # @return [Hash] HTTP request headers 389 | attr_reader :request_headers 390 | 391 | # @return [Boolean] true if the request was served from cache 392 | attr_reader :from_cache 393 | 394 | # @return [Number] id of the network connection used by the browser to fetch 395 | # this resource 396 | attr_reader :connection_id 397 | 398 | # @return [Boolean] true if the network connection used for this request was 399 | # already open 400 | attr_reader :connection_reused 401 | 402 | # @private use Event#for instead of calling this constructor directly 403 | # 404 | # @param [Hash] the raw RPC data for a Response object 405 | # in the Network domain 406 | def initialize(raw_response) 407 | @connection_id = raw_response['connectionId'] 408 | @connection_reused = raw_response['connectionReused'] || false 409 | @from_cache = raw_response['fromDiskCache'] || false 410 | @headers = raw_response['headers'] || {} 411 | @mime_type = raw_response['mimeType'] 412 | @request_headers = raw_response['requestHeaders'] || {} 413 | @status = raw_response['status'] 414 | @status_text = raw_response['statusText'] 415 | if raw_response['timing'] 416 | @timing = WebkitRemote::Client::NetworkResourceTiming.new( 417 | raw_response['timing']) 418 | else 419 | @timing = nil 420 | end 421 | @url = raw_response['url'] 422 | end 423 | end # class WebkitRemote::Client::NetworkResponse 424 | 425 | # Wraps timing information for network events. 426 | class NetworkResourceTiming 427 | # @param [Number] baseline time for the HTTP request used to fetch a resource 428 | attr_reader :time 429 | 430 | # @param [Number] milliseconds from {#time} until the start of the server DNS 431 | # resolution 432 | attr_reader :dns_start_ms 433 | 434 | # @param [Number] milliseconds from {#time} until the server DNS resolution 435 | # completed 436 | attr_reader :dns_end_ms 437 | 438 | # @param [Number] milliseconds from {#time} until the start of the proxy DNS 439 | # resolution 440 | attr_reader :proxy_start_ms 441 | 442 | # @param [Number] milliseconds from {#time} until the proxy DNS resolution 443 | # completed 444 | attr_reader :proxy_end_ms 445 | 446 | # @param [Number] milliseconds from {#time} until the TCP connection 447 | # started being established 448 | attr_reader :connect_start_ms 449 | 450 | # @param [Number] milliseconds from {#time} until the TCP connection 451 | # was established 452 | attr_reader :connect_end_ms 453 | 454 | # @param [Number] milliseconds from {#time} until the start of the SSL 455 | # handshake 456 | attr_reader :ssl_start_ms 457 | 458 | # @param [Number] milliseconds from {#time} until the SSL handshake completed 459 | attr_reader :ssl_end_ms 460 | 461 | # @param [Number] milliseconds from {#time} until the HTTP request started 462 | # being transmitted 463 | attr_reader :send_start_ms 464 | 465 | # @param [Number] milliseconds from {#time} until the HTTP request finished 466 | # transmitting 467 | attr_reader :send_end_ms 468 | 469 | # @param [Number] milliseconds from {#time} until all the response HTTP 470 | # headers were received 471 | attr_reader :receive_headers_end_ms 472 | 473 | # @private use Event#for instead of calling this constructor directly 474 | # 475 | # @param [Hash] the raw RPC data for a ResourceTiming object 476 | # in the Network domain 477 | def initialize(raw_timing) 478 | @time = raw_timing['requestTime'].to_f 479 | 480 | @connect_start_ms = raw_timing['connectStart'].to_f 481 | @connect_end_ms = raw_timing['connectEnd'].to_f 482 | @dns_start_ms = raw_timing['dnsStart'].to_f 483 | @dns_end_ms = raw_timing['dnsEnd'].to_f 484 | @proxy_start_ms = raw_timing['proxyStart'].to_f 485 | @proxy_end_ms = raw_timing['proxyEnd'].to_f 486 | @receive_headers_end_ms = raw_timing['receiveHeadersEnd'].to_f 487 | @send_start_ms = raw_timing['sendStart'].to_f 488 | @send_end_ms = raw_timing['sendEnd'].to_f 489 | @ssl_start_ms = raw_timing['sslStart'].to_f 490 | @ssl_end_ms = raw_timing['sslEnd'].to_f 491 | end 492 | end # class WebkitRemote::Client::NetworkResourceTiming 493 | 494 | # Wraps information about the reason behind a network request. 495 | class NetworkRequestInitiator 496 | # @return [Symbol] reason behind the request; documented values are :parser, 497 | # :script and :other 498 | attr_reader :type 499 | 500 | # @return [String] URL of the document that references the requested resource 501 | attr_reader :url 502 | 503 | # @return [Number] number of the line that references the requested resource 504 | attr_reader :line 505 | 506 | # @return [WebkitRemote::Client::StackTrace] JavaScript trace, set only for 507 | # :script initiators 508 | attr_reader :stack_trace 509 | 510 | # @private Use Event#for instead of calling this constructor directly 511 | def initialize(raw_initiator) 512 | if raw_initiator['lineNumber'] 513 | @line = raw_initiator['lineNumber'].to_i 514 | else 515 | @line = nil 516 | end 517 | 518 | @stack_trace = WebkitRemote::Client::StackTrace.parse raw_initiator['stack'] 519 | @type = (raw_initiator['type'] || 'other').to_sym 520 | @url = raw_initiator['url'] 521 | end 522 | end # class WebkitRemote::Client::NetworkRequestInitiator 523 | 524 | 525 | # Wraps information about a resource served out of the browser's cache. 526 | class NetworkCacheEntry 527 | # @return [Symbol] the type of resource returned by this response; documented 528 | # values are :document, :font, :image, :other, :script, :stylesheet, 529 | # :websocket and :xhr 530 | attr_reader :type 531 | 532 | # @return [String] the URL of the response 533 | attr_reader :url 534 | 535 | # @return [WebkitRemote::Client::NetworkResponse] the cached response data 536 | attr_reader :response 537 | 538 | # @private Use Event#for instead of calling this constructor directly 539 | def initialize(raw_cached_resource) 540 | if raw_cached_resource['response'] 541 | @response = WebkitRemote::Client::NetworkResponse.new( 542 | raw_cached_resource['response']) 543 | else 544 | @response = nil 545 | end 546 | @type = (raw_cached_resource['type'] || 'other').downcase.to_sym 547 | @url = raw_cached_resource['url'] 548 | end 549 | end # namespace WebkitRemote::Client::NetworkCacheEntry 550 | 551 | end # namespace WebkitRemote::Client 552 | 553 | end # namepspace WebkitRemote 554 | -------------------------------------------------------------------------------- /lib/webkit_remote/client/page.rb: -------------------------------------------------------------------------------- 1 | module WebkitRemote 2 | 3 | class Client 4 | 5 | # API for the Page domain. 6 | module Page 7 | # Loads a new URL into the tab under debugging. 8 | # 9 | # @param [String] url the URL to be loaded into the tab 10 | # @return [WebkitRemote::Client] self 11 | def navigate_to(url) 12 | @rpc.call 'Page.navigate', url: url 13 | self 14 | end 15 | 16 | # Reloads the current page. 17 | # 18 | # @param [Hash] opts quirky behavior bits 19 | # @option opts [Boolean] skip_cache if true, the cache is not used; this is 20 | # what happens when the user presses Shift + the refresh combo 21 | # @option opts [String] onload a JavaScript that will be injected in all the 22 | # page's frames after reloading 23 | # @return [WebkitRemote::Client] self 24 | def reload(opts = {}) 25 | options = {} 26 | options[:ignoreCache] = true if opts[:skip_cache] 27 | options[:scriptToEvaluateOnLoad] = opts[:onload] if opts[:onload] 28 | @rpc.call 'Page.reload', options 29 | self 30 | end 31 | 32 | # Enables or disables the generation of events in the Page domain. 33 | # 34 | # @param [Boolean] new_page_events if true, the browser debugger will 35 | # generate Page.* events 36 | def page_events=(new_page_events) 37 | new_page_events = !!new_page_events 38 | if new_page_events != page_events 39 | @rpc.call(new_page_events ? 'Page.enable' : 'Page.disable') 40 | @page_events = new_page_events 41 | end 42 | new_page_events 43 | end 44 | 45 | # @return [Boolean] true if the debugger generates Page.* events 46 | attr_reader :page_events 47 | 48 | # @private Called by the Client constructor to set up Page data structures. 49 | def initialize_page 50 | @page_events = false 51 | end 52 | 53 | end # module WebkitRemote::Client::Page 54 | 55 | initializer :initialize_page 56 | include WebkitRemote::Client::Page 57 | 58 | end # namespace WebkitRemote::Client 59 | 60 | end # namespace WebkitRemote 61 | -------------------------------------------------------------------------------- /lib/webkit_remote/client/page_events.rb: -------------------------------------------------------------------------------- 1 | module WebkitRemote 2 | 3 | class Event 4 | 5 | # Emitted when a page's load event is triggerred. 6 | class PageLoaded < WebkitRemote::Event 7 | register 'Page.loadEventFired' 8 | 9 | # @return [Number] the event timestamp 10 | attr_reader :timestamp 11 | 12 | # @private Use Event#for instead of calling this constructor directly. 13 | def initialize(rpc_event, client) 14 | super 15 | @timestamp = raw_data['timestamp'] 16 | end 17 | 18 | # @private Use Event#can_receive instead of calling this directly. 19 | def self.can_reach?(client) 20 | client.page_events 21 | end 22 | end # class WebkitRemote::Event::PageLoaded 23 | 24 | # Emitted when a page's DOMcontent event is triggerred. 25 | class PageDomReady < WebkitRemote::Event 26 | register 'Page.domContentEventFired' 27 | 28 | # @return [Number] the event timestamp 29 | attr_reader :timestamp 30 | 31 | # @private Use Event#for instead of calling this constructor directly. 32 | def initialize(rpc_event, client) 33 | super 34 | @timestamp = raw_data['timestamp'] 35 | end 36 | 37 | # @private Use Event#can_receive instead of calling this directly. 38 | def self.can_reach?(client) 39 | client.page_events 40 | end 41 | end # class WebkitRemote::Event::PageDomReady 42 | 43 | end # namespace WebkitRemote::Event 44 | 45 | end # namepspace WebkitRemote 46 | -------------------------------------------------------------------------------- /lib/webkit_remote/client/runtime.rb: -------------------------------------------------------------------------------- 1 | module WebkitRemote 2 | 3 | class Client 4 | 5 | # API for the Runtime domain. 6 | module Runtime 7 | # Evals a JavaScript expression. 8 | # 9 | # @param [String] expression the JavaScript expression to be evaluated 10 | # @param [Hash] opts tweaks 11 | # @option opts [String, Symbol] group the name of an object group (think 12 | # memory pools); the objects in a group can be released together by one 13 | # call to WebkitRemote::Client::JsObjectGroup#release 14 | # @return [WebkitRemote::Client::JsObject, Boolean, Number, String] the 15 | # result of evaluating the expression 16 | def remote_eval(expression, opts = {}) 17 | group_name = opts[:group] || object_group_auto_name 18 | # NOTE: returnByValue is always set to false to avoid some extra complexity 19 | result = @rpc.call 'Runtime.evaluate', expression: expression, 20 | objectGroup: group_name 21 | object = WebkitRemote::Client::JsObject.for result['result'], self, 22 | group_name 23 | if result['wasThrown'] 24 | # TODO(pwnall): some wrapper for exceptions? 25 | object 26 | else 27 | object 28 | end 29 | end 30 | 31 | # Retrieves a group of remote objects by its name. 32 | # 33 | # @param [String, Symbol] group_name name given to remote_eval when the 34 | # object was created; nil obtains the anonymous group containing the 35 | # un-grouped objects created by Console#MessageReceived 36 | # @param [Boolean] create if true, fetching a group that does not exist will 37 | # create the group; this parameter should only be used internally 38 | # @return [WebkitRemote::Client::JsObject, Boolean, Number, String, nil] 39 | # a Ruby wrapper for the evaluation result; primitives get wrapped by 40 | # standard Ruby classes, and objects get wrapped by JsObject 41 | # instances 42 | def object_group(group_name, create = false) 43 | group_name = group_name.nil? ? nil : group_name.to_s 44 | group = @runtime_groups[group_name] 45 | return group if group 46 | if create 47 | @runtime_groups[group_name] = 48 | WebkitRemote::Client::JsObjectGroup.new(group_name, self) 49 | else 50 | nil 51 | end 52 | end 53 | 54 | # Generates a temporary group name for JavaScript objects. 55 | # 56 | # This is useful when the API user does not 57 | # 58 | # @return [String] an automatically-generated JS object name 59 | def object_group_auto_name 60 | '_' 61 | end 62 | 63 | # Removes a group from the list of tracked groups. 64 | # 65 | # @private Use WebkitRemote::Client::JsObjectGroup#release instead of 66 | # calling this directly. 67 | # @return [WebkitRemote::Client] self 68 | def object_group_remove(group) 69 | @runtime_groups.delete group.name 70 | self 71 | end 72 | 73 | # @private Called by the WebkitRemote::Client constructor. 74 | def initialize_runtime() 75 | @runtime_groups = {} 76 | end 77 | 78 | # Releases all the objects allocated to this runtime. 79 | # 80 | # @return [WebkitRemote::Client] self 81 | def clear_runtime 82 | @runtime_groups.each do |name, group| 83 | group.release_all 84 | end 85 | self 86 | end 87 | end # module WebkitRemote::Client::Runtime 88 | 89 | initializer :initialize_runtime 90 | clearer :clear_runtime 91 | include WebkitRemote::Client::Runtime 92 | 93 | # The class of the JavaScript undefined object. 94 | class UndefinedClass 95 | def js_undefined? 96 | true 97 | end 98 | 99 | def empty? 100 | true 101 | end 102 | 103 | def blank? 104 | true 105 | end 106 | 107 | def to_a 108 | [] 109 | end 110 | 111 | def to_s 112 | '' 113 | end 114 | 115 | def to_i 116 | 0 117 | end 118 | 119 | def to_f 120 | 0.0 121 | end 122 | 123 | def inspect 124 | 'JavaScript undefined' 125 | end 126 | 127 | def release 128 | self 129 | end 130 | 131 | def released? 132 | true 133 | end 134 | end # class WebkitRemote::Client::UndefinedClass 135 | 136 | Undefined = UndefinedClass.new 137 | 138 | # Mirrors a JsObject, defined in the Runtime domain. 139 | class JsObject 140 | # @return [String] the class name computed by WebKit for this object 141 | attr_reader :js_class_name 142 | 143 | # @return [String] the return value of the JavaScript typeof operator 144 | attr_reader :js_type 145 | 146 | # @return [Symbol] an additional type hint for this object; documented values 147 | # are :array, :date, :node, :null, :regexp 148 | attr_reader :js_subtype 149 | 150 | # @return [String] string that would be displayed in the Webkit console to 151 | # represent this object 152 | attr_reader :description 153 | 154 | # @return [Object] primitive value for this object, if available 155 | attr_reader :value 156 | 157 | # @return [Hash] the raw info provided by the remote debugger 158 | # RPC call; might be useful for accessing extended metadata that is not 159 | # (yet) recognized by WebkitRemote 160 | attr_reader :raw_data 161 | 162 | # @return [Boolean] true if the objects in this group were already released 163 | attr_reader :released 164 | alias_method :released?, :released 165 | 166 | # @return [WebkitRemote::Client] remote debugging client for the browser tab 167 | # that owns the objects in this group 168 | attr_reader :client 169 | 170 | # @return [WebkitRemote::Client::JsObjectGroup] the group that contains 171 | # this object; the object can be released by calling release_all on the 172 | # group 173 | attr_reader :group 174 | 175 | # @return [String] identifies this object in the remote debugger 176 | # @private Use the JsObject methods instead of calling this directly. 177 | attr_reader :remote_id 178 | 179 | # Releases this remote object on the browser side. 180 | # 181 | # @return [Webkit::Client::JsObject] self 182 | def release 183 | return if @released 184 | @client.rpc.call 'Runtime.releaseObject', objectId: @remote_id 185 | @group.remove self 186 | released! 187 | end 188 | 189 | # This object's properties. 190 | # 191 | # If the object's properties have not been retrieved, this method retrieves 192 | # them via a RPC call. 193 | # 194 | # @return [Hash] frozen Hash containg 195 | # the object's properties 196 | def properties 197 | @properties || properties! 198 | end 199 | 200 | # This object's properties, guaranteed to be fresh. 201 | # 202 | # This method always reloads the object's properties via a RPC call. 203 | # 204 | # @return [Hash] frozen Hash containg 205 | # the object's properties 206 | def properties! 207 | result = @client.rpc.call 'Runtime.getProperties', objectId: @remote_id 208 | @properties = Hash[ 209 | result['result'].map do |raw_property| 210 | property = WebkitRemote::Client::JsProperty.new raw_property, self 211 | [property.name, property] 212 | end 213 | ].freeze 214 | end 215 | 216 | # Calls a function with "this" bound to this object. 217 | # 218 | # @param [String] function_expression a JavaScript expression that should 219 | # evaluate to a function 220 | # @param [Array] 221 | # args the arguments passed to the function 222 | # @return [WebkitRemote::Client::JsObject, Boolean, Number, String, nil] 223 | # a Ruby wrapper for the given raw object; primitives get wrapped by 224 | # standard Ruby classes, and objects get wrapped by JsObject 225 | # instances 226 | def bound_call(function_expression, *args) 227 | call_args = args.map do |arg| 228 | if arg.kind_of? WebkitRemote::Client::JsObject 229 | { objectId: arg.remote_id } 230 | else 231 | { value: arg } 232 | end 233 | end 234 | result = @client.rpc.call 'Runtime.callFunctionOn', objectId: @remote_id, 235 | functionDeclaration: function_expression, arguments: call_args, 236 | returnByValue: false 237 | object = WebkitRemote::Client::JsObject.for result['result'], @client, 238 | @group.name 239 | if result['wasThrown'] 240 | # TODO(pwnall): some wrapper for exceptions? 241 | object 242 | else 243 | object 244 | end 245 | end 246 | 247 | # Wraps a raw object returned by the Webkit remote debugger RPC protocol. 248 | # 249 | # @private Use WebkitRemote::Client::Runtime#remote_eval instead of calling 250 | # this directly. 251 | # 252 | # @param [Hash] raw_object a JsObject instance, according 253 | # to the Webkit remote debugging protocol; this is the return value of a 254 | # 'Runtime.evaluate' RPC call 255 | # @param [WebkitRemote::Client::Runtime] client remote debugging client for 256 | # the browser tab that owns this object 257 | # @param [String] group_name name of the object group that will hold this 258 | # object; object groups work like memory pools 259 | # @return [WebkitRemote::Client::JsObject, Boolean, Number, String] a 260 | # Ruby wrapper for the given raw object; primitives get wrapped by 261 | # standard Ruby classes, and objects get wrapped by JsObject 262 | # instances 263 | def self.for(raw_object, client, group_name) 264 | if remote_id = raw_object['objectId'] 265 | group = client.object_group group_name, true 266 | return group.get(remote_id) || 267 | WebkitRemote::Client::JsObject.new(raw_object, group) 268 | else 269 | # primitive types 270 | case raw_object['type'] ? raw_object['type'].to_sym : nil 271 | when :boolean, :number, :string 272 | return raw_object['value'] 273 | when :undefined 274 | return WebkitRemote::Client::Undefined 275 | when :object 276 | case raw_object['subtype'] ? raw_object['subtype'].to_sym : nil 277 | when :null 278 | return nil 279 | end 280 | # TODO(pwnall): Any other exceptions? 281 | end 282 | end 283 | raise RuntimeError, "Unable to parse #{raw_object.inspect}" 284 | end 285 | 286 | # Wraps a remote JavaScript object 287 | # 288 | # @private JsObject#for should be used instead of this, as it handles 289 | # some edge cases 290 | def initialize(raw_object, group) 291 | @group = group 292 | @client = group.client 293 | @released = false 294 | 295 | @raw_data = raw_object 296 | @remote_id = raw_object['objectId'] 297 | @js_class_name = raw_object['className'] 298 | @description = raw_object['description'] 299 | @js_type = raw_object['type'].to_sym 300 | if raw_object['subtype'] 301 | @js_subtype = raw_object['subtype'].to_sym 302 | else 303 | @js_subtype = nil 304 | end 305 | @value = raw_object['value'] 306 | 307 | group.add self 308 | 309 | initialize_modules 310 | end 311 | 312 | def initialize_modules 313 | end 314 | private :initialize_modules 315 | 316 | # Registers a module initializer. 317 | def self.initializer(name) 318 | before_name = :"initialize_modules_before_#{name}" 319 | alias_method before_name, :initialize_modules 320 | private before_name 321 | remove_method :initialize_modules 322 | eval <] raw_property a PropertyDescriptor instance, 473 | # according to the Webkit remote debugging protocol; this is an item in 474 | # the array returned by the 'Runtime.getProperties' RPC call 475 | # @param [WebkitRemote::Client::JsObject] owner the object that this 476 | # property belongs to 477 | def initialize(raw_property, owner) 478 | # NOTE: these are only used at construction time 479 | client = owner.client 480 | group_name = owner.group.name 481 | 482 | @owner = owner 483 | @name = raw_property['name'] 484 | @configurable = !!raw_property['configurable'] 485 | @enumerable = !!raw_property['enumerable'] 486 | @writable = !!raw_property['writable'] 487 | @js_getter = raw_property['get'] && WebkitRemote::Client::JsObject.for( 488 | raw_property['get'], client, group_name) 489 | @js_setter = raw_property['set'] && WebkitRemote::Client::JsObject.for( 490 | raw_property['set'], client, group_name) 491 | @value = raw_property['value'] && WebkitRemote::Client::JsObject.for( 492 | raw_property['value'], client, group_name) 493 | end 494 | 495 | # Debugging output. 496 | def inspect 497 | result = self.to_s 498 | result[-1, 0] = 499 | " name=#{@name.inspect} configurable=#{@configurable} " + 500 | "enumerable=#{@enumerable} writable=#{@writable}" 501 | result 502 | end 503 | end # class WebkitRemote::Client::JsProperty 504 | 505 | # The call stack that represents the context of an assertion or error. 506 | class StackTrace 507 | # Parses a StackTrace object returned by a RPC request. 508 | # 509 | # @param [Array] raw_stack_trace the raw StackTrace object 510 | # in the Runtime domain returned by an RPC request 511 | def initialize(raw_stack_trace) 512 | @description = raw_stack_trace['description'] 513 | @frames = raw_stack_trace['callFrames'].map do |raw_frame| 514 | frame = {} 515 | if raw_frame['columnNumber'] 516 | frame[:column] = raw_frame['columnNumber'].to_i 517 | end 518 | if raw_frame['lineNumber'] 519 | frame[:line] = raw_frame['lineNumber'].to_i 520 | end 521 | if raw_frame['functionName'] 522 | frame[:function] = raw_frame['functionName'] 523 | end 524 | if raw_frame['url'] 525 | frame[:url] = raw_frame['url'] 526 | end 527 | frame 528 | end 529 | 530 | parent_trace = raw_stack_trace['parent'] 531 | if parent_trace 532 | @parent = StackTrace.new parent_trace 533 | else 534 | @parent = nil 535 | end 536 | end 537 | 538 | # @return [String] label of the trace; for async traces, might be the name of 539 | # a function that initiated the async call 540 | attr_reader :description 541 | 542 | # @return [Array] Ruby-friendly stack trace 543 | attr_reader :frames 544 | 545 | # @return [WebkitRemote::Client::StackTrace] stack trace for a parent async 546 | # call; may be null 547 | attr_reader :parent 548 | 549 | # Parses a StackTrace object returned by a RPC request. 550 | # 551 | # @param [Array] raw_stack_trace the raw StackTrace object 552 | # in the Runtime domain returned by an RPC request 553 | # @return [WebkitRemote::Client::StackTrace] 554 | def self.parse(raw_stack_trace) 555 | return nil unless raw_stack_trace 556 | 557 | StackTrace.new raw_stack_trace 558 | end 559 | end # class WebkitRemote::Client::StackTrace 560 | 561 | end # namespace WebkitRemote::Client 562 | 563 | end # namespace WebkitRemote 564 | -------------------------------------------------------------------------------- /lib/webkit_remote/event.rb: -------------------------------------------------------------------------------- 1 | module WebkitRemote 2 | 3 | # An event received via a RPC notification from a Webkit remote debugger. 4 | # 5 | # This is a generic super-class for events. 6 | class Event 7 | # @return [String] event's domain, e.g. "Page", "DOM". 8 | attr_reader :domain 9 | 10 | # @return [String] event's name, e.g. "Page.loadEventFired". 11 | attr_reader :name 12 | 13 | # @return [Hash] the raw event information provided by the 14 | # RPC client 15 | attr_reader :raw_data 16 | 17 | # Checks if the event meets a set of conditions. 18 | # 19 | # This is used in WebkitRemote::Client#wait_for. 20 | # 21 | # @param [Hash] conditions the conditions that must be met 22 | # by an event to get out of the waiting loop 23 | # @option conditions [Class] class the class of events to wait for; this 24 | # condition is met if the event's class is a sub-class of the given class 25 | # @option conditions [Class] type synonym for class that can be used with the 26 | # Ruby 1.9 hash syntax 27 | # @option conditions [String] name the event's name, e.g. 28 | # "Page.loadEventFired" 29 | # @return [Boolean] true if this event matches all the given conditions 30 | def matches?(conditions) 31 | conditions.all? do |key, value| 32 | case key 33 | when :class, :type 34 | kind_of? value 35 | when :name 36 | name == value 37 | else 38 | # Simple cop-out. 39 | send(key) == value 40 | end 41 | end 42 | end 43 | 44 | # Checks if a client can possibly meet an event meeting the given conditions. 45 | # 46 | # @private This is used by Client#wait_for to prevent hard-to-find bugs. 47 | # 48 | # @param [WebkitRemote::Client] client the client to be checked 49 | # @param (see WebkitRemote::Event#matches?) 50 | # @return [Boolean] false if calling WebkitRemote::Client#wait_for with the 51 | # given conditions would get the client stuck 52 | def self.can_receive?(client, conditions) 53 | conditions.all? do |key, value| 54 | case key 55 | when :class, :type 56 | value.can_reach?(client) 57 | when :name 58 | class_for(value).can_reach?(client) 59 | else 60 | true 61 | end 62 | end 63 | end 64 | 65 | # Wraps raw event data received via a RPC notification. 66 | # 67 | # @param [Hash] rpc_event event information yielded by a call 68 | # to WebkitRemote::Rpc.each_event 69 | # @param [WebkitRemote::Client] the client that received this message 70 | # @return [WebkitRemote::Event] an instance of an Event subclass that best 71 | # represents the given event 72 | def self.for(rpc_event, client) 73 | klass = class_for rpc_event[:name] 74 | klass.new rpc_event, client 75 | end 76 | 77 | # The WebkitRemote::Event subclass registered to handle an event. 78 | # 79 | # @private Use WebkitRemote::Event#for instead of calling this directly. 80 | # 81 | # @param [String] rpc_event_name the value of the 'name' property of an event 82 | # notice received via the remote debugging RPC 83 | # @return [Class] WebkitRemote::Event or one of its subclasses 84 | def self.class_for(rpc_event_name) 85 | @registry[rpc_event_name] || Event 86 | end 87 | 88 | # Wraps raw event data received via a RPC notification. 89 | # 90 | # @private API clients should use Event#for instead of calling the 91 | # constructor directly. 92 | # 93 | # If at all possible, subclasses should avoid using the WebkitRemote::Client 94 | # instance, to avoid tight coupling. 95 | # 96 | # @param [Hash] rpc_event event information yielded by a call 97 | # to WebkitRemote::Rpc.each_event 98 | # @param [WebkitRemote::Client] the client that received this message 99 | def initialize(rpc_event, client) 100 | @name = rpc_event[:name] 101 | @domain = rpc_event[:name].split('.', 2).first 102 | @raw_data = rpc_event[:data] || {} 103 | end 104 | 105 | # Registers an Event sub-class for to be instantiated when parsing an event. 106 | # 107 | # @private Only Event sub-classes should use this API. 108 | # 109 | # @param [String] name fully qualified event name, e.g. "Page.loadEventFired" 110 | # @return [Class] self 111 | def self.register(name) 112 | WebkitRemote::Event.register_class self, name 113 | self 114 | end 115 | 116 | # Registers an Event sub-class for to be instantiated when parsing an event. 117 | # 118 | # @private Event sub-classes should call #register on themselves instead of 119 | # calling this directly. 120 | # 121 | # @param [String] klass the Event subclass to be registered 122 | # @param [String] name fully qualified event name, e.g. "Page.loadEventFired" 123 | # @return [Class] self 124 | def self.register_class(klass, name) 125 | if @registry.has_key? name 126 | raise ArgumentError, "#{@registry[name].name} already registered #{name}" 127 | end 128 | @registry[name] = klass 129 | self 130 | end 131 | @registry = {} 132 | 133 | # Checks if a client is set up to receive an event of this class. 134 | # 135 | # @private Use Event# instead of calling this directly. 136 | # 137 | # This method is overridden in Event sub-classes. For example, events in the 138 | # Page domain can only be received if WebkitRemote::Client::Page#page_events 139 | # is true. 140 | def self.can_reach?(client) 141 | true 142 | end 143 | end # class WebkitRemote::Event 144 | 145 | end # namespace WebkitRemote 146 | -------------------------------------------------------------------------------- /lib/webkit_remote/process.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | require 'net/http' 3 | require 'tmpdir' 4 | 5 | module WebkitRemote 6 | 7 | # Tracks a Webkit process. 8 | class Process 9 | # Tracker for a yet-unlaunched process. 10 | # 11 | # @param [Hash] opts tweak the options below 12 | # @option opts [Integer] port the port used by the remote debugging server; 13 | # the default port is 9292 14 | # @option opts [Number] timeout number of seconds to wait for the browser 15 | # to start; the default timeout is 10 seconds 16 | # @option opts [Hash] window set the :left, :top, :width and 17 | # :height of the browser window; by default, the browser window is 18 | # 256x256 starting at 0,0. 19 | # @option opts [Boolean] allow_popups when true, the popup blocker is 20 | # disabled; this is sometimes necessary when driving a Web UI via 21 | # JavaScript 22 | # @option opts [Boolean] headless if true, Chrome runs without any dependency 23 | # on a display server 24 | # @option opts [String] chrome_binary path to the Chrome binary to be used; 25 | # by default, the path is automatically detected 26 | def initialize(opts = {}) 27 | @port = opts[:port] || 9292 28 | @timeout = opts[:timeout] || 10 29 | @running = false 30 | @data_dir = Dir.mktmpdir 'webkit-remote' 31 | @pid = nil 32 | if opts[:window] 33 | @window = opts[:window] 34 | else 35 | @window = { } 36 | end 37 | @window[:top] ||= 0 38 | @window[:left] ||= 0 39 | @window[:width] ||= 256 40 | @window[:height] ||= 256 41 | @cli = chrome_cli opts 42 | end 43 | 44 | # Starts the browser process. 45 | # 46 | # @return [WebkitRemote::Browser] master session to the started Browser 47 | # process; the session's auto_close is set to false so that it can be 48 | # safely discarded; nil if the launch fails 49 | def start 50 | return self if running? 51 | 52 | unless @pid = ::Process.spawn(*@cli) 53 | # The launch failed. 54 | stop 55 | return nil 56 | end 57 | 58 | (@timeout * 20).times do 59 | # Check if the browser exited. 60 | begin 61 | break if ::Process.wait(@pid, ::Process::WNOHANG) 62 | rescue SystemCallError # no children 63 | break 64 | end 65 | 66 | # Check if the browser finished starting up. 67 | begin 68 | browser = WebkitRemote::Browser.new process: self 69 | @running = true 70 | return browser 71 | rescue SystemCallError # most likely ECONNREFUSED 72 | Kernel.sleep 0.05 73 | end 74 | end 75 | # The browser failed, or was too slow to start. 76 | stop 77 | nil 78 | end 79 | 80 | # @return [Boolean] true if the Webkit process is running 81 | attr_reader :running 82 | alias_method :running?, :running 83 | 84 | # Stops the browser process. 85 | # 86 | # Only call this after you're done with the process. 87 | # 88 | # @return [WebkitRemote::Process] self 89 | def stop 90 | return self unless running? 91 | if @pid 92 | begin 93 | ::Process.kill 'TERM', @pid 94 | ::Process.wait @pid 95 | rescue SystemCallError 96 | # Process died on its own. 97 | ensure 98 | @pid = nil 99 | end 100 | end 101 | 102 | FileUtils.rm_rf @data_dir if File.exist?(@data_dir) 103 | @running = false 104 | self 105 | end 106 | 107 | # @return [Integer] port that the process' remote debugging server listens to 108 | attr_reader :port 109 | 110 | # Remove temporary directory if it's still there at garbage collection time. 111 | def finalize 112 | PathUtils.rm_rf @data_dir if File.exist?(@data_dir) 113 | end 114 | 115 | # Command-line that launches Google Chrome / Chromium 116 | # 117 | # @param [Hash] opts options passed to the WebkitRemote::Process constructor 118 | # @return [Array] command line for launching Chrome 119 | def chrome_cli(opts) 120 | # The Chromium wiki recommends this page for available flags: 121 | # http://peter.sh/experiments/chromium-command-line-switches/ 122 | [ 123 | opts[:chrome_binary] || self.class.chrome_binary, 124 | ] + chrome_cli_flags(opts) + [ 125 | "--remote-debugging-port=#{@port}", # Webkit remote debugging 126 | "--user-data-dir=#{@data_dir}", # really ensure a clean slate 127 | "--window-position=#{@window[:left]},#{@window[:top]}", 128 | "--window-size=#{@window[:width]},#{@window[:height]}", 129 | 130 | 'about:blank', # don't load the homepage 131 | { 132 | chdir: @data_dir, 133 | in: '/dev/null', 134 | out: File.join(@data_dir, '.stdout'), 135 | err: File.join(@data_dir, '.stderr'), 136 | close_others: true, 137 | }, 138 | ] 139 | end 140 | 141 | # Flags used on the command-line that launches Google Chrome / Chromium. 142 | # 143 | # @param [Hash] opts options passed to the WebkitRemote::Process constructor 144 | # @return [Array] flags used on the command line for launching Chrome 145 | def chrome_cli_flags(opts) 146 | # TODO - look at --data-path --homedir --profile-directory 147 | flags = [ 148 | '--bwsi', # disable extensions, sync, bookmarks 149 | '--disable-cloud-import', # no talking with the Google servers 150 | '--disable-default-apps', # no bundled apps 151 | '--disable-extensions', # no extensions 152 | '--disable-logging', # don't trash stdout / stderr 153 | '--disable-plugins', # no native content 154 | '--disable-prompt-on-repost', # no confirmation dialog on POST refresh 155 | '--disable-sync', # no talking with the Google servers 156 | '--disable-translate', # no Google Translate calls 157 | '--incognito', # don't use old state, don't preserve state 158 | '--homepage=about:blank', # don't go to Google in new tabs 159 | '--keep-alive-for-test', # don't kill process if the last window dies 160 | '--lang=en-US', # set a default language 161 | '--log-level=3', # FATAL, because there's no setting for "none" 162 | '--mute-audio', # don't let the computer make noise 163 | '--no-default-browser-check', # don't hang when Chrome isn't default 164 | '--no-experiments', # not sure this is useful 165 | '--no-first-run', # don't show the help UI 166 | '--no-service-autorun', # don't mess with autorun settings 167 | '--noerrdialogs', # don't hang on error dialogs 168 | ] 169 | flags << '--disable-popup-blocking' if opts[:allow_popups] 170 | if opts[:headless] 171 | flags << '--headless' # don't create a UI 172 | flags << '--disable-gpu' # needed for --headless to work at the moment 173 | end 174 | flags 175 | end 176 | 177 | # Path to a Google Chrome / Chromium binary. 178 | # 179 | # @return [String] full-qualified path to a binary that launches Chrome 180 | def self.chrome_binary 181 | return @chrome_binary unless @chrome_binary == false 182 | 183 | case RUBY_PLATFORM 184 | when /linux/ 185 | [ 186 | 'google-chrome', 187 | 'google-chromium', 188 | ].each do |binary| 189 | path = `which #{binary}` 190 | unless path.empty? 191 | @chrome_binary = path.strip 192 | break 193 | end 194 | end 195 | when /darwin/ 196 | [ 197 | '/Applications/Chromium.app/Contents/MacOS/Chromium', 198 | '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', 199 | '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary', 200 | ].each do |path| 201 | if File.exist? path 202 | @chrome_binary = path 203 | break 204 | end 205 | end 206 | else 207 | raise "Unsupported platform #{RUBY_PLATFORM}" 208 | end 209 | @chrome_binary ||= nil 210 | end 211 | @chrome_binary = false 212 | end # class WebkitRemote::Browser 213 | 214 | end # namespace WebkitRemote 215 | -------------------------------------------------------------------------------- /lib/webkit_remote/rpc.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'ws_sync_client' 3 | 4 | module WebkitRemote 5 | 6 | # RPC client for the Webkit remote debugging protocol. 7 | class Rpc 8 | # Connects to the remote debugging server in a Webkit tab. 9 | # 10 | # @param [Hash] opts info on the tab to connect to 11 | # @option opts [WebkitRemote::Tab] tab reference to the tab whose debugger 12 | # server this RPC client connects to 13 | def initialize(opts = {}) 14 | unless tab = opts[:tab] 15 | raise ArgumentError, 'Target tab not specified' 16 | end 17 | @closed = false 18 | @next_id = 2 19 | @events = [] 20 | 21 | @debug_url = tab.debug_url 22 | @web_socket = WsSyncClient.new @debug_url 23 | end 24 | 25 | # Remote debugging RPC call. 26 | # 27 | # See the following URL for implemented calls. 28 | # https://developers.google.com/chrome-developer-tools/docs/protocol/1.1/index 29 | # 30 | # @param [String] method name of the RPC method to be invoked 31 | # @param [Hash, nil] params parameters for the RPC method to 32 | # be invoked 33 | # @return [Hash] the return value of the RPC method 34 | def call(method, params = nil) 35 | request_id = @next_id 36 | @next_id += 1 37 | request = { 38 | jsonrpc: '2.0', 39 | id: request_id, 40 | method: method, 41 | } 42 | request[:params] = params if params 43 | request_json = JSON.dump request 44 | @web_socket.send_frame request_json 45 | 46 | loop do 47 | result = receive_message request_id 48 | return result if result 49 | end 50 | end 51 | 52 | # Continuously reports events sent by the remote debugging server. 53 | # 54 | # @yield once for each RPC event received from the remote debugger; break to 55 | # stop the event listening loop 56 | # @yieldparam [Hash] event the name and information hash of 57 | # the event, under the keys :name and :data 58 | # @return [WebkitRemote::Rpc] self 59 | def each_event 60 | loop do 61 | if @events.empty? 62 | receive_message nil 63 | else 64 | yield @events.shift 65 | end 66 | end 67 | self 68 | end 69 | 70 | # Closes the connection to the remote debugging server. 71 | # 72 | # Call this method to avoid leaking resources. 73 | # 74 | # @return [WebkitRemote::Rpc] self 75 | def close 76 | return if @closed 77 | @closed = true 78 | @web_socket.close 79 | @web_socket = nil 80 | self 81 | end 82 | 83 | # @return [Boolean] if true, the connection to the remote debugging server 84 | # has been closed, and this instance is mostly useless 85 | attr_reader :closed 86 | alias_method :closed?, :closed 87 | 88 | # @return [String] points to this client's Webkit remote debugging server 89 | attr_reader :debug_url 90 | 91 | # Blocks until a WebKit message is received, then parses it. 92 | # 93 | # RPC notifications are added to the @events array. 94 | # 95 | # @param [Integer, nil] expected_id if a RPC response is expected, this 96 | # argument has the response id; otherwise, the argument should be nil 97 | # @return [Hash, nil] a Hash containing the RPC result if an 98 | # expected RPC response was received; nil if an RPC notice was received 99 | def receive_message(expected_id) 100 | json = @web_socket.recv_frame 101 | begin 102 | data = JSON.parse json 103 | rescue JSONError 104 | close 105 | raise RuntimeError, 'Invalid JSON received' 106 | end 107 | if data['id'] 108 | # RPC result. 109 | if data['id'] != expected_id 110 | close 111 | raise RuntimeError, 'Out of sequence RPC response id' 112 | end 113 | if data['error'] 114 | code = data['error']['code'] 115 | message = data['error']['message'] 116 | raise RuntimeError, "RPC Error #{code}: #{message}" 117 | end 118 | return data['result'] 119 | elsif data['method'] 120 | # RPC notice. 121 | event = { name: data['method'], data: data['params'] } 122 | @events << event 123 | return nil 124 | else 125 | close 126 | raise RuntimeError, "Unexpected / invalid RPC message #{data.inspect}" 127 | end 128 | end 129 | private :receive_message 130 | end # class WebkitRemote::Rpc 131 | 132 | end # namespace WebkitRemote 133 | -------------------------------------------------------------------------------- /lib/webkit_remote/top_level.rb: -------------------------------------------------------------------------------- 1 | # Top-level namespace. 2 | module WebkitRemote 3 | # Launches a WebKit process locally, and sets up a debugger client for it. 4 | # 5 | # @param (see WebkitRemote::Process#initialize) 6 | # @option (see WebkitRemote::Process#initialize) 7 | # @return [WebkitRemote::Client] a debugging client connected to a local 8 | # WebKit process; the client will automatically stop the process when 9 | # closed 10 | def self.local(opts = {}) 11 | # Use headless if no desktop is available. 12 | if !opts.has_key?(:headless) && (!ENV['DISPLAY'] || ENV['DISPLAY'].empty?) 13 | opts = { headless: true }.merge! opts 14 | end 15 | process = WebkitRemote::Process.new opts 16 | 17 | browser = process.start 18 | browser.stop_process = true 19 | client = WebkitRemote::Client.new tab: browser.tabs.first, 20 | close_browser: true 21 | client 22 | end 23 | 24 | # Connects to a Webkit process, and sets up a debugger client for it. 25 | # 26 | # @param (see WebkitRemote::Browser#initialize) 27 | # @return [WebkitRemote::Client] a debugging client connected to the remote 28 | # WebKit process; the connection will be automatically terminated when 29 | # the debugging client is closed 30 | def self.remote(opts = {}) 31 | browser = WebkitRemote::Browser.new opts 32 | # NOTE: connecting to the last tab to avoid internal tabs and whatnot 33 | client = WebkitRemote::Client.new tab: browser.tabs.last, 34 | close_browser: true 35 | client 36 | end 37 | end # namespace WebkitRemote 38 | -------------------------------------------------------------------------------- /test/fixtures/config.ru: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler.setup :default, :development 3 | 4 | require 'json' 5 | require 'rack/contrib' 6 | 7 | # Compression for Network domain testing. 8 | use Rack::Deflater 9 | 10 | # Custom header for Network domain testing. 11 | use Rack::ResponseHeaders do |headers| 12 | headers['X-Unit-Test'] = 'webkit-remote' 13 | end 14 | 15 | # Cache headers for Network domain testing. 16 | use Rack::StaticCache, urls: ['/html', '/js', '/png'], root: 'test/fixtures', 17 | versioning: false, duration: 24 * 60 * 60 18 | app = lambda do |env| 19 | [ 20 | 200, 21 | {'Content-Type' => 'application/json'}, 22 | [JSON.dump(env)] 23 | ] 24 | end 25 | run app 26 | -------------------------------------------------------------------------------- /test/fixtures/html/console.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WebkitRemote Console test 5 | 25 | 26 | 27 |

Console test loaded

28 | 29 | 30 | -------------------------------------------------------------------------------- /test/fixtures/html/dom.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WebkitRemote DOM test 5 | 6 | 7 |
8 |
9 |

10 | DOM test loaded 11 |

12 |
13 |
14 |
15 |
16 |

17 | Second paragraph for testing query_selector_all. 18 |

19 |
20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /test/fixtures/html/input.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WebkitRemote Input test 5 | 14 | 15 | 16 |
17 | 18 |
19 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /test/fixtures/html/load.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | WebkitRemote load test 4 | 5 |

Loaded.

6 | 7 | 8 | -------------------------------------------------------------------------------- /test/fixtures/html/network.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WebkitRemote Network test 5 | 6 | 7 | 8 | 9 |

Network test loaded

10 | 11 | 12 | -------------------------------------------------------------------------------- /test/fixtures/html/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Popup window for WebkitRemote Process allow_popups test 5 | 8 | 9 | 10 |

Popup for Process allow_popups test loaded.

11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/fixtures/html/popup_user.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WebkitRemote Process allow_popups test 5 | 15 | 16 | 17 |

Process allow_popups test loaded

18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /test/fixtures/html/runtime.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WebkitRemote Runtime test 5 | 27 | 28 | 29 |

Runtime test loaded

30 | 31 | 32 | -------------------------------------------------------------------------------- /test/fixtures/js/network.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var xhr = new XMLHttpRequest(); 3 | xhr.responseType = "blob"; 4 | xhr.onreadystatechange = function () { 5 | if (xhr.readyState === 4) { 6 | console.log("Test done"); 7 | } 8 | }; 9 | xhr.open("GET", "../png/network.png", true); 10 | xhr.send(); 11 | })(); 12 | -------------------------------------------------------------------------------- /test/fixtures/png/network.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwnall/webkit_remote/f38ac7e882726ff00e5c56898d06d91340f8179e/test/fixtures/png/network.png -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler' 3 | begin 4 | Bundler.setup(:default, :development) 5 | rescue Bundler::BundlerError => e 6 | $stderr.puts e.message 7 | $stderr.puts "Run `bundle install` to install missing gems" 8 | exit e.status_code 9 | end 10 | require 'minitest/autorun' 11 | require 'minitest/spec' 12 | 13 | require 'simplecov' 14 | SimpleCov.start 15 | 16 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 17 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 18 | require 'webkit_remote' 19 | 20 | require 'byebug' 21 | require 'pp' 22 | require 'thread' 23 | Thread.abort_on_exception = true 24 | 25 | # Launch a dev server and wait until it starts. 26 | module RunPumaInMinitest 27 | def before_setup 28 | super 29 | @_puma_pid = Process.spawn 'bundle exec puma --port 9969 --quiet ' + 30 | '--threads 1:1 test/fixtures/config.ru', in: '/dev/null', 31 | out: '/dev/null' 32 | Process.detach @_puma_pid 33 | 34 | loop do 35 | begin 36 | response = Net::HTTP.get_response URI.parse('http://localhost:9969') 37 | break if response.kind_of?(Net::HTTPSuccess) 38 | rescue SystemCallError 39 | sleep 0.1 40 | end 41 | end 42 | end 43 | 44 | def after_teardown 45 | Process.kill 'TERM', @_puma_pid 46 | super 47 | end 48 | end 49 | class MiniTest::Test 50 | include RunPumaInMinitest 51 | end 52 | 53 | class MiniTest::Test 54 | # URL for a file in the test/fixtures directory. 55 | def fixture_url(name, type = :html) 56 | "http://localhost:9969/#{type}/#{name}.#{type}" 57 | end 58 | # Path to a file in the test/fixtures directory. 59 | def fixture_path(name, type = :html) 60 | File.join File.dirname(__FILE__), "fixtures/#{type}/#{name}.#{type}" 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /test/webkit_remote/browser_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../helper.rb', File.dirname(__FILE__)) 2 | 3 | describe WebkitRemote::Browser do 4 | before :each do 5 | @process = WebkitRemote::Process.new port: 9669, headless: true 6 | @process.start 7 | end 8 | after :each do 9 | @process.stop if @process 10 | end 11 | 12 | describe 'with process' do 13 | before :each do 14 | @browser = WebkitRemote::Browser.new process: @process 15 | end 16 | after :each do 17 | @browser.close if @browser 18 | end 19 | 20 | it 'sets the host and port correctly' do 21 | @browser.host.must_equal 'localhost' 22 | @browser.port.must_equal 9669 23 | end 24 | 25 | it 'enumerates the browser tabs correctly' do 26 | tabs = @browser.tabs 27 | tabs.length.must_equal 1 28 | tabs.first.must_be_kind_of WebkitRemote::Browser::Tab 29 | tabs.first.browser.must_equal @browser 30 | tabs.first.debug_url.must_match(/^ws:\/\/localhost:9669\//) 31 | tabs.first.url.must_equal 'about:blank' 32 | end 33 | 34 | it 'does not auto-stop the process by default' do 35 | @browser.stop_process?.must_equal false 36 | @browser.close 37 | @browser.closed?.must_equal true 38 | @process.running?.must_equal true 39 | end 40 | 41 | describe 'with process auto-stopping' do 42 | before do 43 | @browser.stop_process = true 44 | end 45 | 46 | it 'stops the process when closed' do 47 | @browser.stop_process?.must_equal true 48 | @browser.close 49 | @browser.closed?.must_equal true 50 | @process.running?.must_equal false 51 | end 52 | end 53 | end 54 | 55 | describe 'with host/port' do 56 | before :each do 57 | @browser = WebkitRemote::Browser.new host: 'localhost', port: 9669 58 | end 59 | after :each do 60 | @browser.close if @browser 61 | end 62 | 63 | it "does not support process auto-stopping" do 64 | @browser.stop_process.must_equal false 65 | lambda { 66 | @browser.stop_process = true 67 | }.must_raise ArgumentError 68 | @browser.stop_process.must_equal false 69 | end 70 | 71 | it 'enumerates the browser tabs correctly' do 72 | tabs = @browser.tabs 73 | tabs.length.must_equal 1 74 | tabs.first.must_be_kind_of WebkitRemote::Browser::Tab 75 | tabs.first.browser.must_equal @browser 76 | tabs.first.debug_url.must_match(/^ws:\/\/localhost:9669\//) 77 | tabs.first.url.must_equal 'about:blank' 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /test/webkit_remote/client/console_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../helper.rb', File.dirname(__FILE__)) 2 | 3 | describe WebkitRemote::Client::Console do 4 | before :all do 5 | @client = WebkitRemote.local port: 9669 6 | @client.page_events = true 7 | end 8 | after :all do 9 | @client.close 10 | end 11 | 12 | describe 'without console events enabled' do 13 | before :all do 14 | @client.console_events = false 15 | @client.navigate_to fixture_url(:console) 16 | @events = @client.wait_for type: WebkitRemote::Event::PageLoaded 17 | end 18 | 19 | it 'does not receive any console event' do 20 | @events.each do |event| 21 | @event.wont_be_kind_of WebkitRemote::Event::ConsoleMessage 22 | end 23 | end 24 | 25 | it 'cannot wait for console events' do 26 | lambda { 27 | @client.wait_for type: WebkitRemote::Event::ConsoleMessage 28 | }.must_raise ArgumentError 29 | lambda { 30 | @client.wait_for type: WebkitRemote::Event::ConsoleCleared 31 | }.must_raise ArgumentError 32 | end 33 | end 34 | 35 | describe 'with console events enabled' do 36 | before :all do 37 | @client.console_events = true 38 | @client.navigate_to fixture_url(:console) 39 | @events = @client.wait_for type: WebkitRemote::Event::PageLoaded 40 | @message_events = @events.select do |event| 41 | event.kind_of? WebkitRemote::Event::ConsoleMessage 42 | end 43 | @messages = @client.console_messages 44 | end 45 | 46 | after :all do 47 | @client.clear_all 48 | end 49 | 50 | it 'receives ConsoleMessage events' do 51 | @message_events.wont_be :empty? 52 | end 53 | 54 | it 'collects messages into Client#console_messages' do 55 | @message_events[0].message.must_equal @messages[0] 56 | @message_events[1].message.must_equal @messages[1] 57 | @message_events[2].message.must_equal @messages[2] 58 | @message_events[3].message.must_equal @messages[3] 59 | end 60 | 61 | it 'parses text correctly' do 62 | @messages[0].text.must_equal 'hello ruby' 63 | @messages[0].level.must_equal :warning 64 | @messages[0].reason.must_equal :console_api 65 | @messages[0].source_url.must_equal fixture_url(:console) 66 | @messages[0].source_line.must_equal 7 67 | 68 | @messages[1].text.must_equal 'stack test' 69 | @messages[1].level.must_equal :log 70 | @messages[2].text.must_match(/^params /) 71 | @messages[2].level.must_equal :error 72 | end 73 | 74 | =begin 75 | TODO(pwnall): Stacks are now available in Runtime.consoleAPICalled 76 | it 'parses the stack trace correctly' do 77 | @messages[1].text.must_equal 'stack test' 78 | @messages[1].level.must_equal :log 79 | @messages[1].stack_trace.must_equal [ 80 | { url: fixture_url(:console), line: 11, column: 19, function: 'f1' }, 81 | { url: fixture_url(:console), line: 14, column: 11, function: 'f2' }, 82 | { url: fixture_url(:console), line: 16, column: 9, function: '' }, 83 | { url: fixture_url(:console), line: 17, column: 9, function: '' }, 84 | ] 85 | end 86 | 87 | TODO(pwnall): Params are now available as args in Runtime.consoleAPICalled 88 | it 'parses parameters correctly' do 89 | @messages[2].text.must_match(/^params /) 90 | @messages[2].level.must_equal :error 91 | @messages[2].params[0, 3].must_equal ['params ', 42, true] 92 | @messages[2].params.length.must_equal 4 93 | 94 | @messages[2].params[3].must_be_kind_of WebkitRemote::Client::JsObject 95 | @messages[2].params[3].properties['hello'].value.must_equal 'ruby' 96 | @messages[2].params[3].group.name.must_be_nil 97 | end 98 | =end 99 | 100 | =begin 101 | describe 'clear_console' do 102 | before :all do 103 | @client.clear_console 104 | @events = @client.wait_for type: WebkitRemote::Event::ConsoleCleared 105 | end 106 | 107 | it 'emits a ConsoleCleared event' do 108 | @events.last.must_be_kind_of WebkitRemote::Event::ConsoleCleared 109 | end 110 | end 111 | 112 | describe 'clear_all' do 113 | before :all do 114 | @client.clear_all 115 | @events = @client.wait_for type: WebkitRemote::Event::ConsoleCleared 116 | end 117 | 118 | it 'calls clear_console, which emits a ConsoleCleared event' do 119 | @events.last.must_be_kind_of WebkitRemote::Event::ConsoleCleared 120 | end 121 | 122 | it 'releases the objects in ConsoleMessage instances' do 123 | @message_events[2].message.params[3].released?.must_equal true 124 | end 125 | end 126 | =end 127 | end 128 | 129 | =begin 130 | TODO(pwnall): These events are now available as Log.entryAdded 131 | describe 'with console and network events enabled' do 132 | before :all do 133 | @client.console_events = true 134 | @client.network_events = true 135 | @client.navigate_to fixture_url(:network) 136 | @events = @client.wait_for type: WebkitRemote::Event::ConsoleMessage, 137 | level: :log 138 | @message_events = @events.select do |event| 139 | event.kind_of? WebkitRemote::Event::ConsoleMessage 140 | end 141 | @messages = @client.console_messages 142 | end 143 | 144 | after :all do 145 | @client.clear_all 146 | end 147 | 148 | it 'receives ConsoleMessage events' do 149 | @message_events.wont_be :empty? 150 | end 151 | 152 | it 'associates messages with network requests' do 153 | @messages[0].text.must_match(/not found/i) 154 | @messages[0].network_resource.wont_equal nil 155 | @messages[0].network_resource.document_url. 156 | must_equal fixture_url(:network) 157 | @messages[0].level.must_equal :error 158 | @messages[0].count.must_equal 1 159 | @messages[0].reason.must_equal :network 160 | @messages[0].type.must_equal :log 161 | end 162 | end 163 | =end 164 | end 165 | -------------------------------------------------------------------------------- /test/webkit_remote/client/dom_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../helper.rb', File.dirname(__FILE__)) 2 | 3 | describe WebkitRemote::Client::Dom do 4 | before :all do 5 | @client = WebkitRemote.local port: 9669 6 | @client.page_events = true 7 | @client.navigate_to fixture_url(:dom) 8 | @client.wait_for type: WebkitRemote::Event::PageLoaded 9 | end 10 | after :all do 11 | @client.close 12 | end 13 | 14 | describe '#dom_root' do 15 | before :all do 16 | @root = @client.dom_root 17 | end 18 | 19 | it 'returns a WebkitRemote::Client::DomNode for a document' do 20 | @root.must_be_kind_of WebkitRemote::Client::DomNode 21 | @root.node_type.must_equal :document 22 | @root.name.must_equal '#document' 23 | @root.document_url.must_equal fixture_url(:dom) 24 | end 25 | end 26 | end 27 | 28 | describe WebkitRemote::Client::DomNode do 29 | before :all do 30 | @client = WebkitRemote.local port: 9669 31 | @client.page_events = true 32 | @client.navigate_to fixture_url(:dom) 33 | @client.wait_for type: WebkitRemote::Event::PageLoaded 34 | @root = @client.dom_root 35 | end 36 | after :all do 37 | @client.close 38 | end 39 | 40 | describe 'querySelector' do 41 | describe 'with a selector that matches' do 42 | before :all do 43 | @p = @root.query_selector 'p#load-confirmation' 44 | end 45 | 46 | it 'returns a WebkitRemote::Client::DomNode' do 47 | @p.must_be_kind_of WebkitRemote::Client::DomNode 48 | end 49 | 50 | it 'returns a WebkitRemote::Client::DomNode with correct attributes' do 51 | skip 'On-demand node processing not implemented' 52 | @p.node_type.must_equal :element 53 | @p.name.must_equal 'P' 54 | end 55 | end 56 | 57 | describe 'with a selector that does not match' do 58 | it 'returns nil' do 59 | node = @root.query_selector '#this-id-should-not-exist' 60 | node.must_be_nil 61 | end 62 | end 63 | end 64 | 65 | describe 'querySelectorAll' do 66 | before :all do 67 | @p_array = @root.query_selector_all 'p' 68 | end 69 | 70 | it 'returns an array of WebkitRemote::Client::DomNodes' do 71 | @p_array.must_respond_to :[] 72 | @p_array.each { |p| p.must_be_kind_of WebkitRemote::Client::DomNode } 73 | end 74 | 75 | it 'returns the correct WebkitRemote::Client::DomNodes' do 76 | @p_array.map { |p| p.attributes['id'] }. 77 | must_equal ['load-confirmation', 'second-paragraph'] 78 | end 79 | end 80 | 81 | describe 'attributes' do 82 | before :all do 83 | @p = @root.query_selector 'p#load-confirmation' 84 | end 85 | 86 | it 'produces a Hash of attributes' do 87 | @p.attributes.must_include 'data-purpose' 88 | @p.attributes['data-purpose'].must_equal 'attr-value-test' 89 | end 90 | end 91 | 92 | describe 'outer_html' do 93 | before :all do 94 | @p = @root.query_selector 'p#load-confirmation' 95 | end 96 | 97 | it 'returns the original HTML behind the element' do 98 | @p.outer_html.strip.must_equal < 100 | DOM test loaded 101 |

102 | DOM_END 103 | end 104 | end 105 | 106 | describe 'remove' do 107 | before :all do 108 | @p = @root.query_selector 'p#load-confirmation' 109 | @p.remove 110 | end 111 | 112 | it 'removes the node from the DOM tree' do 113 | @root.query_selector_all('p').length.must_equal 1 114 | end 115 | end 116 | 117 | describe 'remove_attribute' do 118 | describe 'without cached data' do 119 | before :all do 120 | @p = @root.query_selector 'p#load-confirmation' 121 | @p.remove_attribute 'data-purpose' 122 | end 123 | 124 | it 'strips the attribute from the element' do 125 | @p.attributes!.wont_include 'data-purpose' 126 | end 127 | end 128 | 129 | describe 'with cached data' do 130 | before :all do 131 | @p = @root.query_selector 'p#load-confirmation' 132 | @p.attributes 133 | @p.remove_attribute 'data-purpose' 134 | end 135 | 136 | it 'strips the attribute from the element' do 137 | @p.attributes.wont_include 'data-purpose' 138 | end 139 | end 140 | end 141 | 142 | describe 'js_object' do 143 | before :all do 144 | @p = @root.query_selector 'p#load-confirmation' 145 | @js_object = @p.js_object 146 | end 147 | 148 | it 'returns the corresponding WebkitRemote::Client::JsObject' do 149 | @js_object.must_be_kind_of WebkitRemote::Client::JsObject 150 | @js_object.properties['tagName'].value.must_equal 'P' 151 | @js_object.properties['baseURI'].value.must_equal fixture_url(:dom) 152 | end 153 | 154 | it 'dom_node returns the DomNode back' do 155 | @js_object.dom_node.must_equal @p 156 | end 157 | end 158 | end 159 | -------------------------------------------------------------------------------- /test/webkit_remote/client/input_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../helper.rb', File.dirname(__FILE__)) 2 | 3 | describe WebkitRemote::Client::Input do 4 | before :all do 5 | @client = WebkitRemote.local port: 9669 6 | @client.page_events = true 7 | @client.navigate_to fixture_url(:input) 8 | @client.wait_for type: WebkitRemote::Event::PageLoaded 9 | @client.console_events = true 10 | end 11 | after :all do 12 | @client.close 13 | end 14 | 15 | =begin 16 | describe '#mouse_event' do 17 | it 'generates a move correctly' do 18 | @client.mouse_event :move, 50, 50 19 | events = @client.wait_for type: WebkitRemote::Event::ConsoleMessage 20 | 21 | events.last.message.text.must_equal( 22 | 'Move. x: 50 y: 50 button: 0 detail: 0 shift: false ctrl: false ' + 23 | 'alt: false meta: false') 24 | end 25 | 26 | it 'generates a press correctly' do 27 | @client.mouse_event :down, 51, 52, button: :left, modifiers: [:shift] 28 | events = @client.wait_for type: WebkitRemote::Event::ConsoleMessage 29 | 30 | events.last.message.text.must_equal( 31 | 'Down. x: 51 y: 52 button: 0 detail: 0 shift: true ctrl: false ' + 32 | 'alt: false meta: false') 33 | end 34 | 35 | it 'generates a second press correctly' do 36 | @client.mouse_event :down, 51, 52, button: :right, clicks: 2, 37 | modifiers: [:alt, :ctrl] 38 | events = @client.wait_for type: WebkitRemote::Event::ConsoleMessage 39 | 40 | events.last.message.text.must_equal( 41 | 'Down. x: 51 y: 52 button: 2 detail: 2 shift: false ctrl: true ' + 42 | 'alt: true meta: false') 43 | end 44 | 45 | it 'generates a release correctly' do 46 | @client.mouse_event :up, 51, 52, button: :middle, modifiers: [:command] 47 | events = @client.wait_for type: WebkitRemote::Event::ConsoleMessage 48 | 49 | events.last.message.text.must_equal( 50 | 'Up. x: 51 y: 52 button: 1 detail: 0 shift: false ctrl: false ' + 51 | 'alt: false meta: true') 52 | end 53 | end 54 | =end 55 | 56 | describe '#key_event' do 57 | it 'generates a char correctly' do 58 | @client.key_event :char, text: 'a' 59 | 60 | events = @client.wait_for type: WebkitRemote::Event::ConsoleMessage 61 | 62 | events.last.message.text.must_equal( 63 | 'KPress. keyCode: 97 charCode: 97 key: text: undefined ' + 64 | 'repeat: false shift: false ctrl: false alt: false meta: false') 65 | end 66 | 67 | it 'generates a down correctly' do 68 | @client.key_event :down, vkey: 0x41, key: 'A' 69 | 70 | events = @client.wait_for type: WebkitRemote::Event::ConsoleMessage 71 | 72 | events.last.message.text.must_equal( 73 | 'KDown. keyCode: 65 charCode: 0 key: A text: undefined ' + 74 | 'repeat: false shift: false ctrl: false alt: false meta: false') 75 | end 76 | 77 | it 'generates an up correctly' do 78 | @client.key_event :up, vkey: 0x41, key: 'A' 79 | 80 | events = @client.wait_for type: WebkitRemote::Event::ConsoleMessage 81 | 82 | events.last.message.text.must_equal( 83 | 'KUp. keyCode: 65 charCode: 0 key: A text: undefined ' + 84 | 'repeat: false shift: false ctrl: false alt: false meta: false') 85 | end 86 | 87 | it 'generates a raw_down correctly' do 88 | @client.key_event :raw_down, vkey: 0x41, key: 'A' 89 | 90 | events = @client.wait_for type: WebkitRemote::Event::ConsoleMessage 91 | 92 | events.last.message.text.must_equal( 93 | 'KDown. keyCode: 65 charCode: 0 key: A text: undefined ' + 94 | 'repeat: false shift: false ctrl: false alt: false meta: false') 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /test/webkit_remote/client/js_object_group_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../helper.rb', File.dirname(__FILE__)) 2 | 3 | describe WebkitRemote::Client::JsObjectGroup do 4 | before :each do 5 | @client = WebkitRemote.local port: 9669 6 | @client.page_events = true 7 | @client.navigate_to fixture_url(:runtime) 8 | @client.wait_for type: WebkitRemote::Event::PageLoaded 9 | 10 | @object1 = @client.remote_eval '({})', group: 'g1' 11 | @object2 = @client.remote_eval '({})', group: 'g1' 12 | @object3 = @client.remote_eval '({})', group: 'g2' 13 | @group1 = @client.object_group 'g1' 14 | @group2 = @client.object_group 'g2' 15 | end 16 | after :each do 17 | @group1.release_all if @group1 18 | @group2.release_all if @group2 19 | @client.close 20 | end 21 | 22 | describe 'include?' do 23 | it 'is true for objects in the group' do 24 | @group1.include?(@object1).must_equal true 25 | @group1.include?(@object2).must_equal true 26 | @group2.include?(@object3).must_equal true 27 | end 28 | it 'is false for objects in different groups' do 29 | @group2.include?(@object2).must_equal false 30 | @group1.include?(@object3).must_equal false 31 | @group2.include?(@object1).must_equal false 32 | end 33 | end 34 | 35 | describe 'after an object release' do 36 | before :each do 37 | @object1.release 38 | end 39 | 40 | it 'does not include the released object' do 41 | @group1.include?(@object1).must_equal false 42 | end 43 | it 'includes unreleased objects' do 44 | @group1.include?(@object2).must_equal true 45 | end 46 | it 'does not release the whole group' do 47 | @group1.released?.must_equal false 48 | end 49 | 50 | describe 'after releasing the only other object in the group' do 51 | before :each do 52 | @object2.release 53 | end 54 | 55 | it 'released the whole group' do 56 | @group1.released?.must_equal true 57 | end 58 | it 'removes the group from the client' do 59 | @client.object_group('g1').must_be_nil 60 | end 61 | end 62 | end 63 | 64 | describe '#release_all' do 65 | before :each do 66 | @group1.release_all 67 | end 68 | 69 | it 'releases all the objects in the group' do 70 | @object1.released?.must_equal true 71 | @object2.released?.must_equal true 72 | end 73 | it 'does not release objects in other groups' do 74 | @object3.released?.must_equal false 75 | end 76 | it 'releases the group and removes the group from the client' do 77 | @group1.released?.must_equal true 78 | @client.object_group('g1').must_be_nil 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /test/webkit_remote/client/js_object_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../helper.rb', File.dirname(__FILE__)) 2 | 3 | describe WebkitRemote::Client::JsObject do 4 | before :each do 5 | @client = WebkitRemote.local port: 9669 6 | @client.page_events = true 7 | @client.navigate_to fixture_url(:runtime) 8 | @client.wait_for type: WebkitRemote::Event::PageLoaded 9 | end 10 | after :each do 11 | @client.close 12 | end 13 | 14 | describe 'properties' do 15 | describe 'with simple JSON' do 16 | before :each do 17 | @object = @client.remote_eval 'window.t = ({answer: 42, test: true})' 18 | end 19 | 20 | it 'enumerates the properties correctly' do 21 | @object.properties['answer'].name.must_equal 'answer' 22 | @object.properties['test'].name.must_equal 'test' 23 | @object.properties['other'].must_be_nil 24 | end 25 | 26 | it 'gets the correct values' do 27 | @object.properties['answer'].value.must_equal 42 28 | @object.properties['test'].value.must_equal true 29 | end 30 | 31 | it 'sets owner correctly' do 32 | @object.properties['answer'].owner.must_equal @object 33 | end 34 | 35 | it 'does not have extra properties' do 36 | @object.properties.select { |name, property| property.enumerable? }. 37 | keys.sort.must_equal ['answer', 'test'] 38 | end 39 | 40 | describe 'after property update' do 41 | before do 42 | @object.properties['DONE'] 43 | @client.remote_eval 'window.t.test = "updated"' 44 | end 45 | it 'does not automatically refresh' do 46 | @object.properties['test'].value.must_equal true 47 | end 48 | it 'refreshes when properties! is called' do 49 | @object.properties!['test'].value.must_equal 'updated' 50 | end 51 | end 52 | 53 | describe 'inspect' do 54 | it 'contains the property name and its enumerable status' do 55 | @object.properties['test'].inspect.must_match( 56 | //) 57 | end 58 | end 59 | end 60 | 61 | describe 'with an object with custom properties' do 62 | before :each do 63 | @object = @client.remote_eval <=, 0 173 | 174 | @chunks.map(&:bytes_received).max.must_be :>, 0 175 | end 176 | 177 | it 'receives NetworkLoad events' do 178 | @loads.wont_be :empty? 179 | end 180 | 181 | it 'parses NetworkLoad events' do 182 | @loads[0].resource.must_equal @requests[0].resource 183 | @loads[1].resource.must_equal @requests[2].resource 184 | end 185 | 186 | it 'receives NetworkFailure events' do 187 | @failure_events.wont_be :empty? 188 | end 189 | 190 | it 'parses NetworkFailure events' do 191 | @failure_events[0].resource.must_equal @requests[1].resource 192 | @failure_events[0].error.wont_equal nil 193 | @failure_events[0].canceled.must_equal true 194 | end 195 | 196 | it 'collects request and response data in NetworkResources' do 197 | @resources[1].must_equal @requests[1].resource 198 | @resources[1].request.must_equal @requests[1].request 199 | @resources[1].response.must_equal @responses[1].response 200 | @resources[1].type.must_equal :script 201 | @resources[1].document_url.must_equal fixture_url(:network) 202 | @resources[1].initiator.must_equal @requests[1].initiator 203 | @resources[1].canceled.must_equal true 204 | @resources[1].error.must_equal @failure_events[0].error 205 | @resources[1].last_event.must_equal @failure_events[0] 206 | @resources[1].client.must_equal @client 207 | 208 | @resources[2].must_equal @requests[2].resource 209 | @resources[2].request.must_equal @requests[2].request 210 | @resources[2].response.must_equal @responses[2].response 211 | @resources[2].type.must_equal :script 212 | @resources[2].document_url.must_equal fixture_url(:network) 213 | @resources[2].initiator.must_equal @requests[2].initiator 214 | @resources[2].canceled.must_equal false 215 | @resources[2].error.must_be_nil 216 | @resources[2].last_event.must_equal @loads[1] 217 | @resources[2].client.must_equal @client 218 | 219 | @resources[3].must_equal @requests[3].resource 220 | @resources[3].request.must_equal @requests[3].request 221 | @resources[3].response.must_equal @responses[3].response 222 | @resources[3].type.must_equal :xhr 223 | @resources[3].document_url.must_equal fixture_url(:network) 224 | @resources[3].initiator.must_equal @requests[3].initiator 225 | @resources[3].canceled.must_equal false 226 | @resources[3].error.must_be_nil 227 | @resources[3].last_event.must_equal @loads[2] 228 | @resources[3].client.must_equal @client 229 | 230 | @resources[-1].last_event.must_equal @loads[-1] 231 | end 232 | 233 | it 'retrieves the body for a text NetworkResource' do 234 | @resources[0].body.must_equal File.read(fixture_path(:network)) 235 | end 236 | 237 | it 'retrieves the body for a binary NetworkResource' do 238 | @resources[3].body.must_equal File.binread(fixture_path(:network, :png)) 239 | end 240 | end 241 | 242 | describe 'and a cached request' do 243 | before :each do 244 | @client.disable_cache = false 245 | @client.navigate_to fixture_url(:network) 246 | @client.wait_for type: WebkitRemote::Event::ConsoleMessage, level: :log, 247 | text: 'Test done' 248 | @client.clear_all 249 | 250 | @client.network_events = true 251 | @client.navigate_to fixture_url(:network) 252 | @events = @client.wait_for type: WebkitRemote::Event::ConsoleMessage, 253 | level: :log, text: 'Test done' 254 | @requests = @events.select do |event| 255 | event.kind_of? WebkitRemote::Event::NetworkRequest 256 | end 257 | @responses = @events.select do |event| 258 | event.kind_of? WebkitRemote::Event::NetworkResponse 259 | end 260 | @loads = @events.select do |event| 261 | event.kind_of? WebkitRemote::Event::NetworkLoad 262 | end 263 | @chunks = @events.select do |event| 264 | event.kind_of? WebkitRemote::Event::NetworkData 265 | end 266 | @hits = @events.select do |event| 267 | event.kind_of? WebkitRemote::Event::NetworkCacheHit 268 | end 269 | 270 | @resources = @client.network_resources 271 | end 272 | 273 | it 'receives NetworkCacheHit events' do 274 | @hits.wont_be :empty? 275 | end 276 | 277 | it 'parses NetworkCacheHits events' do 278 | @hits[0].resource.must_equal @requests[2].resource 279 | end 280 | end 281 | end 282 | -------------------------------------------------------------------------------- /test/webkit_remote/client/page_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../helper.rb', File.dirname(__FILE__)) 2 | 3 | describe WebkitRemote::Client::Page do 4 | before do 5 | @client = WebkitRemote.local port: 9669 6 | end 7 | after do 8 | @client.close 9 | end 10 | 11 | describe 'navigate' do 12 | before do 13 | @url = fixture_url(:load) 14 | @client.page_events = true 15 | @client.navigate_to @url 16 | @events = [] 17 | @client.each_event do |event| 18 | @events << event 19 | break if event.kind_of?(WebkitRemote::Event::PageLoaded) 20 | end 21 | end 22 | 23 | it 'changes the tab URL' do 24 | @client.browser.tabs.map(&:url).must_include @url 25 | end 26 | 27 | it 'fires a PageLoaded event' do 28 | @events.map(&:class).must_include WebkitRemote::Event::PageLoaded 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/webkit_remote/client/runtime_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../helper.rb', File.dirname(__FILE__)) 2 | 3 | describe WebkitRemote::Client::Runtime do 4 | before :all do 5 | @client = WebkitRemote.local port: 9669 6 | @client.page_events = true 7 | @client.navigate_to fixture_url(:runtime) 8 | @client.wait_for type: WebkitRemote::Event::PageLoaded 9 | end 10 | after :all do 11 | @client.clear_all 12 | @client.close 13 | end 14 | 15 | describe 'remote_eval' do 16 | describe 'for a number' do 17 | before :each do 18 | @number = @client.remote_eval '11 + 31', group: 'no' 19 | end 20 | it 'returns a Ruby number' do 21 | @number.must_equal 42 22 | end 23 | it 'does not create an object group' do 24 | @client.object_group('no').must_be_nil 25 | end 26 | end 27 | 28 | describe 'for a boolean' do 29 | before :each do 30 | @true = @client.remote_eval '!!1', group: 'no' 31 | @false = @client.remote_eval '!!0', group: 'no' 32 | end 33 | it 'returns a Ruby boolean' do 34 | @true.must_equal true 35 | @false.must_equal false 36 | end 37 | it 'does not create an object group' do 38 | @client.object_group('no').must_be_nil 39 | end 40 | end 41 | 42 | describe 'for a string' do 43 | before :each do 44 | @string = @client.remote_eval '"hello Ruby"', group: 'no' 45 | end 46 | it 'returns a Ruby string' do 47 | @string.must_equal 'hello Ruby' 48 | end 49 | it 'does not create an object group' do 50 | @client.object_group('no').must_be_nil 51 | end 52 | end 53 | 54 | describe 'for null' do 55 | before :each do 56 | @null = @client.remote_eval 'null', group: 'no' 57 | end 58 | it 'returns nil' do 59 | @string.must_be_nil 60 | end 61 | it 'does not create an object group' do 62 | @client.object_group('no').must_be_nil 63 | end 64 | end 65 | 66 | describe 'for undefined' do 67 | before :each do 68 | @undefined = @client.remote_eval '(function() {})()', group: 'no' 69 | end 70 | it 'returns an Undefined object' do 71 | @undefined.js_undefined?.must_equal true 72 | @undefined.to_s.must_equal '' 73 | @undefined.inspect.must_equal 'JavaScript undefined' 74 | @undefined.to_a.must_equal [] 75 | @undefined.to_i.must_equal 0 76 | @undefined.to_f.must_equal 0.0 77 | @undefined.blank?.must_equal true 78 | @undefined.empty?.must_equal true 79 | end 80 | it 'does not create an object group' do 81 | @client.object_group('no').must_be_nil 82 | end 83 | it 'is idempotent' do 84 | @undefined.must_equal @client.remote_eval('(function(){})()') 85 | end 86 | 87 | it 'returns a JSObject-like object' do 88 | @undefined.released?.must_equal true 89 | @undefined.release.must_equal @undefined 90 | end 91 | end 92 | 93 | describe 'for an object created via new' do 94 | before :each do 95 | @object = @client.remote_eval 'new TestClass("hello Ruby")', 96 | group: 'yes' 97 | end 98 | after :each do 99 | group = @client.object_group('yes') 100 | group.release_all if group 101 | end 102 | it 'returns an JsObject instance' do 103 | @object.must_be_kind_of WebkitRemote::Client::JsObject 104 | end 105 | it 'sets the object properties correctly' do 106 | @object.js_class_name.must_equal 'TestClass' 107 | @object.description.must_equal 'TestClass' 108 | end 109 | it 'creates a non-released group' do 110 | @client.object_group('yes').wont_equal nil 111 | @client.object_group('yes').released?.must_equal false 112 | end 113 | end 114 | 115 | describe 'for a JSON object' do 116 | before :each do 117 | @object = @client.remote_eval '({hello: "ruby", answer: 42})', 118 | group: 'yes' 119 | end 120 | after :each do 121 | group = @client.object_group('yes') 122 | group.release_all if group 123 | end 124 | it 'returns an JsObject instance' do 125 | @object.must_be_kind_of WebkitRemote::Client::JsObject 126 | end 127 | it 'sets the object properties correctly' do 128 | @object.js_class_name.must_equal 'Object' 129 | @object.description.must_equal 'Object' 130 | end 131 | it 'creates a non-released group' do 132 | @client.object_group('yes').wont_equal nil 133 | @client.object_group('yes').released?.must_equal false 134 | end 135 | end 136 | 137 | describe 'for a function' do 138 | before :each do 139 | @function = @client.remote_eval '(function (a, b) { return a + b; })', 140 | group: 'yes' 141 | end 142 | after :each do 143 | group = @client.object_group('yes') 144 | group.release_all if group 145 | end 146 | 147 | it 'returns a JsObject instance' do 148 | @function.must_be_kind_of WebkitRemote::Client::JsObject 149 | end 150 | 151 | it 'sets the object properties correctly' do 152 | @function.js_class_name.must_equal 'Function' 153 | @function.js_type.must_equal :function 154 | @function.description.must_equal 'function (a, b) { return a + b; }' 155 | end 156 | 157 | it 'creates a non-released group' do 158 | @client.object_group('yes').wont_equal nil 159 | @client.object_group('yes').released?.must_equal false 160 | end 161 | end 162 | end 163 | 164 | 165 | describe 'clear_all' do 166 | describe 'with a named allocated object' do 167 | before :each do 168 | @object = @client.remote_eval '({hello: "ruby", answer: 42})', 169 | group: 'yes' 170 | end 171 | after :each do 172 | group = @client.object_group('yes') 173 | group.release_all if group 174 | end 175 | 176 | it 'releases the object and its group' do 177 | @client.clear_all 178 | @object.released?.must_equal true 179 | @client.object_group('yes').must_be_nil 180 | end 181 | end 182 | end 183 | end 184 | -------------------------------------------------------------------------------- /test/webkit_remote/client_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../helper.rb', File.dirname(__FILE__)) 2 | 3 | describe WebkitRemote::Client do 4 | before :each do 5 | @process = WebkitRemote::Process.new port: 9669, headless: true 6 | @process.start 7 | @browser = WebkitRemote::Browser.new process: @process, stop_process: true 8 | tab = @browser.tabs.first 9 | @client = WebkitRemote::Client.new tab: tab 10 | end 11 | after :each do 12 | @client.close if @client 13 | @browser.close if @browser 14 | @process.stop if @process 15 | end 16 | 17 | it 'sets close_browser to false, browser to the given Browser instance' do 18 | @client.close_browser?.must_equal false 19 | @client.browser.must_equal @browser 20 | end 21 | 22 | describe '#close with close_browser is true' do 23 | before do 24 | @client.close_browser = true 25 | @client.close 26 | end 27 | 28 | it 'closes the client debugging connection' do 29 | @client.closed?.must_equal true 30 | end 31 | 32 | it 'closes the browser master debugging session' do 33 | @browser.closed?.must_equal true 34 | end 35 | 36 | it 'still retuns a good inspect string' do 37 | @client.inspect.must_match(/<.*WebkitRemote::Client.*>/) 38 | end 39 | end 40 | 41 | describe '#each_event' do 42 | before do 43 | @client.rpc.call 'Page.enable' 44 | @client.rpc.call 'Page.navigate', url: fixture_url(:load) 45 | @events = [] 46 | @client.each_event do |event| 47 | @events << event 48 | break if event.kind_of?(WebkitRemote::Event::PageLoaded) 49 | end 50 | end 51 | 52 | it 'only yields events' do 53 | @events.each do |event| 54 | event.must_be_kind_of WebkitRemote::Event 55 | end 56 | end 57 | 58 | it 'contains a PageLoaded instance' do 59 | @events.map(&:class).must_include WebkitRemote::Event::PageLoaded 60 | end 61 | end 62 | 63 | describe '#wait_for' do 64 | describe 'with page_events enabled' do 65 | before do 66 | @client.page_events = true 67 | @client.rpc.call 'Page.navigate', url: fixture_url(:load) 68 | @events = @client.wait_for type: WebkitRemote::Event::PageLoaded 69 | end 70 | 71 | it 'returns an array ending with a PageLoaded instance' do 72 | @events.wont_be :empty? 73 | @events.last.must_be_kind_of WebkitRemote::Event::PageLoaded 74 | end 75 | end 76 | 77 | describe 'with page_events disabled' do 78 | it 'raises ArgumentError' do 79 | lambda { 80 | @client.wait_for(type: WebkitRemote::Event::PageLoaded) 81 | }.must_raise ArgumentError 82 | end 83 | end 84 | end 85 | 86 | describe '#rpc' do 87 | it 'is is a non-closed WebkitRemote::Rpc instance' do 88 | @client.rpc.must_be_kind_of WebkitRemote::Rpc 89 | @client.rpc.closed?.must_equal false 90 | end 91 | 92 | describe 'after calling close' do 93 | before do 94 | @client_rpc = @client.rpc 95 | @client.close 96 | end 97 | 98 | it 'the Rpc instance is closed' do 99 | @client_rpc.closed?.must_equal true 100 | end 101 | end 102 | end 103 | 104 | describe '#clear_all' do 105 | it 'does not crash' do 106 | @client.clear_all 107 | end 108 | end 109 | 110 | describe '#inspect' do 111 | it 'includes the debugging URL and closed flag' do 112 | @client.inspect.must_match( 113 | //) 114 | end 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /test/webkit_remote/event_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../helper.rb', File.dirname(__FILE__)) 2 | 3 | describe WebkitRemote::Event do 4 | before do 5 | @client = WebkitRemote.local port: 9669 6 | end 7 | after do 8 | @client.close 9 | end 10 | 11 | describe 'on a PageLoaded event' do 12 | before do 13 | @url = fixture_url(:load) 14 | @client.page_events = true 15 | @client.navigate_to @url 16 | events = [] 17 | # NOTE: wait_for uses Event#matches, and we're testing that here 18 | @client.each_event do |event| 19 | events << event 20 | break if event.kind_of?(WebkitRemote::Event::PageLoaded) 21 | end 22 | @event = events.last 23 | end 24 | 25 | describe 'matches' do 26 | it 'handles single conditions' do 27 | @event.matches?(class: WebkitRemote::Event::PageLoaded). 28 | must_equal true 29 | @event.matches?(type: WebkitRemote::Event).must_equal true 30 | @event.matches?(class: WebkitRemote::Event::PageDomReady). 31 | must_equal false 32 | @event.matches?(name: 'Page.loadEventFired').must_equal true 33 | @event.matches?(name: 'loadEventFired').must_equal false 34 | @event.matches?(domain: 'Page').must_equal true 35 | @event.matches?(domain: 'Runtime').must_equal false 36 | end 37 | 38 | it 'handles multiple conditions' do 39 | @event.matches?(type: WebkitRemote::Event::PageLoaded, 40 | domain: 'Page').must_equal true 41 | @event.matches?(type: WebkitRemote::Event::PageLoaded, 42 | domain: 'Runtime').must_equal false 43 | @event.matches?(type: WebkitRemote::Event::PageDomReady, 44 | domain: 'Page').must_equal false 45 | end 46 | end 47 | end 48 | 49 | describe 'can_receive?' do 50 | describe 'when page_events is false' do 51 | before do 52 | @client.page_events = false 53 | end 54 | it 'should be true for the base class' do 55 | WebkitRemote::Event.can_receive?(@client, type: WebkitRemote::Event). 56 | must_equal true 57 | end 58 | it 'should be false for PageLoaded' do 59 | WebkitRemote::Event.can_receive?(@client, 60 | type: WebkitRemote::Event::PageLoaded).must_equal false 61 | end 62 | it 'should be false for Page.loadEventFired' do 63 | WebkitRemote::Event.can_receive?(@client, name: 'Page.loadEventFired'). 64 | must_equal false 65 | end 66 | it 'should ignore extra properties' do 67 | WebkitRemote::Event.can_receive?(@client, name: 'Page.loadEventFired', 68 | other_property: true).must_equal false 69 | end 70 | end 71 | 72 | describe 'when page_events is true' do 73 | before do 74 | @client.page_events = true 75 | end 76 | it 'should be true for PageLoaded' do 77 | WebkitRemote::Event.can_receive?(@client, 78 | type: WebkitRemote::Event::PageLoaded).must_equal true 79 | end 80 | it 'should be true for Page.loadEventFired' do 81 | WebkitRemote::Event.can_receive?(@client, name: 'Page.loadEventFired'). 82 | must_equal true 83 | end 84 | it 'should ignore extra properties' do 85 | WebkitRemote::Event.can_receive?(@client, name: 'Page.loadEventFired', 86 | other_property: true).must_equal true 87 | end 88 | end 89 | end 90 | end 91 | 92 | -------------------------------------------------------------------------------- /test/webkit_remote/process_flags_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../helper.rb', File.dirname(__FILE__)) 2 | 3 | # This tests the command-line flags code by making sure that Chrome works in 4 | # the intended fashion. By the nature of the test, it looks more like an 5 | # integration test than the unit-level Process tests in process_test.rb. 6 | 7 | describe WebkitRemote::Process do 8 | after :all do 9 | @client.close if @client 10 | end 11 | 12 | describe 'with allow_popups: true' do 13 | before :all do 14 | @client = WebkitRemote.local port: 9669, allow_popups: true 15 | @client.console_events = true 16 | end 17 | 18 | it 'runs through a page that uses window.open without a gesture' do 19 | @client.navigate_to fixture_url(:popup_user) 20 | events = @client.wait_for type: WebkitRemote::Event::ConsoleMessage 21 | events.last.message.text.must_equal 'Received popup message.' 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/webkit_remote/process_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../helper.rb', File.dirname(__FILE__)) 2 | 3 | describe WebkitRemote::Process do 4 | describe 'in headless mode' do 5 | before :each do 6 | @process = WebkitRemote::Process.new port: 9669, headless: true 7 | end 8 | after :each do 9 | @process.stop if @process 10 | end 11 | 12 | describe '#running' do 13 | it 'returns false before #start is called' do 14 | @process.running?.must_equal false 15 | end 16 | end 17 | 18 | describe '#start' do 19 | before :each do 20 | @browser = @process.start 21 | end 22 | after :each do 23 | @browser.close if @browser 24 | @process.stop if @process 25 | end 26 | 27 | it 'makes running? return true' do 28 | @process.running?.must_equal true 29 | end 30 | 31 | it 'returns a Browser instance that does not auto-stop the process' do 32 | @browser.must_be_kind_of WebkitRemote::Browser 33 | @browser.closed?.must_equal false 34 | @browser.stop_process?.must_equal false 35 | end 36 | 37 | describe '#stop' do 38 | before :each do 39 | @process.stop 40 | end 41 | 42 | it 'makes running? return false' do 43 | @process.running?.must_equal false 44 | end 45 | 46 | it 'kills the http server that responds to /json' do 47 | begin 48 | @browser.tabs 49 | fail 'browser process not killed' 50 | rescue EOFError 51 | pass 52 | rescue Errno::ECONNRESET 53 | pass 54 | rescue Errno::ECONNREFUSED 55 | pass 56 | end 57 | end 58 | end 59 | 60 | describe '#inspect' do 61 | it 'includes the CLI, PID and running state' do 62 | @process.inspect.must_match( 63 | //) 64 | @process.inspect.must_match( 65 | //) 66 | @process.inspect.must_match( 67 | //) 68 | end 69 | end 70 | end 71 | end 72 | 73 | describe 'on real X desktop' do 74 | before :each do 75 | unless ENV['DISPLAY'] and /\:\d+/ =~ ENV['DISPLAY'] 76 | skip 'No real X desktop configured' 77 | end 78 | @process = WebkitRemote::Process.new port: 9669 79 | end 80 | after :each do 81 | @process.stop if @process 82 | end 83 | 84 | describe '#start' do 85 | before :each do 86 | @browser = @process.start 87 | end 88 | after :each do 89 | @browser.close if @browser 90 | @process.stop if @process 91 | end 92 | 93 | it 'returns a Browser instance that does not auto-stop the process' do 94 | @browser.must_be_kind_of WebkitRemote::Browser 95 | @browser.closed?.must_equal false 96 | @browser.stop_process?.must_equal false 97 | end 98 | 99 | describe '#stop' do 100 | before :each do 101 | @process.stop 102 | end 103 | 104 | it 'kills the http server that responds to /json' do 105 | begin 106 | @browser.tabs 107 | fail 'browser process not killed' 108 | rescue EOFError 109 | pass 110 | rescue Errno::ECONNRESET 111 | pass 112 | rescue Errno::ECONNREFUSED 113 | pass 114 | end 115 | end 116 | end 117 | end 118 | end 119 | 120 | describe 'with invalid chrome_binary path' do 121 | before :each do 122 | @process = WebkitRemote::Process.new port: 9669, 123 | chrome_binary: '/bin/non_existing_binary' 124 | end 125 | after :each do 126 | @process.stop if @process 127 | end 128 | 129 | it '#start raises an exception' do 130 | begin 131 | @process.start 132 | fail 'no exception raised' 133 | rescue SystemCallError => e 134 | e.message.must_match(/non_existing_binary/) 135 | end 136 | end 137 | end 138 | end 139 | -------------------------------------------------------------------------------- /test/webkit_remote/rpc_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../helper.rb', File.dirname(__FILE__)) 2 | 3 | describe WebkitRemote::Rpc do 4 | before :each do 5 | @process = WebkitRemote::Process.new port: 9669, headless: true 6 | @process.start 7 | @browser = WebkitRemote::Browser.new process: @process, stop_process: true 8 | tab = @browser.tabs.first 9 | @rpc = WebkitRemote::Rpc.new tab: tab 10 | end 11 | after :each do 12 | @rpc.close if @rpc 13 | @browser.close if @browser 14 | @process.stop if @process 15 | end 16 | 17 | describe 'call' do 18 | before do 19 | @result = @rpc.call 'Runtime.evaluate', expression: '1 + 2', 20 | returnByValue: true 21 | end 22 | 23 | it 'produces the correct result' do 24 | @result.must_include 'result' 25 | @result['result'].must_include 'value' 26 | @result['result']['value'].must_equal 3 27 | @result['result'].must_include 'type' 28 | @result['result']['type'].must_equal 'number' 29 | end 30 | end 31 | 32 | describe 'each_event' do 33 | before do 34 | @rpc.call 'Page.enable' 35 | @rpc.call 'Page.navigate', url: fixture_url(:load) 36 | @events = [] 37 | @rpc.each_event do |event| 38 | @events << event 39 | break if event[:name] == 'Page.loadEventFired' 40 | end 41 | end 42 | 43 | it 'only yields events' do 44 | @events.each do |event| 45 | event.must_include :name 46 | event.must_include :data 47 | end 48 | end 49 | 50 | it 'contains a Page.loadEventFired event' do 51 | @events.map { |e| e[:name] }.must_include 'Page.loadEventFired' 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /test/webkit_remote_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('helper.rb', File.dirname(__FILE__)) 2 | 3 | describe WebkitRemote do 4 | describe 'local' do 5 | before do 6 | @client = WebkitRemote.local port: 9669, headless: true 7 | end 8 | after do 9 | @client.close 10 | end 11 | 12 | it 'returns a working client' do 13 | @client.must_be_kind_of WebkitRemote::Client 14 | @client.closed?.must_equal false 15 | end 16 | 17 | describe 'after #close' do 18 | before do 19 | @client.close 20 | end 21 | 22 | it 'the client tears down everything' do 23 | @client.closed?.must_equal true 24 | @client.browser.closed?.must_equal true 25 | @client.browser.process.running?.must_equal false 26 | end 27 | end 28 | end 29 | 30 | describe 'remote' do 31 | before do 32 | @process = WebkitRemote::Process.new port: 9669, headless: true 33 | browser = @process.start 34 | browser.close 35 | @client = WebkitRemote.remote host: 'localhost', port: 9669 36 | end 37 | 38 | after do 39 | @client.close 40 | @process.stop 41 | end 42 | 43 | it 'returns a working client' do 44 | @client.must_be_kind_of WebkitRemote::Client 45 | @client.closed?.must_equal false 46 | end 47 | 48 | describe 'after #close' do 49 | before do 50 | @client.close 51 | end 52 | 53 | it 'the client tears the connection' do 54 | @client.closed?.must_equal true 55 | @client.browser.closed?.must_equal true 56 | end 57 | 58 | it 'the client does not impact the browser process' do 59 | @process.running?.must_equal true 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /webkit_remote.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' 4 | # -*- encoding: utf-8 -*- 5 | # stub: webkit_remote 0.6.0 ruby lib 6 | 7 | Gem::Specification.new do |s| 8 | s.name = "webkit_remote".freeze 9 | s.version = "0.6.0" 10 | 11 | s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= 12 | s.require_paths = ["lib".freeze] 13 | s.authors = ["Victor Costan".freeze] 14 | s.date = "2017-06-25" 15 | s.description = "Launches Google Chrome instances and controls them via the Remote Debugging server".freeze 16 | s.email = "victor@costan.us".freeze 17 | s.extra_rdoc_files = [ 18 | "LICENSE.txt", 19 | "README.md" 20 | ] 21 | s.files = [ 22 | ".document", 23 | ".travis.yml", 24 | "Gemfile", 25 | "Gemfile.lock", 26 | "LICENSE.txt", 27 | "README.md", 28 | "Rakefile", 29 | "VERSION", 30 | "lib/webkit_remote.rb", 31 | "lib/webkit_remote/browser.rb", 32 | "lib/webkit_remote/client.rb", 33 | "lib/webkit_remote/client/console.rb", 34 | "lib/webkit_remote/client/console_events.rb", 35 | "lib/webkit_remote/client/dom.rb", 36 | "lib/webkit_remote/client/dom_events.rb", 37 | "lib/webkit_remote/client/dom_runtime.rb", 38 | "lib/webkit_remote/client/input.rb", 39 | "lib/webkit_remote/client/network.rb", 40 | "lib/webkit_remote/client/network_events.rb", 41 | "lib/webkit_remote/client/page.rb", 42 | "lib/webkit_remote/client/page_events.rb", 43 | "lib/webkit_remote/client/runtime.rb", 44 | "lib/webkit_remote/event.rb", 45 | "lib/webkit_remote/process.rb", 46 | "lib/webkit_remote/rpc.rb", 47 | "lib/webkit_remote/top_level.rb", 48 | "test/fixtures/config.ru", 49 | "test/fixtures/html/console.html", 50 | "test/fixtures/html/dom.html", 51 | "test/fixtures/html/input.html", 52 | "test/fixtures/html/load.html", 53 | "test/fixtures/html/network.html", 54 | "test/fixtures/html/popup.html", 55 | "test/fixtures/html/popup_user.html", 56 | "test/fixtures/html/runtime.html", 57 | "test/fixtures/js/network.js", 58 | "test/fixtures/png/network.png", 59 | "test/helper.rb", 60 | "test/webkit_remote/browser_test.rb", 61 | "test/webkit_remote/client/console_test.rb", 62 | "test/webkit_remote/client/dom_test.rb", 63 | "test/webkit_remote/client/input_test.rb", 64 | "test/webkit_remote/client/js_object_group_test.rb", 65 | "test/webkit_remote/client/js_object_test.rb", 66 | "test/webkit_remote/client/network_test.rb", 67 | "test/webkit_remote/client/page_test.rb", 68 | "test/webkit_remote/client/runtime_test.rb", 69 | "test/webkit_remote/client_test.rb", 70 | "test/webkit_remote/event_test.rb", 71 | "test/webkit_remote/process_flags_test.rb", 72 | "test/webkit_remote/process_test.rb", 73 | "test/webkit_remote/rpc_test.rb", 74 | "test/webkit_remote_test.rb", 75 | "webkit_remote.gemspec" 76 | ] 77 | s.homepage = "http://github.com/pwnall/webkit_remote".freeze 78 | s.licenses = ["MIT".freeze] 79 | s.rubygems_version = "2.6.10".freeze 80 | s.summary = "Client for the Webkit Remote Debugging server".freeze 81 | 82 | if s.respond_to? :specification_version then 83 | s.specification_version = 4 84 | 85 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 86 | s.add_runtime_dependency(%q.freeze, [">= 0.1.2"]) 87 | s.add_development_dependency(%q.freeze, [">= 1.5.3"]) 88 | s.add_development_dependency(%q.freeze, [">= 9.0.6"]) 89 | s.add_development_dependency(%q.freeze, [">= 2.0.1"]) 90 | s.add_development_dependency(%q.freeze, [">= 5.3.0"]) 91 | s.add_development_dependency(%q.freeze, [">= 2.8.0"]) 92 | s.add_development_dependency(%q.freeze, [">= 1.6.8"]) 93 | s.add_development_dependency(%q.freeze, [">= 1.2.0"]) 94 | s.add_development_dependency(%q.freeze, [">= 4.1.1"]) 95 | s.add_development_dependency(%q.freeze, [">= 0.9.1"]) 96 | s.add_development_dependency(%q.freeze, [">= 0.8.7.3"]) 97 | else 98 | s.add_dependency(%q.freeze, [">= 0.1.2"]) 99 | s.add_dependency(%q.freeze, [">= 1.5.3"]) 100 | s.add_dependency(%q.freeze, [">= 9.0.6"]) 101 | s.add_dependency(%q.freeze, [">= 2.0.1"]) 102 | s.add_dependency(%q.freeze, [">= 5.3.0"]) 103 | s.add_dependency(%q.freeze, [">= 2.8.0"]) 104 | s.add_dependency(%q.freeze, [">= 1.6.8"]) 105 | s.add_dependency(%q.freeze, [">= 1.2.0"]) 106 | s.add_dependency(%q.freeze, [">= 4.1.1"]) 107 | s.add_dependency(%q.freeze, [">= 0.9.1"]) 108 | s.add_dependency(%q.freeze, [">= 0.8.7.3"]) 109 | end 110 | else 111 | s.add_dependency(%q.freeze, [">= 0.1.2"]) 112 | s.add_dependency(%q.freeze, [">= 1.5.3"]) 113 | s.add_dependency(%q.freeze, [">= 9.0.6"]) 114 | s.add_dependency(%q.freeze, [">= 2.0.1"]) 115 | s.add_dependency(%q.freeze, [">= 5.3.0"]) 116 | s.add_dependency(%q.freeze, [">= 2.8.0"]) 117 | s.add_dependency(%q.freeze, [">= 1.6.8"]) 118 | s.add_dependency(%q.freeze, [">= 1.2.0"]) 119 | s.add_dependency(%q.freeze, [">= 4.1.1"]) 120 | s.add_dependency(%q.freeze, [">= 0.9.1"]) 121 | s.add_dependency(%q.freeze, [">= 0.8.7.3"]) 122 | end 123 | end 124 | 125 | --------------------------------------------------------------------------------