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.
- data/README.textile +10 -2
- data/TODO +4 -0
- data/lib/locator/boolean.rb +1 -1
- data/lib/locator/dom/nokogiri/element.rb +14 -5
- data/lib/locator/dom/nokogiri/page.rb +7 -3
- data/lib/locator/dom/nokogiri.rb +1 -1
- data/lib/locator/dom.rb +1 -1
- data/lib/locator/element/area.rb +2 -2
- data/lib/locator/element/button.rb +3 -3
- data/lib/locator/element/check_box.rb +9 -0
- data/lib/locator/element/elements_list.rb +6 -6
- data/lib/locator/element/field.rb +2 -3
- data/lib/locator/element/file.rb +9 -0
- data/lib/locator/element/form.rb +2 -2
- data/lib/locator/element/form_element.rb +9 -0
- data/lib/locator/element/hidden_field.rb +9 -0
- data/lib/locator/element/input.rb +11 -0
- data/lib/locator/element/label.rb +2 -2
- data/lib/locator/element/labeled_element.rb +8 -10
- data/lib/locator/element/link.rb +2 -2
- data/lib/locator/element/radio_button.rb +9 -0
- data/lib/locator/element/select.rb +3 -3
- data/lib/locator/element/select_option.rb +2 -2
- data/lib/locator/element/text_area.rb +3 -3
- data/lib/locator/element.rb +31 -24
- data/lib/locator/result.rb +46 -0
- data/lib/locator/version.rb +2 -2
- data/lib/locator/xpath.rb +33 -14
- data/lib/locator.rb +43 -21
- data/test/all.rb +1 -1
- data/test/locator/element/button_test.rb +10 -5
- data/test/locator/element/field_test.rb +24 -13
- data/test/locator/element/form_test.rb +12 -6
- data/test/locator/element/label_test.rb +10 -6
- data/test/locator/element/link_test.rb +16 -14
- data/test/locator/element/select_option_test.rb +14 -7
- data/test/locator/element/select_test.rb +10 -6
- data/test/locator/element/text_area_test.rb +10 -5
- data/test/locator/element_test.rb +79 -21
- data/test/locator/xpath_test.rb +0 -5
- data/test/locator_test.rb +63 -13
- data/test/test_helper.rb +0 -14
- 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
|
-
|
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
data/lib/locator/boolean.rb
CHANGED
@@ -1,20 +1,29 @@
|
|
1
|
-
|
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
|
-
|
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
|
16
|
-
@dom.
|
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
|
data/lib/locator/dom/nokogiri.rb
CHANGED
data/lib/locator/dom.rb
CHANGED
data/lib/locator/element/area.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
|
-
|
1
|
+
module Locator
|
2
2
|
class Element
|
3
3
|
class Button < ElementsList
|
4
4
|
def initialize
|
5
|
-
input = Element.new('input',
|
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
|
@@ -1,14 +1,14 @@
|
|
1
|
-
|
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
|
11
|
-
elements.map { |element| element.
|
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
|
-
|
1
|
+
module Locator
|
2
2
|
class Element
|
3
3
|
class Field < ElementsList
|
4
4
|
def initialize
|
5
|
-
|
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
|
data/lib/locator/element/form.rb
CHANGED
@@ -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,16 +1,14 @@
|
|
1
|
-
|
1
|
+
module Locator
|
2
2
|
class Element
|
3
3
|
class LabeledElement < Element
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
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
|
12
|
-
|
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
|
data/lib/locator/element/link.rb
CHANGED
data/lib/locator/element.rb
CHANGED
@@ -1,45 +1,52 @@
|
|
1
|
-
|
2
|
-
class Element
|
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
|
-
|
18
|
-
|
19
|
-
|
20
|
-
@
|
21
|
-
@
|
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
|
25
|
-
attributes =
|
26
|
-
|
27
|
-
|
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
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
data/lib/locator/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
|
2
|
-
VERSION = "0.0.
|
1
|
+
module Locator
|
2
|
+
VERSION = "0.0.2"
|
3
3
|
end
|
data/lib/locator/xpath.rb
CHANGED
@@ -1,13 +1,20 @@
|
|
1
|
-
|
2
|
-
autoload :Boolean, 'locator/boolean'
|
3
|
-
|
1
|
+
module Locator
|
4
2
|
Boolean::Or.operator, Boolean::And.operator = ' | ', ''
|
5
3
|
|
6
|
-
class Xpath <
|
4
|
+
class Xpath < Array
|
7
5
|
def initialize(name = nil, attributes = {})
|
8
|
-
|
9
|
-
|
10
|
-
attributes.each
|
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|
|
19
|
-
expr
|
20
|
-
expr.empty? ? '' : '[' + expr.
|
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
|
25
|
-
"[contains(@#{name}
|
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(
|
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
|
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
|
-
|
4
|
-
autoload :
|
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
|
15
|
-
|
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
|
-
|
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(
|
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(
|
37
|
+
result = if scope = options.delete(:within)
|
38
|
+
within(*Array(scope)) { locate(dom, *args) }
|
38
39
|
else
|
39
|
-
|
40
|
-
|
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
|
-
|
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
|
-
|
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