Created
February 9, 2010 21:38
-
-
Save peleteiro/299696 to your computer and use it in GitHub Desktop.
This file contains hidden or 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
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