intentmedia-capybara-webkit 0.7.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/Appraisals +7 -0
- data/CONTRIBUTING.md +38 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +65 -0
- data/LICENSE +19 -0
- data/README.md +67 -0
- data/Rakefile +78 -0
- data/bin/Info.plist +22 -0
- data/capybara-webkit.gemspec +24 -0
- data/extconf.rb +2 -0
- data/gemfiles/1.0.gemfile +7 -0
- data/gemfiles/1.0.gemfile.lock +65 -0
- data/gemfiles/1.1.gemfile +7 -0
- data/gemfiles/1.1.gemfile.lock +65 -0
- data/lib/capybara-webkit.rb +1 -0
- data/lib/capybara/driver/webkit.rb +113 -0
- data/lib/capybara/driver/webkit/browser.rb +216 -0
- data/lib/capybara/driver/webkit/node.rb +118 -0
- data/lib/capybara/driver/webkit/socket_debugger.rb +43 -0
- data/lib/capybara/webkit.rb +11 -0
- data/lib/capybara_webkit_builder.rb +40 -0
- data/spec/browser_spec.rb +178 -0
- data/spec/driver_rendering_spec.rb +80 -0
- data/spec/driver_spec.rb +1048 -0
- data/spec/integration/driver_spec.rb +20 -0
- data/spec/integration/session_spec.rb +137 -0
- data/spec/self_signed_ssl_cert.rb +42 -0
- data/spec/spec_helper.rb +25 -0
- data/src/Body.h +12 -0
- data/src/ClearCookies.cpp +18 -0
- data/src/ClearCookies.h +11 -0
- data/src/Command.cpp +15 -0
- data/src/Command.h +29 -0
- data/src/CommandFactory.cpp +29 -0
- data/src/CommandFactory.h +16 -0
- data/src/CommandParser.cpp +68 -0
- data/src/CommandParser.h +29 -0
- data/src/Connection.cpp +82 -0
- data/src/Connection.h +36 -0
- data/src/Evaluate.cpp +84 -0
- data/src/Evaluate.h +22 -0
- data/src/Execute.cpp +16 -0
- data/src/Execute.h +12 -0
- data/src/Find.cpp +19 -0
- data/src/Find.h +13 -0
- data/src/FrameFocus.cpp +66 -0
- data/src/FrameFocus.h +28 -0
- data/src/GetCookies.cpp +22 -0
- data/src/GetCookies.h +14 -0
- data/src/Header.cpp +18 -0
- data/src/Header.h +11 -0
- data/src/Headers.cpp +11 -0
- data/src/Headers.h +12 -0
- data/src/JavascriptInvocation.cpp +14 -0
- data/src/JavascriptInvocation.h +19 -0
- data/src/NetworkAccessManager.cpp +25 -0
- data/src/NetworkAccessManager.h +18 -0
- data/src/NetworkCookieJar.cpp +101 -0
- data/src/NetworkCookieJar.h +15 -0
- data/src/Node.cpp +14 -0
- data/src/Node.h +13 -0
- data/src/Render.cpp +19 -0
- data/src/Render.h +12 -0
- data/src/Reset.cpp +20 -0
- data/src/Reset.h +12 -0
- data/src/Response.cpp +19 -0
- data/src/Response.h +13 -0
- data/src/Server.cpp +25 -0
- data/src/Server.h +21 -0
- data/src/SetCookie.cpp +18 -0
- data/src/SetCookie.h +11 -0
- data/src/SetProxy.cpp +24 -0
- data/src/SetProxy.h +11 -0
- data/src/Source.cpp +20 -0
- data/src/Source.h +19 -0
- data/src/Status.cpp +13 -0
- data/src/Status.h +12 -0
- data/src/UnsupportedContentHandler.cpp +32 -0
- data/src/UnsupportedContentHandler.h +18 -0
- data/src/Url.cpp +15 -0
- data/src/Url.h +12 -0
- data/src/Visit.cpp +21 -0
- data/src/Visit.h +15 -0
- data/src/WebPage.cpp +226 -0
- data/src/WebPage.h +54 -0
- data/src/body.cpp +11 -0
- data/src/capybara.js +205 -0
- data/src/find_command.h +24 -0
- data/src/main.cpp +34 -0
- data/src/webkit_server.pro +71 -0
- data/src/webkit_server.qrc +5 -0
- data/templates/Command.cpp +10 -0
- data/templates/Command.h +12 -0
- data/webkit_server.pro +4 -0
- metadata +246 -0
@@ -0,0 +1,43 @@
|
|
1
|
+
# Wraps the TCP socket and prints data sent and received. Used for debugging
|
2
|
+
# the wire protocol. You can use this by passing a :socket_class to Browser.
|
3
|
+
class Capybara::Driver::Webkit
|
4
|
+
class SocketDebugger
|
5
|
+
def self.open(host, port)
|
6
|
+
real_socket = TCPSocket.open(host, port)
|
7
|
+
new(real_socket)
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(socket)
|
11
|
+
@socket = socket
|
12
|
+
end
|
13
|
+
|
14
|
+
def read(length)
|
15
|
+
received @socket.read(length)
|
16
|
+
end
|
17
|
+
|
18
|
+
def puts(line)
|
19
|
+
sent line
|
20
|
+
@socket.puts(line)
|
21
|
+
end
|
22
|
+
|
23
|
+
def print(content)
|
24
|
+
sent content
|
25
|
+
@socket.print(content)
|
26
|
+
end
|
27
|
+
|
28
|
+
def gets
|
29
|
+
received @socket.gets
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def sent(content)
|
35
|
+
Kernel.puts " >> " + content.to_s
|
36
|
+
end
|
37
|
+
|
38
|
+
def received(content)
|
39
|
+
Kernel.puts " << " + content.to_s
|
40
|
+
content
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require "capybara"
|
2
|
+
require "capybara/driver/webkit"
|
3
|
+
|
4
|
+
Capybara.register_driver :webkit do |app|
|
5
|
+
Capybara::Driver::Webkit.new(app)
|
6
|
+
end
|
7
|
+
|
8
|
+
Capybara.register_driver :webkit_debug do |app|
|
9
|
+
browser = Capybara::Driver::Webkit::Browser.new(:socket_class => Capybara::Driver::Webkit::SocketDebugger)
|
10
|
+
Capybara::Driver::Webkit.new(app, :browser => browser)
|
11
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require "fileutils"
|
2
|
+
|
3
|
+
module CapybaraWebkitBuilder
|
4
|
+
extend self
|
5
|
+
|
6
|
+
def make_bin
|
7
|
+
make_binaries = ['gmake', 'make']
|
8
|
+
make_binaries.detect { |make| system("which #{make}") }
|
9
|
+
end
|
10
|
+
|
11
|
+
def makefile
|
12
|
+
qmake_binaries = ['qmake', 'qmake-qt4']
|
13
|
+
qmake = qmake_binaries.detect { |qmake| system("which #{qmake}") }
|
14
|
+
case RUBY_PLATFORM
|
15
|
+
when /linux/
|
16
|
+
system("#{qmake} -spec linux-g++")
|
17
|
+
when /freebsd/
|
18
|
+
system("#{qmake} -spec freebsd-g++")
|
19
|
+
else
|
20
|
+
system("#{qmake} -spec macx-g++")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def qmake
|
25
|
+
system("#{make_bin} qmake")
|
26
|
+
end
|
27
|
+
|
28
|
+
def build
|
29
|
+
system(make_bin) or return false
|
30
|
+
|
31
|
+
FileUtils.mkdir("bin") unless File.directory?("bin")
|
32
|
+
FileUtils.cp("src/webkit_server", "bin", :preserve => true)
|
33
|
+
end
|
34
|
+
|
35
|
+
def build_all
|
36
|
+
makefile &&
|
37
|
+
qmake &&
|
38
|
+
build
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'self_signed_ssl_cert'
|
3
|
+
require 'stringio'
|
4
|
+
require 'capybara/driver/webkit/browser'
|
5
|
+
require 'socket'
|
6
|
+
require 'base64'
|
7
|
+
|
8
|
+
describe Capybara::Driver::Webkit::Browser do
|
9
|
+
|
10
|
+
let(:browser) { Capybara::Driver::Webkit::Browser.new }
|
11
|
+
let(:browser_ignore_ssl_err) {
|
12
|
+
Capybara::Driver::Webkit::Browser.new(:ignore_ssl_errors => true)
|
13
|
+
}
|
14
|
+
|
15
|
+
describe '#server_port' do
|
16
|
+
subject { browser.server_port }
|
17
|
+
it 'returns a valid port number' do
|
18
|
+
should be_a(Integer)
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'returns a port in the allowed range' do
|
22
|
+
should be_between 0x400, 0xffff
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'random port' do
|
27
|
+
it 'chooses a new port number for a new browser instance' do
|
28
|
+
new_browser = Capybara::Driver::Webkit::Browser.new
|
29
|
+
new_browser.server_port.should_not == browser.server_port
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'forwards stdout to the given IO object' do
|
34
|
+
io = StringIO.new
|
35
|
+
new_browser = Capybara::Driver::Webkit::Browser.new(:stdout => io)
|
36
|
+
new_browser.execute_script('console.log("hello world")')
|
37
|
+
sleep(0.5)
|
38
|
+
io.string.should == "hello world\n"
|
39
|
+
end
|
40
|
+
|
41
|
+
context 'handling of SSL validation errors' do
|
42
|
+
before do
|
43
|
+
# set up minimal HTTPS server
|
44
|
+
@host = "127.0.0.1"
|
45
|
+
@server = TCPServer.new(@host, 0)
|
46
|
+
@port = @server.addr[1]
|
47
|
+
|
48
|
+
# set up SSL layer
|
49
|
+
ssl_serv = OpenSSL::SSL::SSLServer.new(@server, $openssl_self_signed_ctx)
|
50
|
+
|
51
|
+
@server_thread = Thread.new(ssl_serv) do |serv|
|
52
|
+
while conn = serv.accept do
|
53
|
+
# read request
|
54
|
+
request = []
|
55
|
+
until (line = conn.readline.strip).empty?
|
56
|
+
request << line
|
57
|
+
end
|
58
|
+
|
59
|
+
# write response
|
60
|
+
html = "<html><body>D'oh!</body></html>"
|
61
|
+
conn.write "HTTP/1.1 200 OK\r\n"
|
62
|
+
conn.write "Content-Type:text/html\r\n"
|
63
|
+
conn.write "Content-Length: %i\r\n" % html.size
|
64
|
+
conn.write "\r\n"
|
65
|
+
conn.write html
|
66
|
+
conn.close
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
after do
|
72
|
+
@server_thread.kill
|
73
|
+
@server.close
|
74
|
+
end
|
75
|
+
|
76
|
+
it "doesn't accept a self-signed certificate by default" do
|
77
|
+
lambda { browser.visit "https://#{@host}:#{@port}/" }.should raise_error
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'accepts a self-signed certificate if configured to do so' do
|
81
|
+
browser_ignore_ssl_err.visit "https://#{@host}:#{@port}/"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
describe "forking" do
|
85
|
+
it "only shuts down the server from the main process" do
|
86
|
+
browser.reset!
|
87
|
+
pid = fork {}
|
88
|
+
Process.wait(pid)
|
89
|
+
expect { browser.reset! }.not_to raise_error
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
describe '#set_proxy' do
|
94
|
+
before do
|
95
|
+
@host = '127.0.0.1'
|
96
|
+
@user = 'user'
|
97
|
+
@pass = 'secret'
|
98
|
+
@url = "http://example.org/"
|
99
|
+
|
100
|
+
@server = TCPServer.new(@host, 0)
|
101
|
+
@port = @server.addr[1]
|
102
|
+
|
103
|
+
@proxy_requests = []
|
104
|
+
@proxy = Thread.new(@server, @proxy_requests) do |serv, proxy_requests|
|
105
|
+
while conn = serv.accept do
|
106
|
+
# read request
|
107
|
+
request = []
|
108
|
+
until (line = conn.readline.strip).empty?
|
109
|
+
request << line
|
110
|
+
end
|
111
|
+
|
112
|
+
# send response
|
113
|
+
auth_header = request.find { |h| h =~ /Authorization:/i }
|
114
|
+
if auth_header || request[0].split(/\s+/)[1] =~ /^\//
|
115
|
+
html = "<html><body>D'oh!</body></html>"
|
116
|
+
conn.write "HTTP/1.1 200 OK\r\n"
|
117
|
+
conn.write "Content-Type:text/html\r\n"
|
118
|
+
conn.write "Content-Length: %i\r\n" % html.size
|
119
|
+
conn.write "\r\n"
|
120
|
+
conn.write html
|
121
|
+
conn.close
|
122
|
+
proxy_requests << request if auth_header
|
123
|
+
else
|
124
|
+
conn.write "HTTP/1.1 407 Proxy Auth Required\r\n"
|
125
|
+
conn.write "Proxy-Authenticate: Basic realm=\"Proxy\"\r\n"
|
126
|
+
conn.write "\r\n"
|
127
|
+
conn.close
|
128
|
+
proxy_requests << request
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
browser.set_proxy(:host => @host,
|
134
|
+
:port => @port,
|
135
|
+
:user => @user,
|
136
|
+
:pass => @pass)
|
137
|
+
browser.visit @url
|
138
|
+
@proxy_requests.size.should == 2
|
139
|
+
@request = @proxy_requests[-1]
|
140
|
+
end
|
141
|
+
|
142
|
+
after do
|
143
|
+
@proxy.kill
|
144
|
+
@server.close
|
145
|
+
end
|
146
|
+
|
147
|
+
it 'uses the HTTP proxy correctly' do
|
148
|
+
@request[0].should match /^GET\s+http:\/\/example.org\/\s+HTTP/i
|
149
|
+
@request.find { |header|
|
150
|
+
header =~ /^Host:\s+example.org$/i }.should_not be nil
|
151
|
+
end
|
152
|
+
|
153
|
+
it 'sends correct proxy authentication' do
|
154
|
+
auth_header = @request.find { |header|
|
155
|
+
header =~ /^Proxy-Authorization:\s+/i }
|
156
|
+
auth_header.should_not be nil
|
157
|
+
|
158
|
+
user, pass = Base64.decode64(auth_header.split(/\s+/)[-1]).split(":")
|
159
|
+
user.should == @user
|
160
|
+
pass.should == @pass
|
161
|
+
end
|
162
|
+
|
163
|
+
it "uses the proxies' response" do
|
164
|
+
browser.body.should include "D'oh!"
|
165
|
+
end
|
166
|
+
|
167
|
+
it 'uses original URL' do
|
168
|
+
browser.url.should == @url
|
169
|
+
end
|
170
|
+
|
171
|
+
it 'is possible to disable proxy again' do
|
172
|
+
@proxy_requests.clear
|
173
|
+
browser.clear_proxy
|
174
|
+
browser.visit "http://#{@host}:#{@port}/"
|
175
|
+
@proxy_requests.size.should == 0
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'capybara/driver/webkit'
|
3
|
+
require 'mini_magick'
|
4
|
+
|
5
|
+
describe Capybara::Driver::Webkit, "rendering an image" do
|
6
|
+
|
7
|
+
before(:all) do
|
8
|
+
# Set up the tmp directory and file name
|
9
|
+
tmp_dir = File.join(PROJECT_ROOT, 'tmp')
|
10
|
+
FileUtils.mkdir_p tmp_dir
|
11
|
+
@file_name = File.join(tmp_dir, 'render-test.png')
|
12
|
+
|
13
|
+
app = lambda do |env|
|
14
|
+
body = <<-HTML
|
15
|
+
<html>
|
16
|
+
<body>
|
17
|
+
<h1>Hello World</h1>
|
18
|
+
</body>
|
19
|
+
</html>
|
20
|
+
HTML
|
21
|
+
[200,
|
22
|
+
{ 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
|
23
|
+
[body]]
|
24
|
+
end
|
25
|
+
|
26
|
+
@driver = Capybara::Driver::Webkit.new(app, :browser => $webkit_browser)
|
27
|
+
@driver.visit("/hello/world?success=true")
|
28
|
+
end
|
29
|
+
|
30
|
+
after(:all) { @driver.reset! }
|
31
|
+
|
32
|
+
def render(options)
|
33
|
+
FileUtils.rm_f @file_name
|
34
|
+
@driver.render @file_name, options
|
35
|
+
|
36
|
+
@image = MiniMagick::Image.open @file_name
|
37
|
+
end
|
38
|
+
|
39
|
+
context "with default options" do
|
40
|
+
before(:all){ render({}) }
|
41
|
+
|
42
|
+
it "should be a PNG" do
|
43
|
+
@image[:format].should == "PNG"
|
44
|
+
end
|
45
|
+
|
46
|
+
it "width default to 1000px (with 15px less for the scrollbar)" do
|
47
|
+
@image[:width].should be < 1001
|
48
|
+
@image[:width].should be > 1000-17
|
49
|
+
end
|
50
|
+
|
51
|
+
it "height should be at least 10px" do
|
52
|
+
@image[:height].should >= 10
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
context "with dimensions set larger than necessary" do
|
57
|
+
before(:all){ render(:width => 500, :height => 400) }
|
58
|
+
|
59
|
+
it "width should match the width given" do
|
60
|
+
@image[:width].should == 500
|
61
|
+
end
|
62
|
+
|
63
|
+
it "height should match the height given" do
|
64
|
+
@image[:height].should > 10
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
context "with dimensions set smaller than the document's default" do
|
69
|
+
before(:all){ render(:width => 50, :height => 10) }
|
70
|
+
|
71
|
+
it "width should be greater than the width given" do
|
72
|
+
@image[:width].should > 50
|
73
|
+
end
|
74
|
+
|
75
|
+
it "height should be greater than the height given" do
|
76
|
+
@image[:height].should > 10
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
data/spec/driver_spec.rb
ADDED
@@ -0,0 +1,1048 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'capybara/driver/webkit'
|
3
|
+
|
4
|
+
describe Capybara::Driver::Webkit do
|
5
|
+
subject { Capybara::Driver::Webkit.new(@app, :browser => $webkit_browser) }
|
6
|
+
before { subject.visit("/hello/world?success=true") }
|
7
|
+
after { subject.reset! }
|
8
|
+
|
9
|
+
context "iframe app" do
|
10
|
+
before(:all) do
|
11
|
+
@app = lambda do |env|
|
12
|
+
params = ::Rack::Utils.parse_query(env['QUERY_STRING'])
|
13
|
+
if params["iframe"] == "true"
|
14
|
+
# We are in an iframe request.
|
15
|
+
p_id = "farewell"
|
16
|
+
msg = "goodbye"
|
17
|
+
iframe = nil
|
18
|
+
else
|
19
|
+
# We are not in an iframe request and need to make an iframe!
|
20
|
+
p_id = "greeting"
|
21
|
+
msg = "hello"
|
22
|
+
iframe = "<iframe id=\"f\" src=\"/?iframe=true\"></iframe>"
|
23
|
+
end
|
24
|
+
body = <<-HTML
|
25
|
+
<html>
|
26
|
+
<head>
|
27
|
+
<style type="text/css">
|
28
|
+
#display_none { display: none }
|
29
|
+
</style>
|
30
|
+
</head>
|
31
|
+
<body>
|
32
|
+
#{iframe}
|
33
|
+
<script type="text/javascript">
|
34
|
+
document.write("<p id='#{p_id}'>#{msg}</p>");
|
35
|
+
</script>
|
36
|
+
</body>
|
37
|
+
</html>
|
38
|
+
HTML
|
39
|
+
[200,
|
40
|
+
{ 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
|
41
|
+
[body]]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
it "finds frames by index" do
|
46
|
+
subject.within_frame(0) do
|
47
|
+
subject.find("//*[contains(., 'goodbye')]").should_not be_empty
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
it "finds frames by id" do
|
52
|
+
subject.within_frame("f") do
|
53
|
+
subject.find("//*[contains(., 'goodbye')]").should_not be_empty
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
it "raises error for missing frame by index" do
|
58
|
+
expect { subject.within_frame(1) { } }.
|
59
|
+
to raise_error(Capybara::Driver::Webkit::WebkitInvalidResponseError)
|
60
|
+
end
|
61
|
+
|
62
|
+
it "raise_error for missing frame by id" do
|
63
|
+
expect { subject.within_frame("foo") { } }.
|
64
|
+
to raise_error(Capybara::Driver::Webkit::WebkitInvalidResponseError)
|
65
|
+
end
|
66
|
+
|
67
|
+
it "returns an attribute's value" do
|
68
|
+
subject.within_frame("f") do
|
69
|
+
subject.find("//p").first["id"].should == "farewell"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
it "returns a node's text" do
|
74
|
+
subject.within_frame("f") do
|
75
|
+
subject.find("//p").first.text.should == "goodbye"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
it "returns the current URL" do
|
80
|
+
subject.within_frame("f") do
|
81
|
+
port = subject.instance_variable_get("@rack_server").port
|
82
|
+
subject.current_url.should == "http://127.0.0.1:#{port}/?iframe=true"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
it "returns the source code for the page" do
|
87
|
+
subject.within_frame("f") do
|
88
|
+
subject.source.should =~ %r{<html>.*farewell.*}m
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
it "evaluates Javascript" do
|
93
|
+
subject.within_frame("f") do
|
94
|
+
result = subject.evaluate_script(%<document.getElementById('farewell').innerText>)
|
95
|
+
result.should == "goodbye"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
it "executes Javascript" do
|
100
|
+
subject.within_frame("f") do
|
101
|
+
subject.execute_script(%<document.getElementById('farewell').innerHTML = 'yo'>)
|
102
|
+
subject.find("//p[contains(., 'yo')]").should_not be_empty
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
context "redirect app" do
|
108
|
+
before(:all) do
|
109
|
+
@app = lambda do |env|
|
110
|
+
if env['PATH_INFO'] == '/target'
|
111
|
+
content_type = "<p>#{env['CONTENT_TYPE']}</p>"
|
112
|
+
[200, {"Content-Type" => "text/html", "Content-Length" => content_type.length.to_s}, [content_type]]
|
113
|
+
elsif env['PATH_INFO'] == '/form'
|
114
|
+
body = <<-HTML
|
115
|
+
<html>
|
116
|
+
<body>
|
117
|
+
<form action="/redirect" method="POST" enctype="multipart/form-data">
|
118
|
+
<input name="submit" type="submit" />
|
119
|
+
</form>
|
120
|
+
</body>
|
121
|
+
</html>
|
122
|
+
HTML
|
123
|
+
[200, {"Content-Type" => "text/html", "Content-Length" => body.length.to_s}, [body]]
|
124
|
+
else
|
125
|
+
[301, {"Location" => "/target"}, [""]]
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
it "should redirect without content type" do
|
131
|
+
subject.visit("/form")
|
132
|
+
subject.find("//input").first.click
|
133
|
+
subject.find("//p").first.text.should == ""
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
context "css app" do
|
138
|
+
before(:all) do
|
139
|
+
body = "css"
|
140
|
+
@app = lambda do |env|
|
141
|
+
[200, {"Content-Type" => "text/css", "Content-Length" => body.length.to_s}, [body]]
|
142
|
+
end
|
143
|
+
subject.visit("/")
|
144
|
+
end
|
145
|
+
|
146
|
+
it "renders unsupported content types gracefully" do
|
147
|
+
subject.body.should =~ /css/
|
148
|
+
end
|
149
|
+
|
150
|
+
it "sets the response headers with respect to the unsupported request" do
|
151
|
+
subject.response_headers["Content-Type"].should == "text/css"
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
context "hello app" do
|
156
|
+
before(:all) do
|
157
|
+
@app = lambda do |env|
|
158
|
+
body = <<-HTML
|
159
|
+
<html>
|
160
|
+
<head>
|
161
|
+
<style type="text/css">
|
162
|
+
#display_none { display: none }
|
163
|
+
</style>
|
164
|
+
</head>
|
165
|
+
<body>
|
166
|
+
<div class='normalize'>Spaces not normalized </div>
|
167
|
+
<div id="display_none">
|
168
|
+
<div id="invisible">Can't see me</div>
|
169
|
+
</div>
|
170
|
+
<input type="text" disabled="disabled"/>
|
171
|
+
<input id="checktest" type="checkbox" checked="checked"/>
|
172
|
+
<script type="text/javascript">
|
173
|
+
document.write("<p id='greeting'>he" + "llo</p>");
|
174
|
+
</script>
|
175
|
+
</body>
|
176
|
+
</html>
|
177
|
+
HTML
|
178
|
+
[200,
|
179
|
+
{ 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
|
180
|
+
[body]]
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
it "handles anchor tags" do
|
185
|
+
subject.visit("#test")
|
186
|
+
subject.find("//*[contains(., 'hello')]").should_not be_empty
|
187
|
+
subject.visit("#test")
|
188
|
+
subject.find("//*[contains(., 'hello')]").should_not be_empty
|
189
|
+
end
|
190
|
+
|
191
|
+
it "finds content after loading a URL" do
|
192
|
+
subject.find("//*[contains(., 'hello')]").should_not be_empty
|
193
|
+
end
|
194
|
+
|
195
|
+
it "has an empty page after reseting" do
|
196
|
+
subject.reset!
|
197
|
+
subject.find("//*[contains(., 'hello')]").should be_empty
|
198
|
+
end
|
199
|
+
|
200
|
+
it "raises an error for an invalid xpath query" do
|
201
|
+
expect { subject.find("totally invalid salad") }.
|
202
|
+
to raise_error(Capybara::Driver::Webkit::WebkitInvalidResponseError, /xpath/i)
|
203
|
+
end
|
204
|
+
|
205
|
+
it "returns an attribute's value" do
|
206
|
+
subject.find("//p").first["id"].should == "greeting"
|
207
|
+
end
|
208
|
+
|
209
|
+
it "parses xpath with quotes" do
|
210
|
+
subject.find('//*[contains(., "hello")]').should_not be_empty
|
211
|
+
end
|
212
|
+
|
213
|
+
it "returns a node's text" do
|
214
|
+
subject.find("//p").first.text.should == "hello"
|
215
|
+
end
|
216
|
+
|
217
|
+
it "normalizes a node's text" do
|
218
|
+
subject.find("//div[contains(@class, 'normalize')]").first.text.should == "Spaces not normalized"
|
219
|
+
end
|
220
|
+
|
221
|
+
it "returns the current URL" do
|
222
|
+
port = subject.instance_variable_get("@rack_server").port
|
223
|
+
subject.current_url.should == "http://127.0.0.1:#{port}/hello/world?success=true"
|
224
|
+
end
|
225
|
+
|
226
|
+
it "escapes URLs" do
|
227
|
+
subject.visit("/hello there")
|
228
|
+
subject.current_url.should =~ /hello%20there/
|
229
|
+
end
|
230
|
+
|
231
|
+
it "visits a page with an anchor" do
|
232
|
+
subject.visit("/hello#display_none")
|
233
|
+
subject.current_url.should =~ /hello#display_none/
|
234
|
+
end
|
235
|
+
|
236
|
+
it "returns the source code for the page" do
|
237
|
+
subject.source.should =~ %r{<html>.*greeting.*}m
|
238
|
+
end
|
239
|
+
|
240
|
+
it "evaluates Javascript and returns a string" do
|
241
|
+
result = subject.evaluate_script(%<document.getElementById('greeting').innerText>)
|
242
|
+
result.should == "hello"
|
243
|
+
end
|
244
|
+
|
245
|
+
it "evaluates Javascript and returns an array" do
|
246
|
+
result = subject.evaluate_script(%<["hello", "world"]>)
|
247
|
+
result.should == %w(hello world)
|
248
|
+
end
|
249
|
+
|
250
|
+
it "evaluates Javascript and returns an int" do
|
251
|
+
result = subject.evaluate_script(%<123>)
|
252
|
+
result.should == 123
|
253
|
+
end
|
254
|
+
|
255
|
+
it "evaluates Javascript and returns a float" do
|
256
|
+
result = subject.evaluate_script(%<1.5>)
|
257
|
+
result.should == 1.5
|
258
|
+
end
|
259
|
+
|
260
|
+
it "evaluates Javascript and returns null" do
|
261
|
+
result = subject.evaluate_script(%<(function () {})()>)
|
262
|
+
result.should == nil
|
263
|
+
end
|
264
|
+
|
265
|
+
it "evaluates Javascript and returns an object" do
|
266
|
+
result = subject.evaluate_script(%<({ 'one' : 1 })>)
|
267
|
+
result.should == { 'one' => 1 }
|
268
|
+
end
|
269
|
+
|
270
|
+
it "evaluates Javascript and returns true" do
|
271
|
+
result = subject.evaluate_script(%<true>)
|
272
|
+
result.should === true
|
273
|
+
end
|
274
|
+
|
275
|
+
it "evaluates Javascript and returns false" do
|
276
|
+
result = subject.evaluate_script(%<false>)
|
277
|
+
result.should === false
|
278
|
+
end
|
279
|
+
|
280
|
+
it "evaluates Javascript and returns an escaped string" do
|
281
|
+
result = subject.evaluate_script(%<'"'>)
|
282
|
+
result.should === "\""
|
283
|
+
end
|
284
|
+
|
285
|
+
it "evaluates Javascript with multiple lines" do
|
286
|
+
result = subject.evaluate_script("[1,\n2]")
|
287
|
+
result.should == [1, 2]
|
288
|
+
end
|
289
|
+
|
290
|
+
it "executes Javascript" do
|
291
|
+
subject.execute_script(%<document.getElementById('greeting').innerHTML = 'yo'>)
|
292
|
+
subject.find("//p[contains(., 'yo')]").should_not be_empty
|
293
|
+
end
|
294
|
+
|
295
|
+
it "raises an error for failing Javascript" do
|
296
|
+
expect { subject.execute_script(%<invalid salad>) }.
|
297
|
+
to raise_error(Capybara::Driver::Webkit::WebkitInvalidResponseError)
|
298
|
+
end
|
299
|
+
|
300
|
+
it "doesn't raise an error for Javascript that doesn't return anything" do
|
301
|
+
lambda { subject.execute_script(%<(function () { "returns nothing" })()>) }.
|
302
|
+
should_not raise_error
|
303
|
+
end
|
304
|
+
|
305
|
+
it "returns a node's tag name" do
|
306
|
+
subject.find("//p").first.tag_name.should == "p"
|
307
|
+
end
|
308
|
+
|
309
|
+
it "reads disabled property" do
|
310
|
+
subject.find("//input").first.should be_disabled
|
311
|
+
end
|
312
|
+
|
313
|
+
it "reads checked property" do
|
314
|
+
subject.find("//input[@id='checktest']").first.should be_checked
|
315
|
+
end
|
316
|
+
|
317
|
+
it "finds visible elements" do
|
318
|
+
subject.find("//p").first.should be_visible
|
319
|
+
subject.find("//*[@id='invisible']").first.should_not be_visible
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
context "form app" do
|
324
|
+
before(:all) do
|
325
|
+
@app = lambda do |env|
|
326
|
+
body = <<-HTML
|
327
|
+
<html><body>
|
328
|
+
<form action="/" method="GET">
|
329
|
+
<input type="text" name="foo" value="bar"/>
|
330
|
+
<input type="text" name="maxlength_foo" value="bar" maxlength="10"/>
|
331
|
+
<input type="text" id="disabled_input" disabled="disabled"/>
|
332
|
+
<input type="checkbox" name="checkedbox" value="1" checked="checked"/>
|
333
|
+
<input type="checkbox" name="uncheckedbox" value="2"/>
|
334
|
+
<select name="animal">
|
335
|
+
<option id="select-option-monkey">Monkey</option>
|
336
|
+
<option id="select-option-capybara" selected="selected">Capybara</option>
|
337
|
+
</select>
|
338
|
+
<select name="toppings" multiple="multiple">
|
339
|
+
<optgroup label="Mediocre Toppings">
|
340
|
+
<option selected="selected" id="topping-apple">Apple</option>
|
341
|
+
<option selected="selected" id="topping-banana">Banana</option>
|
342
|
+
</optgroup>
|
343
|
+
<optgroup label="Best Toppings">
|
344
|
+
<option selected="selected" id="topping-cherry">Cherry</option>
|
345
|
+
</optgroup>
|
346
|
+
</select>
|
347
|
+
<textarea id="only-textarea">what a wonderful area for text</textarea>
|
348
|
+
<input type="radio" id="only-radio" value="1"/>
|
349
|
+
</form>
|
350
|
+
</body></html>
|
351
|
+
HTML
|
352
|
+
[200,
|
353
|
+
{ 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
|
354
|
+
[body]]
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
it "returns a textarea's value" do
|
359
|
+
subject.find("//textarea").first.value.should == "what a wonderful area for text"
|
360
|
+
end
|
361
|
+
|
362
|
+
it "returns a text input's value" do
|
363
|
+
subject.find("//input").first.value.should == "bar"
|
364
|
+
end
|
365
|
+
|
366
|
+
it "returns a select's value" do
|
367
|
+
subject.find("//select").first.value.should == "Capybara"
|
368
|
+
end
|
369
|
+
|
370
|
+
it "sets an input's value" do
|
371
|
+
input = subject.find("//input").first
|
372
|
+
input.set("newvalue")
|
373
|
+
input.value.should == "newvalue"
|
374
|
+
end
|
375
|
+
|
376
|
+
it "sets an input's value greater than the max length" do
|
377
|
+
input = subject.find("//input[@name='maxlength_foo']").first
|
378
|
+
input.set("allegories (poems)")
|
379
|
+
input.value.should == "allegories"
|
380
|
+
end
|
381
|
+
|
382
|
+
it "sets an input's value equal to the max length" do
|
383
|
+
input = subject.find("//input[@name='maxlength_foo']").first
|
384
|
+
input.set("allegories")
|
385
|
+
input.value.should == "allegories"
|
386
|
+
end
|
387
|
+
|
388
|
+
it "sets an input's value less than the max length" do
|
389
|
+
input = subject.find("//input[@name='maxlength_foo']").first
|
390
|
+
input.set("poems")
|
391
|
+
input.value.should == "poems"
|
392
|
+
end
|
393
|
+
|
394
|
+
it "sets an input's nil value" do
|
395
|
+
input = subject.find("//input").first
|
396
|
+
input.set(nil)
|
397
|
+
input.value.should == ""
|
398
|
+
end
|
399
|
+
|
400
|
+
it "sets a select's value" do
|
401
|
+
select = subject.find("//select").first
|
402
|
+
select.set("Monkey")
|
403
|
+
select.value.should == "Monkey"
|
404
|
+
end
|
405
|
+
|
406
|
+
it "sets a textarea's value" do
|
407
|
+
textarea = subject.find("//textarea").first
|
408
|
+
textarea.set("newvalue")
|
409
|
+
textarea.value.should == "newvalue"
|
410
|
+
end
|
411
|
+
|
412
|
+
let(:monkey_option) { subject.find("//option[@id='select-option-monkey']").first }
|
413
|
+
let(:capybara_option) { subject.find("//option[@id='select-option-capybara']").first }
|
414
|
+
let(:animal_select) { subject.find("//select[@name='animal']").first }
|
415
|
+
let(:apple_option) { subject.find("//option[@id='topping-apple']").first }
|
416
|
+
let(:banana_option) { subject.find("//option[@id='topping-banana']").first }
|
417
|
+
let(:cherry_option) { subject.find("//option[@id='topping-cherry']").first }
|
418
|
+
let(:toppings_select) { subject.find("//select[@name='toppings']").first }
|
419
|
+
|
420
|
+
it "selects an option" do
|
421
|
+
animal_select.value.should == "Capybara"
|
422
|
+
monkey_option.select_option
|
423
|
+
animal_select.value.should == "Monkey"
|
424
|
+
end
|
425
|
+
|
426
|
+
it "unselects an option in a multi-select" do
|
427
|
+
toppings_select.value.should include("Apple", "Banana", "Cherry")
|
428
|
+
|
429
|
+
apple_option.unselect_option
|
430
|
+
toppings_select.value.should_not include("Apple")
|
431
|
+
end
|
432
|
+
|
433
|
+
it "reselects an option in a multi-select" do
|
434
|
+
apple_option.unselect_option
|
435
|
+
banana_option.unselect_option
|
436
|
+
cherry_option.unselect_option
|
437
|
+
|
438
|
+
toppings_select.value.should == []
|
439
|
+
|
440
|
+
apple_option.select_option
|
441
|
+
banana_option.select_option
|
442
|
+
cherry_option.select_option
|
443
|
+
|
444
|
+
toppings_select.value.should include("Apple", "Banana", "Cherry")
|
445
|
+
end
|
446
|
+
|
447
|
+
let(:checked_box) { subject.find("//input[@name='checkedbox']").first }
|
448
|
+
let(:unchecked_box) { subject.find("//input[@name='uncheckedbox']").first }
|
449
|
+
|
450
|
+
it "knows a checked box is checked" do
|
451
|
+
checked_box['checked'].should be_true
|
452
|
+
end
|
453
|
+
|
454
|
+
it "knows a checked box is checked using checked?" do
|
455
|
+
checked_box.should be_checked
|
456
|
+
end
|
457
|
+
|
458
|
+
it "knows an unchecked box is unchecked" do
|
459
|
+
unchecked_box['checked'].should_not be_true
|
460
|
+
end
|
461
|
+
|
462
|
+
it "knows an unchecked box is unchecked using checked?" do
|
463
|
+
unchecked_box.should_not be_checked
|
464
|
+
end
|
465
|
+
|
466
|
+
it "checks an unchecked box" do
|
467
|
+
unchecked_box.set(true)
|
468
|
+
unchecked_box.should be_checked
|
469
|
+
end
|
470
|
+
|
471
|
+
it "unchecks a checked box" do
|
472
|
+
checked_box.set(false)
|
473
|
+
checked_box.should_not be_checked
|
474
|
+
end
|
475
|
+
|
476
|
+
it "leaves a checked box checked" do
|
477
|
+
checked_box.set(true)
|
478
|
+
checked_box.should be_checked
|
479
|
+
end
|
480
|
+
|
481
|
+
it "leaves an unchecked box unchecked" do
|
482
|
+
unchecked_box.set(false)
|
483
|
+
unchecked_box.should_not be_checked
|
484
|
+
end
|
485
|
+
|
486
|
+
let(:enabled_input) { subject.find("//input[@name='foo']").first }
|
487
|
+
let(:disabled_input) { subject.find("//input[@id='disabled_input']").first }
|
488
|
+
|
489
|
+
it "knows a disabled input is disabled" do
|
490
|
+
disabled_input['disabled'].should be_true
|
491
|
+
end
|
492
|
+
|
493
|
+
it "knows a not disabled input is not disabled" do
|
494
|
+
enabled_input['disabled'].should_not be_true
|
495
|
+
end
|
496
|
+
end
|
497
|
+
|
498
|
+
context "form events app" do
|
499
|
+
before(:all) do
|
500
|
+
@app = lambda do |env|
|
501
|
+
body = <<-HTML
|
502
|
+
<html><body>
|
503
|
+
<form action="/" method="GET">
|
504
|
+
<input class="watch" type="text"/>
|
505
|
+
<input class="watch" type="password"/>
|
506
|
+
<textarea class="watch"></textarea>
|
507
|
+
<input class="watch" type="checkbox"/>
|
508
|
+
<input class="watch" type="radio"/>
|
509
|
+
</form>
|
510
|
+
<ul id="events"></ul>
|
511
|
+
<script type="text/javascript">
|
512
|
+
var events = document.getElementById("events");
|
513
|
+
var recordEvent = function (event) {
|
514
|
+
var element = document.createElement("li");
|
515
|
+
element.innerHTML = event.type;
|
516
|
+
events.appendChild(element);
|
517
|
+
};
|
518
|
+
|
519
|
+
var elements = document.getElementsByClassName("watch");
|
520
|
+
for (var i = 0; i < elements.length; i++) {
|
521
|
+
var element = elements[i];
|
522
|
+
element.addEventListener("focus", recordEvent);
|
523
|
+
element.addEventListener("keydown", recordEvent);
|
524
|
+
element.addEventListener("keypress", recordEvent);
|
525
|
+
element.addEventListener("keyup", recordEvent);
|
526
|
+
element.addEventListener("change", recordEvent);
|
527
|
+
element.addEventListener("blur", recordEvent);
|
528
|
+
element.addEventListener("click", recordEvent);
|
529
|
+
}
|
530
|
+
</script>
|
531
|
+
</body></html>
|
532
|
+
HTML
|
533
|
+
[200,
|
534
|
+
{ 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
|
535
|
+
[body]]
|
536
|
+
end
|
537
|
+
end
|
538
|
+
|
539
|
+
let(:newtext) { 'newvalue' }
|
540
|
+
|
541
|
+
let(:keyevents) do
|
542
|
+
(%w{focus} +
|
543
|
+
newtext.length.times.collect { %w{keydown keypress keyup} } +
|
544
|
+
%w{change blur}).flatten
|
545
|
+
end
|
546
|
+
|
547
|
+
it "triggers text input events" do
|
548
|
+
subject.find("//input[@type='text']").first.set(newtext)
|
549
|
+
subject.find("//li").map(&:text).should == keyevents
|
550
|
+
end
|
551
|
+
|
552
|
+
it "triggers textarea input events" do
|
553
|
+
subject.find("//textarea").first.set(newtext)
|
554
|
+
subject.find("//li").map(&:text).should == keyevents
|
555
|
+
end
|
556
|
+
|
557
|
+
it "triggers password input events" do
|
558
|
+
subject.find("//input[@type='password']").first.set(newtext)
|
559
|
+
subject.find("//li").map(&:text).should == keyevents
|
560
|
+
end
|
561
|
+
|
562
|
+
it "triggers radio input events" do
|
563
|
+
subject.find("//input[@type='radio']").first.set(true)
|
564
|
+
subject.find("//li").map(&:text).should == %w(click change)
|
565
|
+
end
|
566
|
+
|
567
|
+
it "triggers checkbox events" do
|
568
|
+
subject.find("//input[@type='checkbox']").first.set(true)
|
569
|
+
subject.find("//li").map(&:text).should == %w(click change)
|
570
|
+
end
|
571
|
+
end
|
572
|
+
|
573
|
+
context "mouse app" do
|
574
|
+
before(:all) do
|
575
|
+
@app =lambda do |env|
|
576
|
+
body = <<-HTML
|
577
|
+
<html><body>
|
578
|
+
<div id="change">Change me</div>
|
579
|
+
<div id="mouseup">Push me</div>
|
580
|
+
<div id="mousedown">Release me</div>
|
581
|
+
<form action="/" method="GET">
|
582
|
+
<select id="change_select" name="change_select">
|
583
|
+
<option value="1" id="option-1" selected="selected">one</option>
|
584
|
+
<option value="2" id="option-2">two</option>
|
585
|
+
</select>
|
586
|
+
</form>
|
587
|
+
<script type="text/javascript">
|
588
|
+
document.getElementById("change_select").
|
589
|
+
addEventListener("change", function () {
|
590
|
+
this.className = "triggered";
|
591
|
+
});
|
592
|
+
document.getElementById("change").
|
593
|
+
addEventListener("change", function () {
|
594
|
+
this.className = "triggered";
|
595
|
+
});
|
596
|
+
document.getElementById("mouseup").
|
597
|
+
addEventListener("mouseup", function () {
|
598
|
+
this.className = "triggered";
|
599
|
+
});
|
600
|
+
document.getElementById("mousedown").
|
601
|
+
addEventListener("mousedown", function () {
|
602
|
+
this.className = "triggered";
|
603
|
+
});
|
604
|
+
</script>
|
605
|
+
<a href="/next">Next</a>
|
606
|
+
</body></html>
|
607
|
+
HTML
|
608
|
+
[200,
|
609
|
+
{ 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
|
610
|
+
[body]]
|
611
|
+
end
|
612
|
+
end
|
613
|
+
|
614
|
+
it "clicks an element" do
|
615
|
+
subject.find("//a").first.click
|
616
|
+
subject.current_url =~ %r{/next$}
|
617
|
+
end
|
618
|
+
|
619
|
+
it "fires a mouse event" do
|
620
|
+
subject.find("//*[@id='mouseup']").first.trigger("mouseup")
|
621
|
+
subject.find("//*[@class='triggered']").should_not be_empty
|
622
|
+
end
|
623
|
+
|
624
|
+
it "fires a non-mouse event" do
|
625
|
+
subject.find("//*[@id='change']").first.trigger("change")
|
626
|
+
subject.find("//*[@class='triggered']").should_not be_empty
|
627
|
+
end
|
628
|
+
|
629
|
+
it "fires a change on select" do
|
630
|
+
select = subject.find("//select").first
|
631
|
+
select.value.should == "1"
|
632
|
+
option = subject.find("//option[@id='option-2']").first
|
633
|
+
option.select_option
|
634
|
+
select.value.should == "2"
|
635
|
+
subject.find("//select[@class='triggered']").should_not be_empty
|
636
|
+
end
|
637
|
+
|
638
|
+
it "fires drag events" do
|
639
|
+
draggable = subject.find("//*[@id='mousedown']").first
|
640
|
+
container = subject.find("//*[@id='mouseup']").first
|
641
|
+
|
642
|
+
draggable.drag_to(container)
|
643
|
+
|
644
|
+
subject.find("//*[@class='triggered']").size.should == 1
|
645
|
+
end
|
646
|
+
end
|
647
|
+
|
648
|
+
context "nesting app" do
|
649
|
+
before(:all) do
|
650
|
+
@app = lambda do |env|
|
651
|
+
body = <<-HTML
|
652
|
+
<html><body>
|
653
|
+
<div id="parent">
|
654
|
+
<div class="find">Expected</div>
|
655
|
+
</div>
|
656
|
+
<div class="find">Unexpected</div>
|
657
|
+
</body></html>
|
658
|
+
HTML
|
659
|
+
[200,
|
660
|
+
{ 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
|
661
|
+
[body]]
|
662
|
+
end
|
663
|
+
end
|
664
|
+
|
665
|
+
it "evaluates nested xpath expressions" do
|
666
|
+
parent = subject.find("//*[@id='parent']").first
|
667
|
+
parent.find("./*[@class='find']").map(&:text).should == %w(Expected)
|
668
|
+
end
|
669
|
+
end
|
670
|
+
|
671
|
+
context "slow app" do
|
672
|
+
before(:all) do
|
673
|
+
@app = lambda do |env|
|
674
|
+
body = <<-HTML
|
675
|
+
<html><body>
|
676
|
+
<form action="/next"><input type="submit"/></form>
|
677
|
+
<p>#{env['PATH_INFO']}</p>
|
678
|
+
</body></html>
|
679
|
+
HTML
|
680
|
+
sleep(0.5)
|
681
|
+
[200,
|
682
|
+
{ 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
|
683
|
+
[body]]
|
684
|
+
end
|
685
|
+
end
|
686
|
+
|
687
|
+
it "waits for a request to load" do
|
688
|
+
subject.find("//input").first.click
|
689
|
+
subject.find("//p").first.text.should == "/next"
|
690
|
+
end
|
691
|
+
end
|
692
|
+
|
693
|
+
context "error app" do
|
694
|
+
before(:all) do
|
695
|
+
@app = lambda do |env|
|
696
|
+
if env['PATH_INFO'] == "/error"
|
697
|
+
[404, {}, []]
|
698
|
+
else
|
699
|
+
body = <<-HTML
|
700
|
+
<html><body>
|
701
|
+
<form action="/error"><input type="submit"/></form>
|
702
|
+
</body></html>
|
703
|
+
HTML
|
704
|
+
[200,
|
705
|
+
{ 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
|
706
|
+
[body]]
|
707
|
+
end
|
708
|
+
end
|
709
|
+
end
|
710
|
+
|
711
|
+
it "raises a webkit error for the requested url" do
|
712
|
+
expect {
|
713
|
+
subject.find("//input").first.click
|
714
|
+
wait_for_error_to_complete
|
715
|
+
subject.find("//body")
|
716
|
+
}.
|
717
|
+
to raise_error(Capybara::Driver::Webkit::WebkitInvalidResponseError, %r{/error})
|
718
|
+
end
|
719
|
+
|
720
|
+
def wait_for_error_to_complete
|
721
|
+
sleep(0.5)
|
722
|
+
end
|
723
|
+
end
|
724
|
+
|
725
|
+
context "slow error app" do
|
726
|
+
before(:all) do
|
727
|
+
@app = lambda do |env|
|
728
|
+
if env['PATH_INFO'] == "/error"
|
729
|
+
body = "error"
|
730
|
+
sleep(1)
|
731
|
+
[304, {}, []]
|
732
|
+
else
|
733
|
+
body = <<-HTML
|
734
|
+
<html><body>
|
735
|
+
<form action="/error"><input type="submit"/></form>
|
736
|
+
<p>hello</p>
|
737
|
+
</body></html>
|
738
|
+
HTML
|
739
|
+
[200,
|
740
|
+
{ 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
|
741
|
+
[body]]
|
742
|
+
end
|
743
|
+
end
|
744
|
+
end
|
745
|
+
|
746
|
+
it "raises a webkit error and then continues" do
|
747
|
+
subject.find("//input").first.click
|
748
|
+
expect { subject.find("//p") }.to raise_error(Capybara::Driver::Webkit::WebkitInvalidResponseError)
|
749
|
+
subject.visit("/")
|
750
|
+
subject.find("//p").first.text.should == "hello"
|
751
|
+
end
|
752
|
+
end
|
753
|
+
|
754
|
+
context "popup app" do
|
755
|
+
before(:all) do
|
756
|
+
@app = lambda do |env|
|
757
|
+
body = <<-HTML
|
758
|
+
<html><body>
|
759
|
+
<script type="text/javascript">
|
760
|
+
alert("alert");
|
761
|
+
confirm("confirm");
|
762
|
+
prompt("prompt");
|
763
|
+
</script>
|
764
|
+
<p>success</p>
|
765
|
+
</body></html>
|
766
|
+
HTML
|
767
|
+
sleep(0.5)
|
768
|
+
[200,
|
769
|
+
{ 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
|
770
|
+
[body]]
|
771
|
+
end
|
772
|
+
end
|
773
|
+
|
774
|
+
it "doesn't crash from alerts" do
|
775
|
+
subject.find("//p").first.text.should == "success"
|
776
|
+
end
|
777
|
+
end
|
778
|
+
|
779
|
+
context "custom header" do
|
780
|
+
before(:all) do
|
781
|
+
@app = lambda do |env|
|
782
|
+
body = <<-HTML
|
783
|
+
<html><body>
|
784
|
+
<p id="user-agent">#{env['HTTP_USER_AGENT']}</p>
|
785
|
+
<p id="x-capybara-webkit-header">#{env['HTTP_X_CAPYBARA_WEBKIT_HEADER']}</p>
|
786
|
+
<p id="accept">#{env['HTTP_ACCEPT']}</p>
|
787
|
+
<a href="/">/</a>
|
788
|
+
</body></html>
|
789
|
+
HTML
|
790
|
+
[200,
|
791
|
+
{ 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
|
792
|
+
[body]]
|
793
|
+
end
|
794
|
+
end
|
795
|
+
|
796
|
+
before do
|
797
|
+
subject.header('user-agent', 'capybara-webkit/custom-user-agent')
|
798
|
+
subject.header('x-capybara-webkit-header', 'x-capybara-webkit-header')
|
799
|
+
subject.header('accept', 'text/html')
|
800
|
+
subject.visit('/')
|
801
|
+
end
|
802
|
+
|
803
|
+
it "can set user_agent" do
|
804
|
+
subject.find('id("user-agent")').first.text.should == 'capybara-webkit/custom-user-agent'
|
805
|
+
subject.evaluate_script('navigator.userAgent').should == 'capybara-webkit/custom-user-agent'
|
806
|
+
end
|
807
|
+
|
808
|
+
it "keep user_agent in next page" do
|
809
|
+
subject.find("//a").first.click
|
810
|
+
subject.find('id("user-agent")').first.text.should == 'capybara-webkit/custom-user-agent'
|
811
|
+
subject.evaluate_script('navigator.userAgent').should == 'capybara-webkit/custom-user-agent'
|
812
|
+
end
|
813
|
+
|
814
|
+
it "can set custom header" do
|
815
|
+
subject.find('id("x-capybara-webkit-header")').first.text.should == 'x-capybara-webkit-header'
|
816
|
+
end
|
817
|
+
|
818
|
+
it "can set Accept header" do
|
819
|
+
subject.find('id("accept")').first.text.should == 'text/html'
|
820
|
+
end
|
821
|
+
|
822
|
+
it "can reset all custom header" do
|
823
|
+
subject.reset!
|
824
|
+
subject.visit('/')
|
825
|
+
subject.find('id("user-agent")').first.text.should_not == 'capybara-webkit/custom-user-agent'
|
826
|
+
subject.evaluate_script('navigator.userAgent').should_not == 'capybara-webkit/custom-user-agent'
|
827
|
+
subject.find('id("x-capybara-webkit-header")').first.text.should be_empty
|
828
|
+
subject.find('id("accept")').first.text.should_not == 'text/html'
|
829
|
+
end
|
830
|
+
end
|
831
|
+
|
832
|
+
context "no response app" do
|
833
|
+
before(:all) do
|
834
|
+
@app = lambda do |env|
|
835
|
+
body = <<-HTML
|
836
|
+
<html><body>
|
837
|
+
<form action="/error"><input type="submit"/></form>
|
838
|
+
</body></html>
|
839
|
+
HTML
|
840
|
+
[200,
|
841
|
+
{ 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
|
842
|
+
[body]]
|
843
|
+
end
|
844
|
+
end
|
845
|
+
|
846
|
+
it "raises a webkit error for the requested url" do
|
847
|
+
make_the_server_go_away
|
848
|
+
expect {
|
849
|
+
subject.find("//body")
|
850
|
+
}.
|
851
|
+
to raise_error(Capybara::Driver::Webkit::WebkitNoResponseError, %r{response})
|
852
|
+
make_the_server_come_back
|
853
|
+
end
|
854
|
+
|
855
|
+
def make_the_server_come_back
|
856
|
+
subject.browser.instance_variable_get(:@socket).unstub!(:gets)
|
857
|
+
subject.browser.instance_variable_get(:@socket).unstub!(:puts)
|
858
|
+
subject.browser.instance_variable_get(:@socket).unstub!(:print)
|
859
|
+
end
|
860
|
+
|
861
|
+
def make_the_server_go_away
|
862
|
+
subject.browser.instance_variable_get(:@socket).stub!(:gets).and_return(nil)
|
863
|
+
subject.browser.instance_variable_get(:@socket).stub!(:puts)
|
864
|
+
subject.browser.instance_variable_get(:@socket).stub!(:print)
|
865
|
+
end
|
866
|
+
end
|
867
|
+
|
868
|
+
context "custom font app" do
|
869
|
+
before(:all) do
|
870
|
+
@app = lambda do |env|
|
871
|
+
body = <<-HTML
|
872
|
+
<html>
|
873
|
+
<head>
|
874
|
+
<style type="text/css">
|
875
|
+
p { font-family: "Verdana"; }
|
876
|
+
</style>
|
877
|
+
</head>
|
878
|
+
<body>
|
879
|
+
<p id="text">Hello</p>
|
880
|
+
</body>
|
881
|
+
</html>
|
882
|
+
HTML
|
883
|
+
[200,
|
884
|
+
{ 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
|
885
|
+
[body]]
|
886
|
+
end
|
887
|
+
end
|
888
|
+
|
889
|
+
it "ignores custom fonts" do
|
890
|
+
font_family = subject.evaluate_script(<<-SCRIPT)
|
891
|
+
var element = document.getElementById("text");
|
892
|
+
element.ownerDocument.defaultView.getComputedStyle(element, null).getPropertyValue("font-family");
|
893
|
+
SCRIPT
|
894
|
+
font_family.should == "Arial"
|
895
|
+
end
|
896
|
+
end
|
897
|
+
|
898
|
+
context "cookie-based app" do
|
899
|
+
before(:all) do
|
900
|
+
@cookie = 'cookie=abc; domain=127.0.0.1; path=/'
|
901
|
+
@app = lambda do |env|
|
902
|
+
request = ::Rack::Request.new(env)
|
903
|
+
|
904
|
+
body = <<-HTML
|
905
|
+
<html><body>
|
906
|
+
<p id="cookie">#{request.cookies["cookie"] || ""}</p>
|
907
|
+
</body></html>
|
908
|
+
HTML
|
909
|
+
[200,
|
910
|
+
{ 'Content-Type' => 'text/html; charset=UTF-8',
|
911
|
+
'Content-Length' => body.length.to_s,
|
912
|
+
'Set-Cookie' => @cookie,
|
913
|
+
},
|
914
|
+
[body]]
|
915
|
+
end
|
916
|
+
end
|
917
|
+
|
918
|
+
def echoed_cookie
|
919
|
+
subject.find('id("cookie")').first.text
|
920
|
+
end
|
921
|
+
|
922
|
+
it "remembers the cookie on second visit" do
|
923
|
+
echoed_cookie.should == ""
|
924
|
+
subject.visit "/"
|
925
|
+
echoed_cookie.should == "abc"
|
926
|
+
end
|
927
|
+
|
928
|
+
it "uses a custom cookie" do
|
929
|
+
subject.browser.set_cookie @cookie
|
930
|
+
subject.visit "/"
|
931
|
+
echoed_cookie.should == "abc"
|
932
|
+
end
|
933
|
+
|
934
|
+
it "clears cookies" do
|
935
|
+
subject.browser.clear_cookies
|
936
|
+
subject.visit "/"
|
937
|
+
echoed_cookie.should == ""
|
938
|
+
end
|
939
|
+
|
940
|
+
it "allows enumeration of cookies" do
|
941
|
+
cookies = subject.browser.get_cookies
|
942
|
+
|
943
|
+
cookies.size.should == 1
|
944
|
+
|
945
|
+
cookie = Hash[cookies[0].split(/\s*;\s*/).map { |x| x.split("=", 2) }]
|
946
|
+
cookie["cookie"].should == "abc"
|
947
|
+
cookie["domain"].should include "127.0.0.1"
|
948
|
+
cookie["path"].should == "/"
|
949
|
+
end
|
950
|
+
end
|
951
|
+
|
952
|
+
context "with socket debugger" do
|
953
|
+
let(:socket_debugger_class){ Capybara::Driver::Webkit::SocketDebugger }
|
954
|
+
let(:browser_with_debugger){
|
955
|
+
Capybara::Driver::Webkit::Browser.new(:socket_class => socket_debugger_class)
|
956
|
+
}
|
957
|
+
let(:driver_with_debugger){ Capybara::Driver::Webkit.new(@app, :browser => browser_with_debugger) }
|
958
|
+
|
959
|
+
before(:all) do
|
960
|
+
@app = lambda do |env|
|
961
|
+
body = <<-HTML
|
962
|
+
<html><body>
|
963
|
+
<div id="parent">
|
964
|
+
<div class="find">Expected</div>
|
965
|
+
</div>
|
966
|
+
<div class="find">Unexpected</div>
|
967
|
+
</body></html>
|
968
|
+
HTML
|
969
|
+
[200,
|
970
|
+
{ 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
|
971
|
+
[body]]
|
972
|
+
end
|
973
|
+
end
|
974
|
+
|
975
|
+
it "prints out sent content" do
|
976
|
+
socket_debugger_class.any_instance.stub(:received){|content| content }
|
977
|
+
sent_content = ['Find', 1, 17, "//*[@id='parent']"]
|
978
|
+
socket_debugger_class.any_instance.should_receive(:sent).exactly(sent_content.size).times
|
979
|
+
driver_with_debugger.find("//*[@id='parent']")
|
980
|
+
end
|
981
|
+
|
982
|
+
it "prints out received content" do
|
983
|
+
socket_debugger_class.any_instance.stub(:sent)
|
984
|
+
socket_debugger_class.any_instance.should_receive(:received).at_least(:once).and_return("ok")
|
985
|
+
driver_with_debugger.find("//*[@id='parent']")
|
986
|
+
end
|
987
|
+
end
|
988
|
+
|
989
|
+
context "remove node app" do
|
990
|
+
before(:all) do
|
991
|
+
@app = lambda do |env|
|
992
|
+
body = <<-HTML
|
993
|
+
<html>
|
994
|
+
<div id="parent">
|
995
|
+
<p id="removeMe">Hello</p>
|
996
|
+
</div>
|
997
|
+
</html>
|
998
|
+
HTML
|
999
|
+
[200,
|
1000
|
+
{ 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
|
1001
|
+
[body]]
|
1002
|
+
end
|
1003
|
+
end
|
1004
|
+
|
1005
|
+
before { set_automatic_reload false }
|
1006
|
+
after { set_automatic_reload true }
|
1007
|
+
|
1008
|
+
def set_automatic_reload(value)
|
1009
|
+
if Capybara.respond_to?(:automatic_reload)
|
1010
|
+
Capybara.automatic_reload = value
|
1011
|
+
end
|
1012
|
+
end
|
1013
|
+
|
1014
|
+
it "allows removed nodes when reloading is disabled" do
|
1015
|
+
node = subject.find("//p[@id='removeMe']").first
|
1016
|
+
subject.evaluate_script("document.getElementById('parent').innerHTML = 'Magic'")
|
1017
|
+
node.text.should == 'Hello'
|
1018
|
+
end
|
1019
|
+
end
|
1020
|
+
|
1021
|
+
context "javascript redirect app" do
|
1022
|
+
before(:all) do
|
1023
|
+
@app = lambda do |env|
|
1024
|
+
if env['PATH_INFO'] == '/redirect'
|
1025
|
+
body = <<-HTML
|
1026
|
+
<html>
|
1027
|
+
<script type="text/javascript">
|
1028
|
+
window.location = "/next";
|
1029
|
+
</script>
|
1030
|
+
</html>
|
1031
|
+
HTML
|
1032
|
+
else
|
1033
|
+
body = "<html><p>finished</p></html>"
|
1034
|
+
end
|
1035
|
+
[200,
|
1036
|
+
{ 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s },
|
1037
|
+
[body]]
|
1038
|
+
end
|
1039
|
+
end
|
1040
|
+
|
1041
|
+
it "loads a page without error" do
|
1042
|
+
10.times do
|
1043
|
+
subject.visit("/redirect")
|
1044
|
+
subject.find("//p").first.text.should == "finished"
|
1045
|
+
end
|
1046
|
+
end
|
1047
|
+
end
|
1048
|
+
end
|