locator 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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