casebook-webrat 0.4.4.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/History.txt +332 -0
- data/MIT-LICENSE.txt +19 -0
- data/README.rdoc +85 -0
- data/Rakefile +156 -0
- data/install.rb +1 -0
- data/lib/webrat.rb +34 -0
- data/lib/webrat/core.rb +14 -0
- data/lib/webrat/core/configuration.rb +104 -0
- data/lib/webrat/core/elements/area.rb +31 -0
- data/lib/webrat/core/elements/element.rb +33 -0
- data/lib/webrat/core/elements/field.rb +403 -0
- data/lib/webrat/core/elements/form.rb +103 -0
- data/lib/webrat/core/elements/label.rb +31 -0
- data/lib/webrat/core/elements/link.rb +92 -0
- data/lib/webrat/core/elements/select_option.rb +35 -0
- data/lib/webrat/core/locators.rb +20 -0
- data/lib/webrat/core/locators/area_locator.rb +38 -0
- data/lib/webrat/core/locators/button_locator.rb +54 -0
- data/lib/webrat/core/locators/field_by_id_locator.rb +37 -0
- data/lib/webrat/core/locators/field_labeled_locator.rb +56 -0
- data/lib/webrat/core/locators/field_locator.rb +25 -0
- data/lib/webrat/core/locators/field_named_locator.rb +41 -0
- data/lib/webrat/core/locators/form_locator.rb +19 -0
- data/lib/webrat/core/locators/label_locator.rb +34 -0
- data/lib/webrat/core/locators/link_locator.rb +66 -0
- data/lib/webrat/core/locators/locator.rb +20 -0
- data/lib/webrat/core/locators/select_option_locator.rb +59 -0
- data/lib/webrat/core/logging.rb +21 -0
- data/lib/webrat/core/matchers.rb +4 -0
- data/lib/webrat/core/matchers/have_content.rb +73 -0
- data/lib/webrat/core/matchers/have_selector.rb +74 -0
- data/lib/webrat/core/matchers/have_tag.rb +21 -0
- data/lib/webrat/core/matchers/have_xpath.rb +147 -0
- data/lib/webrat/core/methods.rb +61 -0
- data/lib/webrat/core/mime.rb +29 -0
- data/lib/webrat/core/save_and_open_page.rb +50 -0
- data/lib/webrat/core/scope.rb +350 -0
- data/lib/webrat/core/session.rb +281 -0
- data/lib/webrat/core/xml.rb +115 -0
- data/lib/webrat/core/xml/hpricot.rb +19 -0
- data/lib/webrat/core/xml/nokogiri.rb +76 -0
- data/lib/webrat/core/xml/rexml.rb +24 -0
- data/lib/webrat/core_extensions/blank.rb +58 -0
- data/lib/webrat/core_extensions/deprecate.rb +8 -0
- data/lib/webrat/core_extensions/detect_mapped.rb +12 -0
- data/lib/webrat/core_extensions/meta_class.rb +6 -0
- data/lib/webrat/core_extensions/nil_to_param.rb +5 -0
- data/lib/webrat/core_extensions/tcp_socket.rb +27 -0
- data/lib/webrat/mechanize.rb +74 -0
- data/lib/webrat/merb.rb +9 -0
- data/lib/webrat/merb_session.rb +65 -0
- data/lib/webrat/rack.rb +24 -0
- data/lib/webrat/rails.rb +105 -0
- data/lib/webrat/rspec-rails.rb +13 -0
- data/lib/webrat/selenium.rb +80 -0
- data/lib/webrat/selenium/application_server.rb +71 -0
- data/lib/webrat/selenium/location_strategy_javascript/button.js +12 -0
- data/lib/webrat/selenium/location_strategy_javascript/label.js +16 -0
- data/lib/webrat/selenium/location_strategy_javascript/webrat.js +5 -0
- data/lib/webrat/selenium/location_strategy_javascript/webratlink.js +9 -0
- data/lib/webrat/selenium/location_strategy_javascript/webratlinkwithin.js +15 -0
- data/lib/webrat/selenium/location_strategy_javascript/webratselectwithoption.js +5 -0
- data/lib/webrat/selenium/matchers.rb +4 -0
- data/lib/webrat/selenium/matchers/have_content.rb +66 -0
- data/lib/webrat/selenium/matchers/have_selector.rb +49 -0
- data/lib/webrat/selenium/matchers/have_tag.rb +72 -0
- data/lib/webrat/selenium/matchers/have_xpath.rb +45 -0
- data/lib/webrat/selenium/merb_application_server.rb +48 -0
- data/lib/webrat/selenium/rails_application_server.rb +42 -0
- data/lib/webrat/selenium/selenium_extensions.js +6 -0
- data/lib/webrat/selenium/selenium_rc_server.rb +81 -0
- data/lib/webrat/selenium/selenium_session.rb +241 -0
- data/lib/webrat/selenium/sinatra_application_server.rb +35 -0
- data/lib/webrat/sinatra.rb +44 -0
- data/vendor/selenium-server.jar +0 -0
- metadata +147 -0
@@ -0,0 +1,80 @@
|
|
1
|
+
require "webrat"
|
2
|
+
gem "selenium-client", ">=1.2.14"
|
3
|
+
require "selenium/client"
|
4
|
+
require "webrat/selenium/selenium_session"
|
5
|
+
require "webrat/selenium/matchers"
|
6
|
+
require "webrat/core_extensions/tcp_socket"
|
7
|
+
|
8
|
+
module Webrat
|
9
|
+
# To use Webrat's Selenium support, you'll need the selenium-client gem installed.
|
10
|
+
# Activate it with (for example, in your <tt>env.rb</tt>):
|
11
|
+
#
|
12
|
+
# require "webrat"
|
13
|
+
#
|
14
|
+
# Webrat.configure do |config|
|
15
|
+
# config.mode = :selenium
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# == Dropping down to the selenium-client API
|
19
|
+
#
|
20
|
+
# If you ever need to do something with Selenium not provided in the Webrat API,
|
21
|
+
# you can always drop down to the selenium-client API using the <tt>selenium</tt> method.
|
22
|
+
# For example:
|
23
|
+
#
|
24
|
+
# When "I drag the photo to the left" do
|
25
|
+
# selenium.dragdrop("id=photo_123", "+350, 0")
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# == Choosing the underlying framework to test
|
29
|
+
#
|
30
|
+
# Webrat assumes you're using rails by default but it can also work with sinatra
|
31
|
+
# and merb. To take advantage of this you can use the configuration block to
|
32
|
+
# set the application_framework variable.
|
33
|
+
# require "webrat"
|
34
|
+
#
|
35
|
+
# Webrat.configure do |config|
|
36
|
+
# config.mode = :selenium
|
37
|
+
# config.application_port = 4567
|
38
|
+
# config.application_framework = :sinatra # could also be :merb
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# == Auto-starting of the appserver and java server
|
42
|
+
#
|
43
|
+
# Webrat will automatically start the Selenium Java server process and an instance
|
44
|
+
# of Mongrel when a test is run. The Mongrel will run in the "selenium" environment
|
45
|
+
# instead of "test", so ensure you've got that defined, and will run on port
|
46
|
+
# Webrat.configuration.application_port.
|
47
|
+
#
|
48
|
+
# == Waiting
|
49
|
+
#
|
50
|
+
# In order to make writing Selenium tests as easy as possible, Webrat will automatically
|
51
|
+
# wait for the correct elements to exist on the page when trying to manipulate them
|
52
|
+
# with methods like <tt>fill_in</tt>, etc. In general, this means you should be able to write
|
53
|
+
# your Webrat::Selenium tests ignoring the concurrency issues that can plague in-browser
|
54
|
+
# testing, so long as you're using the Webrat API.
|
55
|
+
module Selenium
|
56
|
+
module Methods
|
57
|
+
def response
|
58
|
+
webrat_session.response
|
59
|
+
end
|
60
|
+
|
61
|
+
def wait_for(*args, &block)
|
62
|
+
webrat_session.wait_for(*args, &block)
|
63
|
+
end
|
64
|
+
|
65
|
+
def save_and_open_screengrab
|
66
|
+
webrat_session.save_and_open_screengrab
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
if defined?(ActionController::IntegrationTest)
|
73
|
+
module ActionController #:nodoc:
|
74
|
+
IntegrationTest.class_eval do
|
75
|
+
include Webrat::Methods
|
76
|
+
include Webrat::Selenium::Methods
|
77
|
+
include Webrat::Selenium::Matchers
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Webrat
|
2
|
+
module Selenium
|
3
|
+
|
4
|
+
class ApplicationServer
|
5
|
+
|
6
|
+
def self.boot
|
7
|
+
case Webrat.configuration.application_framework
|
8
|
+
when :sinatra
|
9
|
+
require "webrat/selenium/sinatra_application_server"
|
10
|
+
SinatraApplicationServer.new.boot
|
11
|
+
when :merb
|
12
|
+
require "webrat/selenium/merb_application_server"
|
13
|
+
MerbApplicationServer.new.boot
|
14
|
+
when :rails
|
15
|
+
require "webrat/selenium/rails_application_server"
|
16
|
+
RailsApplicationServer.new.boot
|
17
|
+
else
|
18
|
+
raise WebratError.new(<<-STR)
|
19
|
+
Unknown Webrat application_framework: #{Webrat.configuration.application_framework.inspect}
|
20
|
+
|
21
|
+
Please ensure you have a Webrat configuration block that specifies an application_framework
|
22
|
+
in your test_helper.rb, spec_helper.rb, or env.rb (for Cucumber).
|
23
|
+
|
24
|
+
For example:
|
25
|
+
|
26
|
+
Webrat.configure do |config|
|
27
|
+
# ...
|
28
|
+
config.application_framework = :rails
|
29
|
+
end
|
30
|
+
STR
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def boot
|
35
|
+
start
|
36
|
+
wait
|
37
|
+
stop_at_exit
|
38
|
+
end
|
39
|
+
|
40
|
+
def stop_at_exit
|
41
|
+
at_exit do
|
42
|
+
stop
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def wait
|
47
|
+
$stderr.print "==> Waiting for #{Webrat.configuration.application_framework} application server on port #{Webrat.configuration.application_port}... "
|
48
|
+
wait_for_socket
|
49
|
+
$stderr.print "Ready!\n"
|
50
|
+
end
|
51
|
+
|
52
|
+
def wait_for_socket
|
53
|
+
silence_stream(STDOUT) do
|
54
|
+
TCPSocket.wait_for_service_with_timeout \
|
55
|
+
:host => Webrat.configuration.application_address,
|
56
|
+
:port => Webrat.configuration.application_port.to_i,
|
57
|
+
:timeout => 30 # seconds
|
58
|
+
end
|
59
|
+
rescue SocketError
|
60
|
+
fail
|
61
|
+
end
|
62
|
+
|
63
|
+
def prepare_pid_file(file_path, pid_file_name)
|
64
|
+
FileUtils.mkdir_p File.expand_path(file_path)
|
65
|
+
File.expand_path("#{file_path}/#{pid_file_name}")
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
if (locator == '*') {
|
2
|
+
return selenium.browserbot.locationStrategies['xpath'].call(this, "//input[@type='submit']", inDocument, inWindow)
|
3
|
+
}
|
4
|
+
var inputs = inDocument.getElementsByTagName('input');
|
5
|
+
return $A(inputs).find(function(candidate){
|
6
|
+
inputType = candidate.getAttribute('type');
|
7
|
+
if (inputType == 'submit' || inputType == 'image') {
|
8
|
+
var buttonText = $F(candidate);
|
9
|
+
return (PatternMatcher.matches(locator, buttonText));
|
10
|
+
}
|
11
|
+
return false;
|
12
|
+
});
|
@@ -0,0 +1,16 @@
|
|
1
|
+
var allLabels = inDocument.getElementsByTagName("label");
|
2
|
+
var candidateLabels = $A(allLabels).select(function(candidateLabel){
|
3
|
+
var regExp = new RegExp('^' + locator + '\\b', 'i');
|
4
|
+
var labelText = getText(candidateLabel).strip();
|
5
|
+
return (labelText.search(regExp) >= 0);
|
6
|
+
});
|
7
|
+
if (candidateLabels.length == 0) {
|
8
|
+
return null;
|
9
|
+
}
|
10
|
+
candidateLabels = candidateLabels.sortBy(function(s) { return s.length * -1; }); //reverse length sort
|
11
|
+
var locatedLabel = candidateLabels.first();
|
12
|
+
var labelFor = locatedLabel.getAttribute('for');
|
13
|
+
if ((labelFor == null) && (locatedLabel.hasChildNodes())) {
|
14
|
+
return locatedLabel.firstChild; //TODO: should find the first form field, not just any node
|
15
|
+
}
|
16
|
+
return selenium.browserbot.locationStrategies['id'].call(this, labelFor, inDocument, inWindow);
|
@@ -0,0 +1,5 @@
|
|
1
|
+
var locationStrategies = selenium.browserbot.locationStrategies;
|
2
|
+
return locationStrategies['id'].call(this, locator, inDocument, inWindow)
|
3
|
+
|| locationStrategies['name'].call(this, locator, inDocument, inWindow)
|
4
|
+
|| locationStrategies['label'].call(this, locator, inDocument, inWindow)
|
5
|
+
|| null;
|
@@ -0,0 +1,9 @@
|
|
1
|
+
var links = inDocument.getElementsByTagName('a');
|
2
|
+
var candidateLinks = $A(links).select(function(candidateLink) {
|
3
|
+
return PatternMatcher.matches(locator, getText(candidateLink));
|
4
|
+
});
|
5
|
+
if (candidateLinks.length == 0) {
|
6
|
+
return null;
|
7
|
+
}
|
8
|
+
candidateLinks = candidateLinks.sortBy(function(s) { return s.length * -1; }); //reverse length sort
|
9
|
+
return candidateLinks.first();
|
@@ -0,0 +1,15 @@
|
|
1
|
+
var locatorParts = locator.split('|');
|
2
|
+
var cssAncestor = locatorParts[0];
|
3
|
+
var linkText = locatorParts[1];
|
4
|
+
var matchingElements = cssQuery(cssAncestor, inDocument);
|
5
|
+
var candidateLinks = matchingElements.collect(function(ancestor){
|
6
|
+
var links = ancestor.getElementsByTagName('a');
|
7
|
+
return $A(links).select(function(candidateLink) {
|
8
|
+
return PatternMatcher.matches(linkText, getText(candidateLink));
|
9
|
+
});
|
10
|
+
}).flatten().compact();
|
11
|
+
if (candidateLinks.length == 0) {
|
12
|
+
return null;
|
13
|
+
}
|
14
|
+
candidateLinks = candidateLinks.sortBy(function(s) { return s.length * -1; }); //reverse length sort
|
15
|
+
return candidateLinks.first();
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Webrat
|
2
|
+
module Selenium
|
3
|
+
module Matchers
|
4
|
+
class HasContent #:nodoc:
|
5
|
+
def initialize(content)
|
6
|
+
@content = content
|
7
|
+
end
|
8
|
+
|
9
|
+
def matches?(response)
|
10
|
+
if @content.is_a?(Regexp)
|
11
|
+
text_finder = "regexp:#{@content.source}"
|
12
|
+
else
|
13
|
+
text_finder = @content
|
14
|
+
end
|
15
|
+
|
16
|
+
response.session.wait_for do
|
17
|
+
response.selenium.is_text_present(text_finder)
|
18
|
+
end
|
19
|
+
rescue Webrat::TimeoutError
|
20
|
+
false
|
21
|
+
end
|
22
|
+
|
23
|
+
# ==== Returns
|
24
|
+
# String:: The failure message.
|
25
|
+
def failure_message
|
26
|
+
"expected the following element's content to #{content_message}:\n#{@element}"
|
27
|
+
end
|
28
|
+
|
29
|
+
# ==== Returns
|
30
|
+
# String:: The failure message to be displayed in negative matches.
|
31
|
+
def negative_failure_message
|
32
|
+
"expected the following element's content to not #{content_message}:\n#{@element}"
|
33
|
+
end
|
34
|
+
|
35
|
+
def content_message
|
36
|
+
case @content
|
37
|
+
when String
|
38
|
+
"include \"#{@content}\""
|
39
|
+
when Regexp
|
40
|
+
"match #{@content.inspect}"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Matches the contents of an HTML document with
|
46
|
+
# whatever string is supplied
|
47
|
+
def contain(content)
|
48
|
+
HasContent.new(content)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Asserts that the body of the response contain
|
52
|
+
# the supplied string or regexp
|
53
|
+
def assert_contain(content)
|
54
|
+
hc = HasContent.new(content)
|
55
|
+
assert hc.matches?(response), hc.failure_message
|
56
|
+
end
|
57
|
+
|
58
|
+
# Asserts that the body of the response
|
59
|
+
# does not contain the supplied string or regepx
|
60
|
+
def assert_not_contain(content)
|
61
|
+
hc = HasContent.new(content)
|
62
|
+
assert !hc.matches?(response), hc.negative_failure_message
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Webrat
|
2
|
+
module Selenium
|
3
|
+
module Matchers
|
4
|
+
class HaveSelector
|
5
|
+
def initialize(expected)
|
6
|
+
@expected = expected
|
7
|
+
end
|
8
|
+
|
9
|
+
def matches?(response)
|
10
|
+
response.session.wait_for do
|
11
|
+
response.selenium.is_element_present("css=#{@expected}")
|
12
|
+
end
|
13
|
+
rescue Webrat::TimeoutError
|
14
|
+
false
|
15
|
+
end
|
16
|
+
|
17
|
+
# ==== Returns
|
18
|
+
# String:: The failure message.
|
19
|
+
def failure_message
|
20
|
+
"expected following text to match selector #{@expected}:\n#{@document}"
|
21
|
+
end
|
22
|
+
|
23
|
+
# ==== Returns
|
24
|
+
# String:: The failure message to be displayed in negative matches.
|
25
|
+
def negative_failure_message
|
26
|
+
"expected following text to not match selector #{@expected}:\n#{@document}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def have_selector(content)
|
31
|
+
HaveSelector.new(content)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Asserts that the body of the response contains
|
35
|
+
# the supplied selector
|
36
|
+
def assert_have_selector(expected)
|
37
|
+
hs = HaveSelector.new(expected)
|
38
|
+
assert hs.matches?(response), hs.failure_message
|
39
|
+
end
|
40
|
+
|
41
|
+
# Asserts that the body of the response
|
42
|
+
# does not contain the supplied string or regepx
|
43
|
+
def assert_have_no_selector(expected)
|
44
|
+
hs = HaveSelector.new(expected)
|
45
|
+
assert !hs.matches?(response), hs.negative_failure_message
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Webrat
|
2
|
+
module Selenium
|
3
|
+
module Matchers
|
4
|
+
|
5
|
+
class HaveTag < HaveSelector #:nodoc:
|
6
|
+
# ==== Returns
|
7
|
+
# String:: The failure message.
|
8
|
+
def failure_message
|
9
|
+
"expected following output to contain a #{tag_inspect} tag:\n#{@document}"
|
10
|
+
end
|
11
|
+
|
12
|
+
# ==== Returns
|
13
|
+
# String:: The failure message to be displayed in negative matches.
|
14
|
+
def negative_failure_message
|
15
|
+
"expected following output to omit a #{tag_inspect}:\n#{@document}"
|
16
|
+
end
|
17
|
+
|
18
|
+
def tag_inspect
|
19
|
+
options = @expected.last.dup
|
20
|
+
content = options.delete(:content)
|
21
|
+
|
22
|
+
html = "<#{@expected.first}"
|
23
|
+
options.each do |k,v|
|
24
|
+
html << " #{k}='#{v}'"
|
25
|
+
end
|
26
|
+
|
27
|
+
if content
|
28
|
+
html << ">#{content}</#{@expected.first}>"
|
29
|
+
else
|
30
|
+
html << "/>"
|
31
|
+
end
|
32
|
+
|
33
|
+
html
|
34
|
+
end
|
35
|
+
|
36
|
+
def query
|
37
|
+
options = @expected.last.dup
|
38
|
+
selector = @expected.first.to_s
|
39
|
+
|
40
|
+
selector << ":contains('#{options.delete(:content)}')" if options[:content]
|
41
|
+
|
42
|
+
options.each do |key, value|
|
43
|
+
selector << "[#{key}='#{value}']"
|
44
|
+
end
|
45
|
+
|
46
|
+
Nokogiri::CSS.parse(selector).map { |ast| ast.to_xpath }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def have_tag(name, attributes = {}, &block)
|
51
|
+
HaveTag.new([name, attributes], &block)
|
52
|
+
end
|
53
|
+
|
54
|
+
alias_method :match_tag, :have_tag
|
55
|
+
|
56
|
+
# Asserts that the body of the response contains
|
57
|
+
# the supplied tag with the associated selectors
|
58
|
+
def assert_have_tag(name, attributes = {})
|
59
|
+
ht = HaveTag.new([name, attributes])
|
60
|
+
assert ht.matches?(response), ht.failure_message
|
61
|
+
end
|
62
|
+
|
63
|
+
# Asserts that the body of the response
|
64
|
+
# does not contain the supplied string or regepx
|
65
|
+
def assert_have_no_tag(name, attributes = {})
|
66
|
+
ht = HaveTag.new([name, attributes])
|
67
|
+
assert !ht.matches?(response), ht.negative_failure_message
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Webrat
|
2
|
+
module Selenium
|
3
|
+
module Matchers
|
4
|
+
class HaveXpath
|
5
|
+
def initialize(expected)
|
6
|
+
@expected = expected
|
7
|
+
end
|
8
|
+
|
9
|
+
def matches?(response)
|
10
|
+
response.session.wait_for do
|
11
|
+
response.selenium.is_element_present("xpath=#{@expected}")
|
12
|
+
end
|
13
|
+
rescue Webrat::TimeoutError
|
14
|
+
false
|
15
|
+
end
|
16
|
+
|
17
|
+
# ==== Returns
|
18
|
+
# String:: The failure message.
|
19
|
+
def failure_message
|
20
|
+
"expected following text to match xpath #{@expected}:\n#{@document}"
|
21
|
+
end
|
22
|
+
|
23
|
+
# ==== Returns
|
24
|
+
# String:: The failure message to be displayed in negative matches.
|
25
|
+
def negative_failure_message
|
26
|
+
"expected following text to not match xpath #{@expected}:\n#{@document}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def have_xpath(xpath)
|
31
|
+
HaveXpath.new(xpath)
|
32
|
+
end
|
33
|
+
|
34
|
+
def assert_have_xpath(expected)
|
35
|
+
hs = HaveXpath.new(expected)
|
36
|
+
assert hs.matches?(response), hs.failure_message
|
37
|
+
end
|
38
|
+
|
39
|
+
def assert_have_no_xpath(expected)
|
40
|
+
hs = HaveXpath.new(expected)
|
41
|
+
assert !hs.matches?(response), hs.negative_failure_message
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|