terminus 0.5.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +72 -0
- data/lib/capybara/driver/terminus.rb +14 -4
- data/lib/terminus.rb +2 -1
- data/lib/terminus/browser.rb +30 -7
- data/lib/terminus/client.rb +1 -1
- data/lib/terminus/client/phantomjs.rb +1 -1
- data/lib/terminus/connector.rb +120 -3
- data/lib/terminus/controller.rb +1 -1
- data/lib/terminus/node.rb +17 -3
- data/lib/terminus/proxy.rb +6 -4
- data/lib/terminus/proxy/external.rb +14 -9
- data/lib/terminus/public/compiled/terminus-min.js +3 -3
- data/lib/terminus/public/compiled/terminus.js +62 -17
- data/lib/terminus/public/loader.js +2 -1
- data/lib/terminus/public/terminus.js +62 -17
- data/lib/terminus/views/bootstrap.erb +3 -3
- data/lib/terminus/views/index.erb +1 -1
- metadata +40 -85
- data/README.rdoc +0 -56
- data/lib/terminus/connector/server.rb +0 -142
- data/lib/terminus/connector/socket_handler.rb +0 -72
- data/spec/1.1/reports/android.txt +0 -874
- data/spec/1.1/reports/chrome.txt +0 -880
- data/spec/1.1/reports/firefox.txt +0 -879
- data/spec/1.1/reports/opera.txt +0 -880
- data/spec/1.1/reports/phantomjs.txt +0 -874
- data/spec/1.1/reports/safari.txt +0 -880
- data/spec/1.1/spec_helper.rb +0 -31
- data/spec/1.1/terminus_driver_spec.rb +0 -24
- data/spec/1.1/terminus_session_spec.rb +0 -19
- data/spec/2.0/reports/android.txt +0 -815
- data/spec/2.0/reports/chrome.txt +0 -806
- data/spec/2.0/reports/firefox.txt +0 -812
- data/spec/2.0/reports/opera.txt +0 -806
- data/spec/2.0/reports/phantomjs.txt +0 -803
- data/spec/2.0/reports/safari.txt +0 -806
- data/spec/2.0/spec_helper.rb +0 -21
- data/spec/2.0/terminus_spec.rb +0 -25
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 30218e6e5c5e4b1e6511ce36a401a402889aaa21
|
4
|
+
data.tar.gz: 6dc3ecb9878543aaeb30294cefaee7cc87b16985
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b9a983a66f5310a7cec00f4979c0425908a4cc62e1e29a11e0476d18457efaf82de970f088516a9180c2a93585fdd8337edd1bc709a4f9654aca4025d32e6b05
|
7
|
+
data.tar.gz: 80e252b012aad3bcc0f3a01a50108e293c298a34aeddba1612c6233f7887e475f1c28f0d7d4a99aa0553169daa3573a5a302566d47c509877d90e8fc9299aa83
|
data/README.md
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
# Terminus
|
2
|
+
|
3
|
+
[Terminus[(http://terminus.jcoglan.com) is an experimental
|
4
|
+
[Capybara](https://github.com/jnicklas/capybara) driver for real browsers. It
|
5
|
+
lets you control your application in any browser on any device (including
|
6
|
+
[PhantomJS](http://phantomjs.org/)), without needing browser plugins. This
|
7
|
+
allows several types of testing to be automated:
|
8
|
+
|
9
|
+
* Cross-browser testing
|
10
|
+
* Headless testing
|
11
|
+
* Multi-browser interaction e.g. messaging apps
|
12
|
+
* Testing on remote machines, phones, iPads etc
|
13
|
+
|
14
|
+
*Since it is experimental, this project is sporadically maintained. Usage is
|
15
|
+
entirely at your own risk.*
|
16
|
+
|
17
|
+
|
18
|
+
## Installation
|
19
|
+
|
20
|
+
```
|
21
|
+
$ gem install terminus
|
22
|
+
```
|
23
|
+
|
24
|
+
|
25
|
+
## Running the example
|
26
|
+
|
27
|
+
Install the dependencies and boot the Terminus server, then open
|
28
|
+
http://localhost:70004/ in your browser.
|
29
|
+
|
30
|
+
```
|
31
|
+
$ bundle install
|
32
|
+
$ bundle exec bin/terminus
|
33
|
+
```
|
34
|
+
|
35
|
+
With your browser open, start an IRB session and begin controlling the app:
|
36
|
+
|
37
|
+
```
|
38
|
+
$ irb -r ./example/app
|
39
|
+
>> extend Capybara::DSL
|
40
|
+
>> visit '/'
|
41
|
+
>> click_link 'Sign up!'
|
42
|
+
>> fill_in 'Username', :with => 'jcoglan'
|
43
|
+
>> fill_in 'Password', :with => 'hello'
|
44
|
+
>> choose 'Web scale'
|
45
|
+
>> click_button 'Go!'
|
46
|
+
```
|
47
|
+
|
48
|
+
|
49
|
+
## License
|
50
|
+
|
51
|
+
(The MIT License)
|
52
|
+
|
53
|
+
Copyright (c) 2010-2013 James Coglan
|
54
|
+
|
55
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
56
|
+
this software and associated documentation files (the 'Software'), to deal in
|
57
|
+
the Software without restriction, including without limitation the rights to
|
58
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
59
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
60
|
+
so, subject to the following conditions:
|
61
|
+
|
62
|
+
The above copyright notice and this permission notice shall be included in all
|
63
|
+
copies or substantial portions of the Software.
|
64
|
+
|
65
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
66
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
67
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
68
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
69
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
70
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
71
|
+
SOFTWARE.
|
72
|
+
|
@@ -15,10 +15,15 @@ class Capybara::Driver::Terminus < Capybara::Driver::Base
|
|
15
15
|
Terminus.register_local_port(@rack_server.port)
|
16
16
|
end
|
17
17
|
|
18
|
-
def
|
19
|
-
browser.
|
18
|
+
def find_css(css)
|
19
|
+
browser.find_css(css, self)
|
20
20
|
end
|
21
21
|
|
22
|
+
def find_xpath(xpath)
|
23
|
+
browser.find_xpath(xpath, self)
|
24
|
+
end
|
25
|
+
alias :find :find_xpath
|
26
|
+
|
22
27
|
def invalid_element_errors
|
23
28
|
[::Terminus::ObsoleteElementError]
|
24
29
|
end
|
@@ -43,14 +48,18 @@ class Capybara::Driver::Terminus < Capybara::Driver::Base
|
|
43
48
|
:debugger,
|
44
49
|
:evaluate_script,
|
45
50
|
:execute_script,
|
51
|
+
:go_back,
|
52
|
+
:go_forward,
|
46
53
|
:html,
|
47
54
|
:reset!,
|
48
55
|
:response_headers,
|
49
56
|
:save_screenshot,
|
50
57
|
:source,
|
51
|
-
:status_code
|
58
|
+
:status_code,
|
59
|
+
:title
|
52
60
|
|
53
61
|
def within_window(name)
|
62
|
+
name = name['id'] unless String === name
|
54
63
|
current_browser = browser
|
55
64
|
Terminus.browser = browser.id + '/' + name
|
56
65
|
result = yield
|
@@ -83,6 +92,7 @@ end
|
|
83
92
|
|
84
93
|
Capybara.server do |app, port|
|
85
94
|
handler = Rack::Handler.get('webrick')
|
86
|
-
|
95
|
+
logger = Terminus.debug ? Logger.new(STDOUT) : WEBrick::Log.new(nil, 0)
|
96
|
+
handler.run(app, :Port => port, :AccessLog => [], :Logger => logger)
|
87
97
|
end
|
88
98
|
|
data/lib/terminus.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'erb'
|
2
2
|
require 'forwardable'
|
3
3
|
require 'http/parser'
|
4
|
+
require 'logger'
|
4
5
|
require 'net/http'
|
5
6
|
require 'rbconfig'
|
6
7
|
require 'readline'
|
@@ -18,7 +19,7 @@ require 'useragent'
|
|
18
19
|
|
19
20
|
module Terminus
|
20
21
|
FAYE_MOUNT = '/messaging'
|
21
|
-
DEFAULT_HOST = '
|
22
|
+
DEFAULT_HOST = '127.0.0.1'
|
22
23
|
DEFAULT_PORT = 7004
|
23
24
|
LOCALHOST = /^(localhost|0\.0\.0\.0|127\.0\.0\.1)$/
|
24
25
|
PING_PATH = '/favicon.ico'
|
data/lib/terminus/browser.rb
CHANGED
@@ -34,12 +34,12 @@ module Terminus
|
|
34
34
|
def ask(command, retries = RETRY_LIMIT)
|
35
35
|
debug(:ask, id, command)
|
36
36
|
value = if @connector
|
37
|
-
message =
|
37
|
+
message = MultiJson.dump('commandId' => '_', 'command' => command)
|
38
38
|
response = @connector.request(message)
|
39
39
|
if response.nil?
|
40
40
|
retries == false ? false : ask(command)
|
41
41
|
else
|
42
|
-
result_hash =
|
42
|
+
result_hash = MultiJson.load(response)
|
43
43
|
result_hash['value']
|
44
44
|
end
|
45
45
|
else
|
@@ -87,8 +87,14 @@ module Terminus
|
|
87
87
|
nil
|
88
88
|
end
|
89
89
|
|
90
|
-
def
|
91
|
-
|
90
|
+
def find_css(css, driver = nil)
|
91
|
+
return [] unless @find_enabled
|
92
|
+
ask([:find_css, css, false]).map { |id| Node.new(self, id, driver) }
|
93
|
+
end
|
94
|
+
|
95
|
+
def find_xpath(xpath, driver = nil)
|
96
|
+
return [] unless @find_enabled
|
97
|
+
ask([:find_xpath, xpath, false]).map { |id| Node.new(self, id, driver) }
|
92
98
|
end
|
93
99
|
|
94
100
|
def frame!(frame_browser)
|
@@ -99,6 +105,14 @@ module Terminus
|
|
99
105
|
@frames.to_a
|
100
106
|
end
|
101
107
|
|
108
|
+
def go_back
|
109
|
+
execute_script('window.history.back()')
|
110
|
+
end
|
111
|
+
|
112
|
+
def go_forward
|
113
|
+
execute_script('window.history.forward()')
|
114
|
+
end
|
115
|
+
|
102
116
|
def html
|
103
117
|
ask([:body])
|
104
118
|
end
|
@@ -132,8 +146,10 @@ module Terminus
|
|
132
146
|
@attributes['raw_url'] = message['url']
|
133
147
|
message['url'] = rewrite_local(message['url'])
|
134
148
|
|
135
|
-
@attributes
|
136
|
-
@
|
149
|
+
@attributes = @attributes.merge(message)
|
150
|
+
@find_enabled = true
|
151
|
+
@user_agent = UserAgent.parse(message['ua'].gsub(/.*?\bOPR\b/, 'Opera'))
|
152
|
+
|
137
153
|
detect_dock_host
|
138
154
|
|
139
155
|
@infinite_redirect = message['infinite']
|
@@ -160,6 +176,7 @@ module Terminus
|
|
160
176
|
ask([:clear_cookies])
|
161
177
|
|
162
178
|
@attributes.delete('url')
|
179
|
+
@find_enabled = false
|
163
180
|
end
|
164
181
|
|
165
182
|
def response_headers
|
@@ -205,12 +222,18 @@ module Terminus
|
|
205
222
|
command_id
|
206
223
|
end
|
207
224
|
|
225
|
+
def title
|
226
|
+
ask([:title])
|
227
|
+
end
|
228
|
+
|
208
229
|
def visit(url, retries = RETRY_LIMIT)
|
209
230
|
close_frames!
|
210
231
|
uri = @controller.rewrite_remote(url, @dock_host)
|
211
232
|
uri.host = @dock_host if uri.host =~ LOCALHOST
|
212
233
|
@controller.visit_url(uri.to_s)
|
213
234
|
|
235
|
+
@find_enabled = true
|
236
|
+
|
214
237
|
if @connector
|
215
238
|
ask([:visit, uri.to_s], false)
|
216
239
|
@connector.drain_socket
|
@@ -277,7 +300,7 @@ module Terminus
|
|
277
300
|
|
278
301
|
def start_connector
|
279
302
|
return if @connector or @dock_host.nil? or Terminus.browser != self
|
280
|
-
@connector = Connector
|
303
|
+
@connector = Connector.new(self)
|
281
304
|
url = "ws://#{@dock_host}:#{@connector.port}/"
|
282
305
|
debug(:connect, id, url)
|
283
306
|
messenger.publish(socket_channel, 'url' => url)
|
data/lib/terminus/client.rb
CHANGED
@@ -25,7 +25,7 @@ module Terminus
|
|
25
25
|
@id = Faye.random
|
26
26
|
@options = options
|
27
27
|
@address = TCPServer.new(0).addr
|
28
|
-
@connector = Connector
|
28
|
+
@connector = Connector.new(self)
|
29
29
|
@port = options[:port] || @address[1]
|
30
30
|
@terminus = Terminus.create(:port => @port)
|
31
31
|
@browser = ChildProcess.build(*browser_args)
|
data/lib/terminus/connector.rb
CHANGED
@@ -1,8 +1,125 @@
|
|
1
|
+
# Based on code from the Poltergeist project
|
2
|
+
# https://github.com/jonleighton/poltergeist
|
3
|
+
#
|
4
|
+
# Copyright (c) 2011 Jonathan Leighton
|
5
|
+
#
|
6
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
7
|
+
# of this software and associated documentation files (the "Software"), to deal
|
8
|
+
# in the Software without restriction, including without limitation the rights
|
9
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
10
|
+
# copies of the Software, and to permit persons to whom the Software is
|
11
|
+
# furnished to do so, subject to the following conditions:
|
12
|
+
#
|
13
|
+
# The above copyright notice and this permission notice shall be included in
|
14
|
+
# all copies or substantial portions of the Software.
|
15
|
+
#
|
16
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
17
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
18
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
19
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
20
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
21
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
22
|
+
# SOFTWARE.
|
23
|
+
|
24
|
+
require 'websocket/driver'
|
25
|
+
|
1
26
|
module Terminus
|
2
|
-
|
27
|
+
class Connector
|
28
|
+
|
29
|
+
RECV_SIZE = 1024
|
30
|
+
BIND_TIMEOUT = 5
|
31
|
+
|
32
|
+
def initialize(browser, timeout = BIND_TIMEOUT)
|
33
|
+
@browser = browser
|
34
|
+
@skips = 0
|
35
|
+
@server = start_server
|
36
|
+
@timeout = timeout
|
37
|
+
reset
|
38
|
+
end
|
39
|
+
|
40
|
+
def reset
|
41
|
+
@closing = false
|
42
|
+
@driver = nil
|
43
|
+
@socket = nil
|
44
|
+
end
|
45
|
+
|
46
|
+
def connected?
|
47
|
+
not @socket.nil?
|
48
|
+
end
|
49
|
+
|
50
|
+
def port
|
51
|
+
@server.addr[1]
|
52
|
+
end
|
53
|
+
|
54
|
+
def request(message)
|
55
|
+
@browser.debug(:send, @browser.id, message)
|
56
|
+
accept unless connected?
|
57
|
+
@driver.text(message)
|
58
|
+
true while @closing && receive
|
59
|
+
result = receive
|
60
|
+
@browser.debug(:recv, @browser.id, result)
|
61
|
+
reset if result.nil?
|
62
|
+
result
|
63
|
+
rescue Errno::EBADF, Errno::ECONNRESET, Errno::EPIPE, Errno::EWOULDBLOCK
|
64
|
+
reset
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
|
68
|
+
def drain_socket
|
69
|
+
@closing = true if @socket
|
70
|
+
end
|
71
|
+
|
72
|
+
def close
|
73
|
+
[@server, @socket].compact.each do |s|
|
74
|
+
s.close_read
|
75
|
+
s.close_write
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def write(data)
|
80
|
+
@socket.write(data)
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def start_server
|
86
|
+
time = Time.now
|
87
|
+
TCPServer.open(0)
|
88
|
+
rescue Errno::EADDRINUSE
|
89
|
+
if (Time.now - time) < BIND_TIMEOUT
|
90
|
+
sleep(0.01)
|
91
|
+
retry
|
92
|
+
else
|
93
|
+
raise
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def accept
|
98
|
+
@skips.times { @server.accept.close }
|
99
|
+
|
100
|
+
@socket = @server.accept
|
101
|
+
@messages = []
|
102
|
+
@driver = WebSocket::Driver.server(self)
|
103
|
+
|
104
|
+
@driver.on(:connect) { |e| @driver.start }
|
105
|
+
@driver.on(:message) { |e| @messages << e.data }
|
106
|
+
|
107
|
+
# @browser.debug(:accept, @browser.id, @handler.url)
|
108
|
+
end
|
109
|
+
|
110
|
+
def receive
|
111
|
+
@browser.debug(:receive, @browser.id)
|
112
|
+
start = Time.now
|
3
113
|
|
4
|
-
|
5
|
-
|
114
|
+
until @messages.any?
|
115
|
+
raise Errno::EWOULDBLOCK if (Time.now - start) >= @timeout
|
116
|
+
IO.select([@socket], [], [], @timeout) or raise Errno::EWOULDBLOCK
|
117
|
+
data = @socket.recv(RECV_SIZE)
|
118
|
+
break if data.empty?
|
119
|
+
@driver.parse(data)
|
120
|
+
end
|
121
|
+
@messages.shift
|
122
|
+
end
|
6
123
|
|
7
124
|
end
|
8
125
|
end
|
data/lib/terminus/controller.rb
CHANGED
@@ -55,7 +55,7 @@ module Terminus
|
|
55
55
|
|
56
56
|
def register_local_port(port)
|
57
57
|
@local_ports << port
|
58
|
-
@host_aliases[Host.new(URI.parse("http://
|
58
|
+
@host_aliases[Host.new(URI.parse("http://127.0.0.1:#{port}/"))] = Host.new(URI.parse("http://127.0.0.1:80/"))
|
59
59
|
end
|
60
60
|
|
61
61
|
def return_to_dock
|
data/lib/terminus/node.rb
CHANGED
@@ -40,10 +40,15 @@ module Terminus
|
|
40
40
|
end
|
41
41
|
alias :eql? :==
|
42
42
|
|
43
|
-
def
|
44
|
-
@browser.ask([:
|
43
|
+
def find_css(css)
|
44
|
+
@browser.ask([:find_css, css, @id]).map { |id| Node.new(@browser, id, @driver) }
|
45
45
|
end
|
46
46
|
|
47
|
+
def find_xpath(xpath)
|
48
|
+
@browser.ask([:find_xpath, xpath, @id]).map { |id| Node.new(@browser, id, @driver) }
|
49
|
+
end
|
50
|
+
alias :find :find_xpath
|
51
|
+
|
47
52
|
def hash
|
48
53
|
@id.hash
|
49
54
|
end
|
@@ -66,6 +71,15 @@ module Terminus
|
|
66
71
|
raise Capybara::NotSupportedByDriverError.new if result == 'not_allowed'
|
67
72
|
end
|
68
73
|
|
74
|
+
def all_text
|
75
|
+
@browser.ask([:text, @id, false])
|
76
|
+
end
|
77
|
+
|
78
|
+
def visible_text
|
79
|
+
@browser.ask([:text, @id, true])
|
80
|
+
end
|
81
|
+
alias :text :visible_text
|
82
|
+
|
69
83
|
def trigger(event_type)
|
70
84
|
@browser.ask([:trigger, @id, event_type])
|
71
85
|
end
|
@@ -85,8 +99,8 @@ module Terminus
|
|
85
99
|
|
86
100
|
SYNC_DSL_METHODS = [ [:[], :attribute],
|
87
101
|
[:[]=, :set_attribute],
|
102
|
+
[:disabled?, :is_disabled],
|
88
103
|
:tag_name,
|
89
|
-
:text,
|
90
104
|
:value,
|
91
105
|
[:visible?, :is_visible]
|
92
106
|
]
|
data/lib/terminus/proxy.rb
CHANGED
@@ -40,7 +40,7 @@ module Terminus
|
|
40
40
|
|
41
41
|
def self.content_type(response)
|
42
42
|
type = response[1].find { |key, _| key =~ /^content-type$/i }
|
43
|
-
type && type.last.split(';').first
|
43
|
+
type && type.flatten.last.split(';').first
|
44
44
|
end
|
45
45
|
|
46
46
|
def initialize(app)
|
@@ -69,7 +69,7 @@ module Terminus
|
|
69
69
|
set_cookie = response[1].keys.grep(/^set-cookie$/i).first
|
70
70
|
return unless set_cookie
|
71
71
|
|
72
|
-
host = External === @app ? @app.
|
72
|
+
host = External === @app ? @app.host : env['HTTP_HOST']
|
73
73
|
endpoint = "http://#{host}#{env['PATH_INFO']}"
|
74
74
|
|
75
75
|
[*response[1][set_cookie]].compact.each do |cookie|
|
@@ -100,11 +100,13 @@ module Terminus
|
|
100
100
|
end
|
101
101
|
|
102
102
|
def rewrite_response(env, response)
|
103
|
+
headers = response[1]
|
104
|
+
|
105
|
+
headers.each_key { |k| headers[k] = [headers[k]].flatten.first }
|
103
106
|
rewrite_location(env, response)
|
104
107
|
return response if return_unmodified?(env, response)
|
105
108
|
|
106
|
-
|
107
|
-
response[1].delete_if { |key, _| key =~ /^content-length$/i }
|
109
|
+
headers.delete_if { |key, _| key =~ /^content-length$/i }
|
108
110
|
response[2] = DriverBody.new(env, response)
|
109
111
|
response
|
110
112
|
end
|