locator 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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