locator 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/README.textile +10 -2
  2. data/TODO +4 -0
  3. data/lib/locator/boolean.rb +1 -1
  4. data/lib/locator/dom/nokogiri/element.rb +14 -5
  5. data/lib/locator/dom/nokogiri/page.rb +7 -3
  6. data/lib/locator/dom/nokogiri.rb +1 -1
  7. data/lib/locator/dom.rb +1 -1
  8. data/lib/locator/element/area.rb +2 -2
  9. data/lib/locator/element/button.rb +3 -3
  10. data/lib/locator/element/check_box.rb +9 -0
  11. data/lib/locator/element/elements_list.rb +6 -6
  12. data/lib/locator/element/field.rb +2 -3
  13. data/lib/locator/element/file.rb +9 -0
  14. data/lib/locator/element/form.rb +2 -2
  15. data/lib/locator/element/form_element.rb +9 -0
  16. data/lib/locator/element/hidden_field.rb +9 -0
  17. data/lib/locator/element/input.rb +11 -0
  18. data/lib/locator/element/label.rb +2 -2
  19. data/lib/locator/element/labeled_element.rb +8 -10
  20. data/lib/locator/element/link.rb +2 -2
  21. data/lib/locator/element/radio_button.rb +9 -0
  22. data/lib/locator/element/select.rb +3 -3
  23. data/lib/locator/element/select_option.rb +2 -2
  24. data/lib/locator/element/text_area.rb +3 -3
  25. data/lib/locator/element.rb +31 -24
  26. data/lib/locator/result.rb +46 -0
  27. data/lib/locator/version.rb +2 -2
  28. data/lib/locator/xpath.rb +33 -14
  29. data/lib/locator.rb +43 -21
  30. data/test/all.rb +1 -1
  31. data/test/locator/element/button_test.rb +10 -5
  32. data/test/locator/element/field_test.rb +24 -13
  33. data/test/locator/element/form_test.rb +12 -6
  34. data/test/locator/element/label_test.rb +10 -6
  35. data/test/locator/element/link_test.rb +16 -14
  36. data/test/locator/element/select_option_test.rb +14 -7
  37. data/test/locator/element/select_test.rb +10 -6
  38. data/test/locator/element/text_area_test.rb +10 -5
  39. data/test/locator/element_test.rb +79 -21
  40. data/test/locator/xpath_test.rb +0 -5
  41. data/test/locator_test.rb +63 -13
  42. data/test/test_helper.rb +0 -14
  43. metadata +11 -2
data/README.textile CHANGED
@@ -1,5 +1,13 @@
1
1
  h1. Locator
2
2
 
3
- This library aims to extract common html element selection from testing tools such as "Webrat":http://github.com/brynary/webrat/blob/master/lib/webrat/core/locators.rb, "Capybara":http://github.com/jnicklas/capybara/blob/master/lib/capybara/xpath.rb or "Steam":http://github.com/svenfuchs/steam/blob/master/lib/steam/locators.rb.
3
+ This library aims to extract common html element selection from testing tools such as "Webrat":http://github.com/brynary/webrat/blob/master/lib/webrat/core/locators.rb, "Capybara":http://github.com/jnicklas/capybara/blob/master/lib/capybara/xpath.rb or "Steam":http://github.com/svenfuchs/steam/blob/master/lib/steam/locators.rb. At its core it constructs "XPath":http://github.com/svenfuchs/locator/blob/master/lib/locator/xpath.rb objects using a simple "boolean expression engine":http://github.com/svenfuchs/locator/blob/master/lib/locator/boolean.rb in order to locate elements in a Nokogiri DOM. It provides a bunch of "Element classes":http://github.com/svenfuchs/locator/blob/master/lib/locator/element.rb that are targeted at implementing a Webrat-style DSL for convenience.
4
4
 
5
- At its core it constructs "XPath":http://github.com/svenfuchs/locator/blob/master/lib/locator/xpath.rb objects using a simple "boolean expression engine":http://github.com/svenfuchs/locator/blob/master/lib/locator/boolean.rb in order to locate elements in a Nokogiri DOM. It provides a bunch of "Element classes":http://github.com/svenfuchs/locator/blob/master/lib/locator/element.rb that are targeted at implementing a Webrat-style DSL for convenience.
5
+ h2. Usage
6
+
7
+ Of course you can use the underlying implementation, too, but there are three main public methods which are supposed to give you access to all you need.
8
+
9
+ h3. Building an XPath
10
+
11
+ h3. Locating an element
12
+
13
+ h3. Scoping to an element
data/TODO ADDED
@@ -0,0 +1,4 @@
1
+ somehow allow these?
2
+
3
+ locate(html, :foo) { locate(:bar) }
4
+ within(html, :foo) { locate(:bar) }
@@ -1,4 +1,4 @@
1
- class Locator
1
+ module Locator
2
2
  module Boolean
3
3
  class Terms < Array
4
4
  def and!(other)
@@ -1,20 +1,29 @@
1
- class Locator
1
+ module Locator
2
2
  module Dom
3
3
  module Nokogiri
4
4
  class Element
5
- attr_reader :element
5
+ attr_reader :element, :matches
6
6
 
7
7
  def initialize(element)
8
8
  @element = element
9
+ @matches = []
9
10
  end
10
-
11
+
12
+ def <=>(other)
13
+ to_s.length <=> other.to_s.length
14
+ end
15
+
11
16
  def xpath
12
17
  element.path.to_s
13
18
  end
14
-
19
+
15
20
  def css_path
16
21
  element.css_path.to_s
17
22
  end
23
+
24
+ def content
25
+ element.content
26
+ end
18
27
 
19
28
  def inner_html
20
29
  element.inner_html
@@ -39,7 +48,7 @@ class Locator
39
48
  def attributes(names)
40
49
  names.map { |name| attribute(name) }
41
50
  end
42
-
51
+
43
52
  def elements_by_xpath(xpath)
44
53
  element.xpath(xpath).map { |element| Element.new(element) }
45
54
  end
@@ -1,4 +1,4 @@
1
- class Locator
1
+ module Locator
2
2
  module Dom
3
3
  module Nokogiri
4
4
  class Page
@@ -12,8 +12,12 @@ class Locator
12
12
  elements_by_xpath("//*[@id='#{id}']").first
13
13
  end
14
14
 
15
- def elements_by_xpath(xpath)
16
- @dom.xpath(xpath).map { |element| Element.new(element) }
15
+ def elements_by_css(*rules)
16
+ @dom.css(*rules).map { |element| Element.new(element) }
17
+ end
18
+
19
+ def elements_by_xpath(*xpaths)
20
+ @dom.xpath(*xpaths).map { |element| Element.new(element) }
17
21
  end
18
22
 
19
23
  def to_s
@@ -1,6 +1,6 @@
1
1
  require 'nokogiri'
2
2
 
3
- class Locator
3
+ module Locator
4
4
  module Dom
5
5
  module Nokogiri
6
6
  autoload :Element, 'locator/dom/nokogiri/element'
data/lib/locator/dom.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  require 'nokogiri'
2
2
 
3
- class Locator
3
+ module Locator
4
4
  module Dom
5
5
  autoload :Nokogiri, 'locator/dom/nokogiri'
6
6
 
@@ -1,8 +1,8 @@
1
- class Locator
1
+ module Locator
2
2
  class Element
3
3
  class Area < Element
4
4
  def initialize
5
- super('area', [:id, :alt])
5
+ super('area', :equals => [:id], :matches => [:alt])
6
6
  end
7
7
  end
8
8
  end
@@ -1,9 +1,9 @@
1
- class Locator
1
+ module Locator
2
2
  class Element
3
3
  class Button < ElementsList
4
4
  def initialize
5
- input = Element.new('input', [:id, :name, :value, :alt], :type => %w(submit button image))
6
- button = Element.new('button', [:id, :name, :content])
5
+ input = Element.new('input', { :equals => [:id, :name], :matches => [:value] }, :type => %w(submit button image))
6
+ button = Element.new('button', :equals => [:id, :name], :matches => [:content])
7
7
  super(input, button)
8
8
  end
9
9
  end
@@ -0,0 +1,9 @@
1
+ module Locator
2
+ class Element
3
+ class CheckBox < Input
4
+ def initialize
5
+ super(:type => :checkbox)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -1,14 +1,14 @@
1
- class Locator
1
+ module Locator
2
2
  class Element
3
- class ElementsList
3
+ class ElementsList < Element
4
4
  attr_reader :elements
5
-
5
+
6
6
  def initialize(*elements)
7
7
  @elements = elements
8
8
  end
9
-
10
- def xpath(*args)
11
- elements.map { |element| element.xpath(*args) }.join(' | ')
9
+
10
+ def lookup(dom, selector, attributes)
11
+ Result.new(elements.map { |element| element.send(:lookup, dom, selector, attributes) }.flatten)
12
12
  end
13
13
  end
14
14
  end
@@ -1,9 +1,8 @@
1
- class Locator
1
+ module Locator
2
2
  class Element
3
3
  class Field < ElementsList
4
4
  def initialize
5
- types = [:text, :password , :email, :url, :search, :tel, :color]
6
- super(LabeledElement.new('input', [:id, :name], :type => types), TextArea.new)
5
+ super(Input.new, TextArea.new)
7
6
  end
8
7
  end
9
8
  end
@@ -0,0 +1,9 @@
1
+ module Locator
2
+ class Element
3
+ class File < Input
4
+ def initialize
5
+ super('input', :type => :file)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -1,8 +1,8 @@
1
- class Locator
1
+ module Locator
2
2
  class Element
3
3
  class Form < Element
4
4
  def initialize
5
- super('form', [:id, :name])
5
+ super('form', :equals => [:id, :name])
6
6
  end
7
7
  end
8
8
  end
@@ -0,0 +1,9 @@
1
+ module Locator
2
+ class Element
3
+ class FormElement < Element
4
+ def lookup(dom, selector, attributes)
5
+ super(dom, selector, attributes) + LabeledElement.new(name).send(:lookup, dom, selector, attributes)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Locator
2
+ class Element
3
+ class HiddenField < Input
4
+ def initialize
5
+ super(:type => :hidden)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ module Locator
2
+ class Element
3
+ class Input < FormElement
4
+ def initialize(attributes = {})
5
+ attributes = { :type => [:text, :password , :email, :url, :search, :tel, :color] }.merge(attributes)
6
+ matchables = { :equals => [:id, :name] }
7
+ super('input', { :equals => [:id, :name] }, attributes)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -1,8 +1,8 @@
1
- class Locator
1
+ module Locator
2
2
  class Element
3
3
  class Label < Element
4
4
  def initialize
5
- super('label', [:id, :content])
5
+ super('label', :equals => [:id, :content])
6
6
  end
7
7
  end
8
8
  end
@@ -1,16 +1,14 @@
1
- class Locator
1
+ module Locator
2
2
  class Element
3
3
  class LabeledElement < Element
4
- attr_reader :labeled
5
-
6
- def initialize(name = '*', locatables = [], attributes = {})
7
- @labeled = Element.new(name, [], attributes)
8
- super
4
+ def lookup(dom, selector, attributes)
5
+ ids = element_ids(dom, selector)
6
+ result = ids.empty? ? Result.new : super(dom, nil, attributes.merge(:id => ids))
7
+ result.each { |element| element.matches << selector }
9
8
  end
10
-
11
- def compose(selector, attributes)
12
- super
13
- replace(self + labeled.and("[@id=//label[contains(.,\"#{selector}\")]/@for]")) if selector
9
+
10
+ def element_ids(dom, selector)
11
+ Label.new.send(:lookup, dom, selector).map { |element| element.attribute('for') }
14
12
  end
15
13
  end
16
14
  end
@@ -1,8 +1,8 @@
1
- class Locator
1
+ module Locator
2
2
  class Element
3
3
  class Link < Element
4
4
  def initialize
5
- super('a', [:id, :content], :href => true)
5
+ super('a', nil, :href => true)
6
6
  end
7
7
  end
8
8
  end
@@ -0,0 +1,9 @@
1
+ module Locator
2
+ class Element
3
+ class RadioButton < Input
4
+ def initialize
5
+ super(:type => :radio)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -1,8 +1,8 @@
1
- class Locator
1
+ module Locator
2
2
  class Element
3
- class Select < LabeledElement
3
+ class Select < FormElement
4
4
  def initialize
5
- super('select', [:id, :name])
5
+ super('select', :equals => [:id, :name])
6
6
  end
7
7
  end
8
8
  end
@@ -1,8 +1,8 @@
1
- class Locator
1
+ module Locator
2
2
  class Element
3
3
  class SelectOption < Element
4
4
  def initialize
5
- super('option', [:id, :value, :content])
5
+ super('option', :equals => [:id, :value, :content])
6
6
  end
7
7
  end
8
8
  end
@@ -1,8 +1,8 @@
1
- class Locator
1
+ module Locator
2
2
  class Element
3
- class TextArea < LabeledElement
3
+ class TextArea < FormElement
4
4
  def initialize
5
- super('textarea', [:id, :name])
5
+ super('textarea', :equals => [:id, :name])
6
6
  end
7
7
  end
8
8
  end
@@ -1,45 +1,52 @@
1
- class Locator
2
- class Element < Xpath
1
+ module Locator
2
+ class Element
3
3
  autoload :Area, 'locator/element/area'
4
4
  autoload :Button, 'locator/element/button'
5
+ autoload :CheckBox, 'locator/element/check_box'
5
6
  autoload :ElementsList, 'locator/element/elements_list'
6
7
  autoload :Field, 'locator/element/field'
8
+ autoload :File, 'locator/element/file'
7
9
  autoload :Form, 'locator/element/form'
10
+ autoload :FormElement, 'locator/element/form_element'
11
+ autoload :HiddenField, 'locator/element/hidden_field'
12
+ autoload :Input, 'locator/element/input'
8
13
  autoload :Label, 'locator/element/label'
9
14
  autoload :LabeledElement, 'locator/element/labeled_element'
10
15
  autoload :Link, 'locator/element/link'
16
+ autoload :RadioButton, 'locator/element/radio_button'
11
17
  autoload :Select, 'locator/element/select'
12
18
  autoload :SelectOption, 'locator/element/select_option'
13
19
  autoload :TextArea, 'locator/element/text_area'
14
-
15
- attr_reader :locatables
16
20
 
17
- def initialize(name = '*', locatables = [], attributes = {})
18
- super(name)
19
- @content = !!locatables.delete(:content)
20
- @locatables = locatables
21
- @attributes = attributes
21
+ attr_reader :name, :locatables, :attributes
22
+
23
+ def initialize(name = nil, locatables = nil, attributes = nil)
24
+ @name = name || '*'
25
+ @locatables = { :equals => :id, :matches => :content }.merge(locatables || {})
26
+ @attributes = attributes || {}
27
+ end
28
+
29
+ def locate(*args)
30
+ all(*args).first
22
31
  end
23
32
 
24
- def xpath(*args)
25
- attributes = Hash === args.last ? args.pop : {}
26
- compose(args.pop, attributes)
27
- to_s
33
+ def all(dom, *args)
34
+ attributes, selector = args.last.is_a?(Hash) ? args.pop : {}, args.pop
35
+ result = lookup(dom, selector, attributes)
36
+ result.sort! if selector
37
+ result
38
+ end
39
+
40
+ def xpath(attributes = {})
41
+ Xpath.new(name, self.attributes.merge(attributes)).to_s
28
42
  end
29
43
 
30
44
  protected
31
-
32
- def content?
33
- @content
34
- end
35
45
 
36
- def compose(selector, attributes)
37
- attributes = @attributes.merge(attributes)
38
- attributes.each { |name, value| and! equals(name, value) }
39
- terms = []
40
- terms << equals(locatables, selector) if selector
41
- terms << contains(selector) if content? && selector
42
- and!(terms) unless terms.empty?
46
+ def lookup(dom, selector, attributes = {})
47
+ dom = dom.respond_to?(:elements_by_xpath) ? dom : Locator::Dom.page(dom)
48
+ elements = dom.elements_by_xpath(xpath(attributes))
49
+ Result.new(elements).filter!(selector, locatables)
43
50
  end
44
51
  end
45
52
  end
@@ -0,0 +1,46 @@
1
+ module Locator
2
+ class Result < Array
3
+ class << self
4
+ def equals?(value, selector)
5
+ value == selector
6
+ end
7
+
8
+ def matches?(value, selector)
9
+ value = normalize_whitespace(value)
10
+ Regexp === selector ? value =~ selector : value.include?(selector)
11
+ end
12
+
13
+ def normalize_whitespace(value)
14
+ value.gsub(/\s+/, ' ').strip
15
+ end
16
+ end
17
+
18
+ def filter!(selector, locatables)
19
+ selector ? replace(filter(selector, locatables)) : self
20
+ end
21
+
22
+ def filter(selector, locatables)
23
+ selector ? locatables.map { |(type, attrs)| filter_by(type, selector, attrs) }.flatten : self
24
+ end
25
+
26
+ def sort!
27
+ each { |element| element.matches.sort! }
28
+ super { |lft, rgt| lft.matches.first.length <=> rgt.matches.first.length }
29
+ end
30
+
31
+ def +(other)
32
+ replace(super)
33
+ end
34
+
35
+ protected
36
+
37
+ def filter_by(type, selector, attributes)
38
+ select do |element|
39
+ Array(attributes).any? do |name|
40
+ value = name == :content ? element.content : element.attribute(name.to_s)
41
+ element.matches << value if self.class.send("#{type}?", value, selector)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -1,3 +1,3 @@
1
- class Locator
2
- VERSION = "0.0.1"
1
+ module Locator
2
+ VERSION = "0.0.2"
3
3
  end
data/lib/locator/xpath.rb CHANGED
@@ -1,13 +1,20 @@
1
- class Locator
2
- autoload :Boolean, 'locator/boolean'
3
-
1
+ module Locator
4
2
  Boolean::Or.operator, Boolean::And.operator = ' | ', ''
5
3
 
6
- class Xpath < Boolean::Terms
4
+ class Xpath < Array
7
5
  def initialize(name = nil, attributes = {})
8
- names = Array(name || '*').map { |name| xpath?(name) ? name : ".//#{name}" }
9
- super(names)
10
- attributes.each { |name, value| and!(equals(name, value)) }
6
+ super(Array(name || '*').map { |name| xpath?(name) ? name : ".//#{name}" })
7
+
8
+ attributes.each do |name, value|
9
+ case name
10
+ when :class
11
+ and!(has_class(name, value))
12
+ else
13
+ and!(equals(name, value))
14
+ end
15
+ end
16
+
17
+ flatten!
11
18
  end
12
19
 
13
20
  def equals(names, values)
@@ -15,24 +22,36 @@ class Locator
15
22
  when TrueClass
16
23
  "[@#{names}]"
17
24
  else
18
- values = Array(values).map { |value| xpath?(value) ? value : "\"#{value}\"" }
19
- expr = Array(names).map { |name| values.map { |value| "@#{name}=#{value}" } }
20
- expr.empty? ? '' : '[' + expr.flatten.join(' or ') + ']'
25
+ values = Array(values).map { |value| quote(value) }
26
+ expr= Array(names).map { |name| values.map { |value| "@#{name}=#{value}" } }.flatten
27
+ expr.empty? ? '' : '[' + expr.join(' or ') + ']'
21
28
  end
22
29
  end
23
30
 
24
- def matches(name, value)
25
- "[contains(@#{name},\"#{value}\")]"
31
+ def has_class(name, value)
32
+ "[contains(concat(' ', @#{name}, ' '), concat(' ', \"#{value}\", ' '))]"
26
33
  end
27
34
 
28
35
  def contains(value)
29
- "/descendant-or-self::*[contains(.,\"#{value}\")]"
36
+ "/descendant-or-self::*[contains(., \"#{value}\")]"
37
+ end
38
+
39
+ def and!(other)
40
+ replace(self.map { |l| other.map { |r| "#{l}#{r}" } })
41
+ end
42
+
43
+ def to_s
44
+ flatten.join(' | ')
30
45
  end
31
46
 
32
47
  protected
33
48
 
49
+ def quote(value)
50
+ xpath?(value) ? value : "\"#{value}\""
51
+ end
52
+
34
53
  def xpath?(string)
35
- string.to_s[0, 1] == '/'
54
+ string =~ %r(^\.?//)
36
55
  end
37
56
  end
38
57
  end
data/lib/locator.rb CHANGED
@@ -1,18 +1,20 @@
1
1
  require 'core_ext/string/underscore'
2
2
 
3
- class Locator
4
- autoload :Actions, 'locator/actions'
3
+ module Locator
4
+ autoload :Boolean, 'locator/boolean'
5
5
  autoload :Dom, 'locator/dom'
6
6
  autoload :Element, 'locator/element'
7
+ autoload :Result, 'locator/result'
7
8
  autoload :Xpath, 'locator/xpath'
8
9
 
9
10
  class << self
10
11
  def [](type)
11
- locators[type.to_sym]
12
+ locators[type.to_sym] || raise("unknown locator type: #{type}")
12
13
  end
13
14
 
14
- def xpath(type, *args)
15
- Locator[type].new.xpath(*args)
15
+ def build(type)
16
+ locator = locators[type.to_sym] if type
17
+ locator ? locator.new : Locator::Element.new(type)
16
18
  end
17
19
 
18
20
  protected
@@ -24,29 +26,49 @@ class Locator
24
26
  end
25
27
  end
26
28
 
27
- attr_reader :dom, :scopes
28
-
29
- def initialize(dom)
30
- @dom = dom.respond_to?(:elements_by_xpath) ? dom : Dom.page(dom)
31
- @scopes = []
29
+ def xpath(type, *args)
30
+ Locator[type].new.xpath(*args)
32
31
  end
33
32
 
34
- def locate(type, *args)
33
+ def locate(dom, *args, &block)
34
+ return args.first if args.first.respond_to?(:elements_by_xpath)
35
+
35
36
  options = Hash === args.last ? args.last : {}
36
- if scope = options.delete(:within)
37
- within(scope) { locate(type, *args) }
37
+ result = if scope = options.delete(:within)
38
+ within(*Array(scope)) { locate(dom, *args) }
38
39
  else
39
- path = type.is_a?(Symbol) ? Locator.xpath(type, *args) : type
40
- scope = scopes.pop || dom
41
- scope.elements_by_xpath(path).first
40
+ type = args.shift if args.first.is_a?(Symbol)
41
+ Locator.build(type).locate(current_scope(dom), *args)
42
42
  end
43
+
44
+ result && block_given? ? within(result) { yield(self) } : result
43
45
  end
44
46
 
45
- # TODO currently only take an xpath or element
46
- def within(scope, &block)
47
- scope = scope.is_a?(Hash) ? scope.delete(:xpath) : scope
48
- scope = locate(scope) unless scope.respond_to?(:xpath)
47
+ def within(*scope)
49
48
  scopes.push(scope)
50
- instance_eval(&block)
49
+ yield(self)
51
50
  end
51
+
52
+ protected
53
+
54
+ def current_scope(dom)
55
+ dom = Locator::Dom.page(dom) unless dom.respond_to?(:elements_by_xpath)
56
+
57
+ case (scope = scopes.pop) && scope.first
58
+ when NilClass
59
+ dom
60
+ when %r(^\.?//)
61
+ dom.elements_by_xpath(scope.first).first
62
+ when String
63
+ dom.elements_by_css(scope.first).first
64
+ else
65
+ locate(dom, *scope)
66
+ end
67
+ end
68
+
69
+ def scopes
70
+ @scopes ||= []
71
+ end
72
+
73
+ extend(self)
52
74
  end
data/test/all.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  Dir[File.dirname(__FILE__) + '/**/*_test.rb'].each do |filename|
2
- require filename
2
+ require filename unless filename.include?('_locator')
3
3
  end