Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
343 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |