Skip to content

Instantly share code, notes, and snippets.

@peleteiro
Created February 9, 2010 21:38
Show Gist options
  • Save peleteiro/299696 to your computer and use it in GitHub Desktop.
Save peleteiro/299696 to your computer and use it in GitHub Desktop.
require 'spec/matchers/wrap_expectation'
require 'nokogiri'
class BeHtmlWith
def matches?(stwing, &block)
@scope.wrap_expectation self do
begin
bwock = block || @block || proc{}
builder = Nokogiri::HTML::Builder.new(&bwock)
match = builder.doc.root
doc = Nokogiri::HTML(stwing)
@last_match = 0
@failure_message = match_nodes(match, doc)
return @failure_message.nil?
end
end
end
=begin
The trick up our sleeve is Nokogiri::HTML::Builder. We passed
the matching block into it - that's where all the 'form',
'fieldset', 'input', etc. elements came from. And this trick
exposes both our target page and our matched elements to the
full power of Nokogiri. Schema validation, for example, would
be very easy.
The matches? method works by building two DOMs, and forcing
our page's DOM to satisfy each element, attribute, and text
in our specification's DOM.
To match nodes, we first find all nodes, by name, below
the current node. Note that match_nodes() recurses. Then
we throw away all nodes that don't satisfy our matching
criteria.
We pick the first node that passes that check, and
then recursively match its children to each child,
if any, from our matching node.
=end
def match_nodes(match, doc)
node = doc.xpath("descendant::#{match.name}").
select{|n| resemble(match, n) }.
first or return complaint(match, doc)
this_match = node.xpath('preceding::*').length
if @last_match > this_match
return complaint(match, doc, 'node is out of specified order!')
end
@last_match = this_match
match.children.grep(Nokogiri::XML::Element).each do |child|
issue = match_nodes(child, node) and
return issue
end
return nil
end
=begin
At any point in that recursion, if we can't find a match,
we build a string describing that situation, and pass it
back up the call stack. This immediately stops any iterating
and recursing underway!
Two nodes "resemble" each other if their names are the
same (naturally!); if your matching element's
attributes are a subset of your page's element's
attributes, and if their text is similar:
=end
def resemble(match, node)
valuate(node.attributes.pass(*match.attributes.keys)) ==
valuate(match.attributes) or return false
match_text = match.children.grep(Nokogiri::XML::Text).map{|t| t.to_s.strip }
node_text = node .children.grep(Nokogiri::XML::Text).map{|t| t.to_s.strip }
match_text.empty? or 0 == ( match_text - node_text ).length
end
=begin
That method cannot simply compare node.text, because Nokogiri
conglomerates all that node's descendants' texts together, and
these would gum up our search. So those elaborate lines with
grep() and map() serve to extract all the current node's
immediate textual children, then compare them as sets.
Put another way, <form> does not appear to contain "First name".
Specifications can only match text by declaring their immediate
parent.
The remaining support methods are self-explanatory. They
prepare Node attributes for comparison, build our diagnostics,
and plug our matcher object into RSpec:
=end
def valuate(attributes)
attributes.inject({}) do |h,(k,v)|
h.merge(k => v.value)
end # this converts objects to strings, so our Hashes
end # can compare for equality
def complaint(node, match, berate = nil)
"\n #{berate}".rstrip +
"\n\n#{node.to_html}\n" +
" does not match\n\n" +
match.to_html
end
attr_accessor :failure_message
def negative_failure_message
"yack yack yack"
end
def initialize(scope, &block)
@scope, @block = scope, block
end
end
def be_html_with(&block)
BeHtmlWith.new(self, &block)
end
class Hash
def pass(*keys)
select{|k,v| keys.include? k }
end
def block(*keys)
reject{|k,v| keys.include? k }
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment