├── .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 |
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 |
--------------------------------------------------------------------------------