webkit_remote 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -14,22 +14,46 @@ class Process
14
14
  # the default port is 9292
15
15
  # @option opts [Number] timeout number of seconds to wait for the browser
16
16
  # to start; the default timeout is 10 seconds
17
+ # @option opts [Hash<Symbol, Number>] window set the :left, :top, :width and
18
+ # :height of the browser window; by default, the browser window is
19
+ # 256x256 starting at 0,0.
20
+ # @option opts [Hash<Symbol, Number>, Boolean] xvfb use Xvfb instead of a
21
+ # real screen; set the :display, :width, :height, :depth and :dpi of the
22
+ # server, or use the default display number 20, at 1280x1024x32 with
23
+ # 72dpi
17
24
  def initialize(opts = {})
18
25
  @port = opts[:port] || 9292
19
26
  @timeout = opts[:timeout] || 10
20
27
  @running = false
21
28
  @data_dir = Dir.mktmpdir 'webkit-remote'
22
29
  @pid = nil
30
+ @xvfb_pid = nil
23
31
  @cli = chrome_cli opts
32
+ if opts[:xvfb]
33
+ @xvfb_cli = xvfb_cli opts
34
+ else
35
+ @xvfb_cli = nil
36
+ end
24
37
  end
25
38
 
26
39
  # Starts the browser process.
27
40
  #
28
- # @return [WebkitRemote::Browser] self
41
+ # @return [WebkitRemote::Browser] master session to the started Browser
42
+ # process; the session's auto_close is set to false so that it can be
43
+ # safely discarded; nil if the launch fails
29
44
  def start
30
45
  return self if running?
46
+
47
+ if @xvfb_cli
48
+ unless @xvfb_pid = POSIX::Spawn.spawn(*@xvfb_cli)
49
+ # The Xvfb launch failed.
50
+ return nil
51
+ end
52
+ end
53
+
31
54
  unless @pid = POSIX::Spawn.spawn(*@cli)
32
55
  # The launch failed
56
+ stop
33
57
  return nil
34
58
  end
35
59
 
@@ -51,6 +75,7 @@ class Process
51
75
  end
52
76
  end
53
77
  # The browser failed, or was too slow to start.
78
+ stop
54
79
  nil
55
80
  end
56
81
 
@@ -65,10 +90,28 @@ class Process
65
90
  # @return [WebkitRemote::Process] self
66
91
  def stop
67
92
  return self unless running?
68
- begin
69
- ::Process.kill 'TERM', @pid
70
- ::Process.wait @pid
93
+ if @pid
94
+ begin
95
+ ::Process.kill 'TERM', @pid
96
+ ::Process.wait @pid
97
+ rescue SystemCallError
98
+ # Process died on its own.
99
+ ensure
100
+ @pid = nil
101
+ end
71
102
  end
103
+
104
+ if @xvfb_pid
105
+ begin
106
+ ::Process.kill 'TERM', @xvfb_pid
107
+ ::Process.wait @xvfb_pid
108
+ rescue SystemCallError
109
+ # Process died on its own.
110
+ ensure
111
+ @xvfb_pid = nil
112
+ end
113
+ end
114
+
72
115
  FileUtils.rm_rf @data_dir if File.exists?(@data_dir)
73
116
  @running = false
74
117
  self
@@ -90,6 +133,7 @@ class Process
90
133
  # The Chromium wiki recommends this page for available flags:
91
134
  # http://peter.sh/experiments/chromium-command-line-switches/
92
135
  [
136
+ chrome_env(opts),
93
137
  self.class.chrome_binary,
94
138
  '--disable-default-apps', # no bundled apps
95
139
  '--disable-desktop-shortcuts', # don't mess with the desktop
@@ -115,13 +159,73 @@ class Process
115
159
  "--remote-debugging-port=#{@port}", # Webkit remote debugging
116
160
  "--user-data-dir=#{@data_dir}", # really ensure a clean slate
117
161
  '--window-position=0,0', # remove randomness source
118
- '--window-size=128,128', # remove randomness source
162
+ '--window-size=256,256', # remove randomness source
119
163
  'about:blank', # don't load the homepage
120
164
  {
121
165
  chdir: @data_dir,
122
166
  in: '/dev/null',
123
167
  out: File.join(@data_dir, '.stdout'),
124
- err: File.join(@data_dir, '.stderr'),
168
+ err: File.join(@data_dir, '.stderr')
169
+ },
170
+ ]
171
+ end
172
+
173
+ # Environment variables set when launching Chrome.
174
+ #
175
+ # @param [Hash] opts options passed to the WebkitRemote::Process constructor
176
+ # @return [Hash<String, String>] variables to be added to the environment of
177
+ # the Chrome process before launching
178
+ def chrome_env(opts)
179
+ if opts[:xvfb]
180
+ if opts[:xvfb].respond_to?(:[]) and opts[:xvfb][:display]
181
+ display = opts[:xvfb][:display]
182
+ else
183
+ display = 20
184
+ end
185
+ { 'DISPLAY' => ":#{display}.0" }
186
+ else
187
+ {}
188
+ end
189
+ end
190
+
191
+ # Command-line that launches Xvfb.
192
+ #
193
+ # @param [Hash] opts options passed to the WebkitRemote::Process constructor
194
+ # @return [Array<String>] command line for launching Xvfb
195
+ def xvfb_cli(opts)
196
+ # The OSX man page for Xvfb:
197
+ # http://developer.apple.com/library/mac/documentation/darwin/reference/manpages/man1/Xvfb.1.html
198
+
199
+ xvfb_opts = opts[:xvfb]
200
+ unless xvfb_opts.respond_to? :[]
201
+ xvfb_opts = {}
202
+ end
203
+
204
+ display = xvfb_opts[:display] || 20
205
+ width = xvfb_opts[:width] || 1280
206
+ height = xvfb_opts[:height] || 1024
207
+ depth = xvfb_opts[:depth] || 32
208
+ dpi = xvfb_opts[:dpi] || 72
209
+
210
+ # https://bugs.freedesktop.org/show_bug.cgi?id=17453
211
+ if depth == 32
212
+ depth = '24+32'
213
+ end
214
+
215
+ [
216
+ self.class.xvfb_binary,
217
+ ":#{display}",
218
+ '-screen', '0', "#{width}x#{height}x#{depth}",
219
+ '-auth', File.join(@data_dir, '.Xauthority'),
220
+ '-c',
221
+ '-dpi', dpi.to_s,
222
+ '-terminate',
223
+ '-wr',
224
+ {
225
+ chdir: @data_dir,
226
+ in: '/dev/null',
227
+ out: File.join(@data_dir, '.X_stdout'),
228
+ err: File.join(@data_dir, '.X_stderr'),
125
229
  },
126
230
  ]
127
231
  end
@@ -161,6 +265,20 @@ class Process
161
265
  @chrome_binary ||= nil
162
266
  end
163
267
  @chrome_binary = false
268
+
269
+ # Path to the Xvfb virtual X server binary.
270
+ #
271
+ # @return [String] full-qualified path to a binary that launches Xvfb.
272
+ def self.xvfb_binary
273
+ return @xvfb_binary unless @xvfb_binary == false
274
+
275
+ path = `which Xvfb`
276
+ unless path.empty?
277
+ @xvfb_binary = path.strip
278
+ end
279
+ @xvfb_binary ||= nil
280
+ end
281
+ @xvfb_binary = false
164
282
  end # class WebkitRemote::Browser
165
283
 
166
284
  end # namespace WebkitRemote
@@ -7,7 +7,12 @@ module WebkitRemote
7
7
  # WebKit process; the client will automatically stop the process when
8
8
  # closed
9
9
  def self.local(opts = {})
10
+ # Use Xvfb if no desktop is available.
11
+ if !opts.has_key?(:xvfb) && (!ENV['DISPLAY'] || ENV['DISPLAY'].empty?)
12
+ opts = { xvfb: true }.merge! opts
13
+ end
10
14
  process = WebkitRemote::Process.new opts
15
+
11
16
  browser = process.start
12
17
  browser.stop_process = true
13
18
  client = WebkitRemote::Client.new tab: browser.tabs.first,
@@ -17,6 +17,10 @@
17
17
  })();
18
18
  // Arguments test.
19
19
  console.error("params ", 42, true, { hello: "ruby" });
20
+ // Repetition test.
21
+ for (var i = 0; i < 3; i++) {
22
+ console.warn("one more time");
23
+ }
20
24
  </script>
21
25
  </head>
22
26
  <body>
@@ -0,0 +1,23 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <title>WebkitRemote DOM test</title>
5
+ </head>
6
+ <body>
7
+ <article id="load-article">
8
+ <section id="load-confirmation-section">
9
+ <p id="load-confirmation" data-purpose="attr-value-test">
10
+ DOM test loaded
11
+ </p>
12
+ </section>
13
+ </article>
14
+ <article id="second-article">
15
+ <section id="second-section">
16
+ <p id="second-paragraph" data-purpose="query-selector-all-test">
17
+ Second paragraph for testing query_selector_all.
18
+ </p>
19
+ </section>
20
+ </article>
21
+ </body>
22
+ </html>
23
+
data/test/helper.rb CHANGED
@@ -17,6 +17,7 @@ $LOAD_PATH.unshift(File.dirname(__FILE__))
17
17
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
18
18
  require 'webkit_remote'
19
19
 
20
+ require 'debugger'
20
21
  require 'pp'
21
22
  require 'thread'
22
23
  Thread.abort_on_exception = true
@@ -2,7 +2,7 @@ require File.expand_path('../helper.rb', File.dirname(__FILE__))
2
2
 
3
3
  describe WebkitRemote::Browser do
4
4
  before :each do
5
- @process = WebkitRemote::Process.new port: 9669
5
+ @process = WebkitRemote::Process.new port: 9669, xvfb: true
6
6
  @process.start
7
7
  end
8
8
  after :each do
@@ -40,17 +40,34 @@ describe WebkitRemote::Client::Console do
40
40
  @client.console_events = true
41
41
  @client.navigate_to fixture_url(:console)
42
42
  @events = @client.wait_for type: WebkitRemote::Event::PageLoaded
43
- @messages = @events.select do |event|
43
+ @message_events = @events.select do |event|
44
44
  event.kind_of? WebkitRemote::Event::ConsoleMessage
45
45
  end
46
+ @repeat_events = @events.select do |event|
47
+ event.kind_of? WebkitRemote::Event::ConsoleMessageRepeated
48
+ end
49
+ @messages = @client.console_messages
46
50
  end
47
51
 
48
52
  after :all do
49
53
  @client.clear_all
50
54
  end
51
55
 
52
- it 'receives console events' do
53
- @messages.wont_be :empty?
56
+ it 'receives ConsoleMessage events' do
57
+ @message_events.wont_be :empty?
58
+ end
59
+
60
+ it 'receives ConsoleMessageRepeated events' do
61
+ @repeat_events.wont_be :empty?
62
+ end
63
+
64
+ it 'collects messages into Client#console_messages' do
65
+ @message_events[0].message.must_equal @messages[0]
66
+ @message_events[1].message.must_equal @messages[1]
67
+ @message_events[2].message.must_equal @messages[2]
68
+ @message_events[3].message.must_equal @messages[3]
69
+ @repeat_events[0].message.must_equal @messages[3]
70
+ @repeat_events[1].message.must_equal @messages[3]
54
71
  end
55
72
 
56
73
  it 'parses text correctly' do
@@ -81,11 +98,18 @@ describe WebkitRemote::Client::Console do
81
98
  @messages[2].params[0, 3].must_equal ['params ', 42, true]
82
99
  @messages[2].params.length.must_equal 4
83
100
 
84
- @messages[2].params[3].must_be_kind_of WebkitRemote::Client::RemoteObject
85
- @messages[2].params[3].properties[:hello].value.must_equal 'ruby'
101
+ @messages[2].params[3].must_be_kind_of WebkitRemote::Client::JsObject
102
+ @messages[2].params[3].properties['hello'].value.must_equal 'ruby'
86
103
  @messages[2].params[3].group.name.must_equal nil
87
104
  end
88
105
 
106
+ it 'parses repeated messages correctly' do
107
+ @messages[3].text.must_equal 'one more time'
108
+ @messages[3].count.must_equal 3
109
+ @repeat_events[0].count.must_equal 2
110
+ @repeat_events[1].count.must_equal 3
111
+ end
112
+
89
113
  describe 'clear_console' do
90
114
  before :all do
91
115
  @client.clear_console
@@ -108,8 +132,41 @@ describe WebkitRemote::Client::Console do
108
132
  end
109
133
 
110
134
  it 'releases the objects in ConsoleMessage instances' do
111
- @messages[2].params[3].released?.must_equal true
135
+ @message_events[2].message.params[3].released?.must_equal true
136
+ end
137
+ end
138
+ end
139
+
140
+ describe 'with console and network events enabled' do
141
+ before :all do
142
+ @client.console_events = true
143
+ @client.network_events = true
144
+ @client.navigate_to fixture_url(:network)
145
+ @events = @client.wait_for type: WebkitRemote::Event::ConsoleMessage,
146
+ level: :log
147
+ @message_events = @events.select do |event|
148
+ event.kind_of? WebkitRemote::Event::ConsoleMessage
112
149
  end
150
+ @messages = @client.console_messages
151
+ end
152
+
153
+ after :all do
154
+ @client.clear_all
155
+ end
156
+
157
+ it 'receives ConsoleMessage events' do
158
+ @message_events.wont_be :empty?
159
+ end
160
+
161
+ it 'associates messages with network requests' do
162
+ @messages[0].text.must_match /not found/i
163
+ @messages[0].network_resource.wont_equal nil
164
+ @messages[0].network_resource.document_url.
165
+ must_equal fixture_url(:network)
166
+ @messages[0].level.must_equal :error
167
+ @messages[0].count.must_equal 1
168
+ @messages[0].reason.must_equal :network
169
+ @messages[0].type.must_equal :log
113
170
  end
114
171
  end
115
172
  end
@@ -0,0 +1,149 @@
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
+ before :all do
42
+ @p = @root.query_selector 'p#load-confirmation'
43
+ end
44
+
45
+ it 'returns a WebkitRemote::Client::DomNode' do
46
+ @p.must_be_kind_of WebkitRemote::Client::DomNode
47
+ end
48
+
49
+ it 'returns a WebkitRemote::Client::DomNode with correct attributes' do
50
+ skip 'On-demand node processing not implemented'
51
+ @p.node_type.must_equal :element
52
+ @p.name.must_equal 'P'
53
+ end
54
+ end
55
+
56
+ describe 'querySelectorAll' do
57
+ before :all do
58
+ @p_array = @root.query_selector_all 'p'
59
+ end
60
+
61
+ it 'returns an array of WebkitRemote::Client::DomNodes' do
62
+ @p_array.must_respond_to :[]
63
+ @p_array.each { |p| p.must_be_kind_of WebkitRemote::Client::DomNode }
64
+ end
65
+
66
+ it 'returns the correct WebkitRemote::Client::DomNodes' do
67
+ @p_array.map { |p| p.attributes['id'] }.
68
+ must_equal ['load-confirmation', 'second-paragraph']
69
+ end
70
+ end
71
+
72
+ describe 'attributes' do
73
+ before :all do
74
+ @p = @root.query_selector 'p#load-confirmation'
75
+ end
76
+
77
+ it 'produces a Hash of attributes' do
78
+ @p.attributes.must_include 'data-purpose'
79
+ @p.attributes['data-purpose'].must_equal 'attr-value-test'
80
+ end
81
+ end
82
+
83
+ describe 'outer_html' do
84
+ before :all do
85
+ @p = @root.query_selector 'p#load-confirmation'
86
+ end
87
+
88
+ it 'returns the original HTML behind the element' do
89
+ @p.outer_html.strip.must_equal <<DOM_END.strip
90
+ <p id="load-confirmation" data-purpose="attr-value-test">
91
+ DOM test loaded
92
+ </p>
93
+ DOM_END
94
+ end
95
+ end
96
+
97
+ describe 'remove' do
98
+ before :all do
99
+ @p = @root.query_selector 'p#load-confirmation'
100
+ @p.remove
101
+ end
102
+
103
+ it 'removes the node from the DOM tree' do
104
+ @root.query_selector_all('p').length.must_equal 1
105
+ end
106
+ end
107
+
108
+ describe 'remove_attribute' do
109
+ describe 'without cached data' do
110
+ before :all do
111
+ @p = @root.query_selector 'p#load-confirmation'
112
+ @p.remove_attribute 'data-purpose'
113
+ end
114
+
115
+ it 'strips the attribute from the element' do
116
+ @p.attributes!.wont_include 'data-purpose'
117
+ end
118
+ end
119
+
120
+ describe 'with cached data' do
121
+ before :all do
122
+ @p = @root.query_selector 'p#load-confirmation'
123
+ @p.attributes
124
+ @p.remove_attribute 'data-purpose'
125
+ end
126
+
127
+ it 'strips the attribute from the element' do
128
+ @p.attributes.wont_include 'data-purpose'
129
+ end
130
+ end
131
+ end
132
+
133
+ describe 'js_object' do
134
+ before :all do
135
+ @p = @root.query_selector 'p#load-confirmation'
136
+ @js_object = @p.js_object
137
+ end
138
+
139
+ it 'returns the corresponding WebkitRemote::Client::JsObject' do
140
+ @js_object.must_be_kind_of WebkitRemote::Client::JsObject
141
+ @js_object.properties['tagName'].value.must_equal 'P'
142
+ @js_object.properties['baseURI'].value.must_equal fixture_url(:dom)
143
+ end
144
+
145
+ it 'dom_node returns the DomNode back' do
146
+ @js_object.dom_node.must_equal @p
147
+ end
148
+ end
149
+ end