Skip to content

Instantly share code, notes, and snippets.

@kaspth
Last active December 20, 2015 00:09
Show Gist options
  • Save kaspth/6039324 to your computer and use it in GitHub Desktop.
Save kaspth/6039324 to your computer and use it in GitHub Desktop.
An attempt to absolve css_select and assert_select from their argument parsing. To make the code more declarative and stuff... The order of arguments: 0: html element (optional) 1: selector 2: comparator 3: message
def css_select(*args)
# See assert_select to understand what's going on here.
arg = args.shift
if arg.is_a?(HTML::Node)
root = arg
arg = args.shift
elsif arg == nil
raise ArgumentError, "First argument is either selector or element to select, but nil found. Perhaps you called assert_select with an element that does not exist?"
elsif defined?(@selected) && @selected
matches = []
@selected.each do |selected|
subset = css_select(selected, HTML::Selector.new(arg.dup, args.dup))
subset.each do |match|
matches << match unless matches.any? { |m| m.equal?(match) }
end
end
return matches
else
root = response_from_page
end
case arg
when String
selector = HTML::Selector.new(arg, args)
when Array
selector = HTML::Selector.new(*arg)
when HTML::Selector
selector = arg
else raise ArgumentError, "Expecting a selector as the first argument"
end
selector.select(root)
end
def css_select(*args)
# See assert_select to understand what's going on here.
parser = ArgumentsParser.new(self, args, Proc.new do |selector|
# will only be called if we're a nested select, i.e. @selected is set
matches = []
@selected.each do |selected|
subset = css_select(selected, selector)
subset.each do |match|
matches << match unless matches.include?(match)
end
end
return matches
end)
parser.root.css(parser.selector)
end
def assert_select(*args, &block)
@selected ||= nil
parser = ArgumentsParser.new(self, args, Proc.new do |_|
root = Loofah.fragment('')
root.add_child @selected
root
end)
# Start with optional element followed by mandatory selector.
root = parser.root
# First or second argument is the selector
selector = parser.selector
# Next argument is used for equality tests.
equals = parser.equals
# Last argument is the message we use if the assertion fails.
message = parser.message
#- message = "No match made with selector #{selector.inspect}" unless message
matches = root.css(selector)
# more code which I've skipped for now...
end
protected
# +assert_select+ and +css_select+ call this to obtain the content in the HTML page.
def response_from_page
html_document.root
end
def html_document
@html_document ||= if @response.content_type =~ /xml$/
Loofah.xml_fragment(@response.body)
else
Loofah.fragment(@response.body)
end
end
def nested_call?
defined(@selected) && @selected
end
class ArgumentsParser #:nodoc:
attr_accessor :root, :selector
def initialize(initiator, *args, &root_for_nested_call_proc)
raise ArgumentError, "ArgumentsParser expects a block for parsing a nested call's arguments" unless block_given?
@initiator = initiator
@args = args
# see +determine_root_from+
@selector_is_second_argument = false
@root = determine_root_from(@args.shift)
arg = @selector_is_second_argument ? @args.shift : @args.first
@selector = selector_from(arg)
end
def determine_root_from(root_or_selector)
if root_or_selector.is_a?(Nokogiri::XML::Node)
# First argument is a node (tag or text, but also HTML root),
# so we know what we're selecting from,
# we also know that the second argument is the selector
@selector_is_second_argument = true
root_or_selector
elsif root_or_selector == nil
raise ArgumentError, "First argument is either selector or element to select, but nil found. Perhaps you called assert_select with an element that does not exist?"
elsif @initiator.nested_call?
# root_or_selector is a selector since the first call failed
root_for_nested_select_proc.call(root_or_selector)
else
@initiator.response_from_page
end
end
def selector_from(arg)
unless arg.is_a? String
raise ArgumentError, "Expecting a selector as the first argument"
end
arg
end
end
class HTMLArgumentsParser < ArgumentsParser
attr_accessor :equals, :message
def initialize(*)
super
@equals = assign_equals_from(@args.shift)
@message = @args.shift
if @args.shift
raise ArgumentError, "Not expecting that last argument, you either have too many arguments, or they're the wrong type"
end
end
def assign_equals_from(comparator)
equals = {}
case comparator
when Hash
equals = comparator
when String, Regexp
equals[:text] = comparator
when Integer
equals[:count] = comparator
when Range
equals[:minimum] = comparator.begin
equals[:maximum] = comparator.end
when FalseClass
equals[:count] = 0
when NilClass, TrueClass
equals[:minimum] = 1
else raise ArgumentError, "I don't understand what you're trying to match"
end
# By default we're looking for at least one match.
if equals[:count]
equals[:minimum] = equals[:maximum] = equals[:count]
else
equals[:minimum] ||= 1
end
equals
end
end
end
@kaspth
Copy link
Author

kaspth commented Jul 19, 2013

Hi @vipulnsward, could I get some feedback on this? 😄 (I know that some of it is horrible.)
I was trying to abstract the knowledge of the order of arguments away from css_select and assert_select.

Compare the old css_select and my version. If it wasn't for that proc, it would be:

parser = ArgumentsParser.new(self, args) # parses arguments here

parser.selector.select(parser.root)

Since most of it was dealing with arguments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment