Skip to content

Commit

Permalink
Adding view matchers from Merb
Browse files Browse the repository at this point in the history
  • Loading branch information
brynary committed Nov 6, 2008
1 parent 5d3cb35 commit f6d95d3
Show file tree
Hide file tree
Showing 3 changed files with 343 additions and 0 deletions.
1 change: 1 addition & 0 deletions lib/webrat/core.rb
Expand Up @@ -8,3 +8,4 @@
require "webrat/core/select_option"
require "webrat/core/session"
require "webrat/core/methods"
require "webrat/core/matchers"
234 changes: 234 additions & 0 deletions lib/webrat/core/matchers.rb
@@ -0,0 +1,234 @@
module Webrat
module Matchers

class HaveXpath
def initialize(expected, &block)
# Require nokogiri and fall back on rexml
begin
require "nokogiri"
rescue LoadError => e
if require "rexml/document"
require "merb-core/vendor/nokogiri/css"
warn("Standard REXML library is slow. Please consider installing nokogiri.\nUse \"sudo gem install nokogiri\"")
end
end

@expected = expected
@block = block
end

def matches?(stringlike)
if defined?(Nokogiri::XML)
matches_nokogiri?(stringlike)
else
matches_rexml?(stringlike)
end
end

def matches_rexml?(stringlike)
stringlike = stringlike.body.to_s if stringlike.respond_to?(:body)

@document = case stringlike
when REXML::Document
stringlike.root
when REXML::Node
stringlike
when StringIO, String
begin
REXML::Document.new(stringlike.to_s).root
rescue REXML::ParseException => e
if e.message.include?("second root element")
REXML::Document.new("<fake-root-element>#{stringlike}</fake-root-element>").root
else
raise e
end
end
end

query.all? do |q|
matched = REXML::XPath.match(@document, q)
matched.any? && (!block_given? || matched.all?(&@block))
end
end

def matches_nokogiri?(stringlike)
stringlike = stringlike.body.to_s if stringlike.respond_to?(:body)

@document = case stringlike
when Nokogiri::HTML::Document, Nokogiri::XML::NodeSet
stringlike
when StringIO
Nokogiri::HTML(stringlike.string)
else
Nokogiri::HTML(stringlike.to_s)
end
@document.xpath(*query).any?
end

def query
[@expected].flatten.compact
end

# ==== Returns
# String:: The failure message.
def failure_message
"expected following text to match xpath #{@expected}:\n#{@document}"
end

# ==== Returns
# String:: The failure message to be displayed in negative matches.
def negative_failure_message
"expected following text to not match xpath #{@expected}:\n#{@document}"
end
end

class HaveSelector < HaveXpath

# ==== Returns
# String:: The failure message.
def failure_message
"expected following text to match selector #{@expected}:\n#{@document}"
end

# ==== Returns
# String:: The failure message to be displayed in negative matches.
def negative_failure_message
"expected following text to not match selector #{@expected}:\n#{@document}"
end

def query
Nokogiri::CSS::Parser.parse(*super).map { |ast| ast.to_xpath }
end

end

class HaveTag < HaveSelector

# ==== Returns
# String:: The failure message.
def failure_message
"expected following output to contain a #{tag_inspect} tag:\n#{@document}"
end

# ==== Returns
# String:: The failure message to be displayed in negative matches.
def negative_failure_message
"expected following output to omit a #{tag_inspect}:\n#{@document}"
end

def tag_inspect
options = @expected.last.dup
content = options.delete(:content)

html = "<#{@expected.first}"
options.each do |k,v|
html << " #{k}='#{v}'"
end

if content
html << ">#{content}</#{@expected.first}>"
else
html << "/>"
end

html
end

def query
options = @expected.last.dup
selector = @expected.first.to_s

selector << ":contains('#{options.delete(:content)}')" if options[:content]

options.each do |key, value|
selector << "[#{key}='#{value}']"
end

Nokogiri::CSS::Parser.parse(selector).map { |ast| ast.to_xpath }
end

end

class HasContent
def initialize(content)
@content = content
end

def matches?(element)
element = element.body.to_s if element.respond_to?(:body)
@element = element

case @content
when String
@element.contains?(@content)
when Regexp
@element.matches?(@content)
end
end

# ==== Returns
# String:: The failure message.
def failure_message
"expected the following element's content to #{content_message}:\n#{@element.inner_text}"
end

# ==== Returns
# String:: The failure message to be displayed in negative matches.
def negative_failure_message
"expected the following element's content to not #{content_message}:\n#{@element.inner_text}"
end

def content_message
case @content
when String
"include \"#{@content}\""
when Regexp
"match #{@content.inspect}"
end
end
end

# Matches HTML content against a CSS 3 selector.
#
# ==== Parameters
# expected<String>:: The CSS selector to look for.
#
# ==== Returns
# HaveSelector:: A new have selector matcher.
# ---
# @api public
def have_selector(expected)
HaveSelector.new(expected)
end
alias_method :match_selector, :have_selector

# Matches HTML content against an XPath query
#
# ==== Parameters
# expected<String>:: The XPath query to look for.
#
# ==== Returns
# HaveXpath:: A new have xpath matcher.
# ---
# @api public
def have_xpath(expected)
HaveXpath.new(expected)
end
alias_method :match_xpath, :have_xpath

def have_tag(name, attributes = {})
HaveTag.new([name, attributes])
end
alias_method :match_tag, :have_tag

# Matches the contents of an HTML document with
# whatever string is supplied
#
# ---
# @api public
def contain(content)
HasContent.new(content)
end

end
end
108 changes: 108 additions & 0 deletions spec/api/matchers_spec.rb
@@ -0,0 +1,108 @@
require File.expand_path(File.dirname(__FILE__) + "/../spec_helper")

describe Webrat::Matchers do
include Webrat::Matchers

before(:each) do
@body = <<-EOF
<div id='main'>
<div class='inner'>hello, world!</div>
</div>
EOF
end

describe "#have_selector" do

it "should be able to match a CSS selector" do
@body.should have_selector("div")
end

it "should not match a CSS selector that does not exist" do
@body.should_not have_selector("p")
end

it "should be able to loop over all the matched elements" do
@body.should have_selector("div") { |node| node.name.should == "div" }
end

it "should not match of any of the matchers in the block fail" do
lambda {
@body.should_not have_selector("div") { |node| node.name.should == "p" }
}.should raise_error(Spec::Expectations::ExpectationNotMetError)
end

end

describe "#have_tag" do

it "should be able to match a tag" do
@body.should have_tag("div")
end

it "should not match the tag when it should not match" do
@body.should_not have_tag("p")
end

it "should be able to specify the content of the tag" do
@body.should have_tag("div", :content => "hello, world!")
end

it "should be able to specify the attributes of the tag" do
@body.should have_tag("div", :class => "inner")
end

end

describe Webrat::Matchers::HasContent do
include Webrat::Matchers

before(:each) do
@element = stub(:element)
@element.stub!(:inner_text).and_return <<-EOF
<div id='main'>
<div class='inner'>hello, world!</div>
</div>
EOF

@element.stub!(:contains?)
@element.stub!(:matches?)
end

describe "#matches?" do
it "should call element#contains? when the argument is a string" do
@element.should_receive(:contains?)

Webrat::Matchers::HasContent.new("hello, world!").matches?(@element)
end

it "should call element#matches? when the argument is a regular expression" do
@element.should_receive(:matches?)

Webrat::Matchers::HasContent.new(/hello, world/).matches?(@element)
end
end

describe "#failure_message" do
it "should include the content string" do
hc = Webrat::Matchers::HasContent.new("hello, world!")
hc.matches?(@element)

hc.failure_message.should include("\"hello, world!\"")
end

it "should include the content regular expresson" do
hc = Webrat::Matchers::HasContent.new(/hello,\sworld!/)
hc.matches?(@element)

hc.failure_message.should include("/hello,\\sworld!/")
end

it "should include the element's inner content" do
hc = Webrat::Matchers::HasContent.new(/hello,\sworld!/)
hc.matches?(@element)

hc.failure_message.should include(@element.inner_text)
end
end
end
end

0 comments on commit f6d95d3

Please sign in to comment.