module Merb::Test::Rspec::ViewMatchers
class HaveXpath
def initialize(expected, &block)
# Require nokogiri and fall back on rexml
begin
require "nokogiri"
require "merb-core/test/test_ext/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)
@query = query
@document = case stringlike
when REXML::Document
stringlike.root
when REXML::Node, Array
@query = query.map { |q| q.gsub(%r'//', './') }
stringlike
else
begin
REXML::Document.new(stringlike.to_s).root
rescue REXML::ParseException => e
if e.message.include?("second root element")
REXML::Document.new("#{stringlike}").root
else
raise e
end
end
end
matched = @query.map do |q|
if @document.is_a?(Array)
@document.map { |d| REXML::XPath.match(d, q) }
else
REXML::XPath.match(@document, q)
end
end.flatten.compact
matched.any? && (!@block || @block.call(matched))
end
def matches_nokogiri?(stringlike)
stringlike = stringlike.body.to_s if stringlike.respond_to?(:body)
@query = query
@document = case stringlike
when Nokogiri::HTML::Document
stringlike
when Nokogiri::XML::NodeSet
@query = query.map { |q| q.gsub(%r'//', './') }
stringlike
when StringIO
Nokogiri::HTML(stringlike.string)
else
Nokogiri::HTML(stringlike.to_s)
end
matched = @document.xpath(*@query)
matched.any? && (!@block || @block.call(matched))
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} (#{@query}):\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:: The CSS selector to look for.
#
# ==== Returns
# HaveSelector:: A new have selector matcher.
# ---
# @api public
def have_selector(expected, &block)
HaveSelector.new(expected, &block)
end
alias_method :match_selector, :have_selector
# Matches HTML content against an XPath query
#
# ==== Parameters
# expected:: The XPath query to look for.
#
# ==== Returns
# HaveXpath:: A new have xpath matcher.
# ---
# @api public
def have_xpath(expected, &block)
HaveXpath.new(expected, &block)
end
alias_method :match_xpath, :have_xpath
def have_tag(name, attributes = {}, &block)
HaveTag.new([name, attributes], &block)
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