pincers 0.2.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/pincers/backend/base.rb +27 -3
- data/lib/pincers/backend/nokogiri.rb +4 -2
- data/lib/pincers/backend/webdriver.rb +82 -18
- data/lib/pincers/core/root_context.rb +10 -1
- data/lib/pincers/core/search_context.rb +134 -38
- data/lib/pincers/errors.rb +8 -0
- data/lib/pincers/support/configuration.rb +2 -1
- data/lib/pincers/support/query.rb +31 -0
- data/lib/pincers/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 88daefd24fb535d60be1252cb33ca632d6466c0f
|
4
|
+
data.tar.gz: abe5bde9ae30c13d6a424268e85606b383179d42
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2726362f0e0d4dd95bf87c50f795f557ef670007b80990c22f1425a955d23d93796623433b50e0344075fd96260ddfbfbe88988edea541c19caf863894744985
|
7
|
+
data.tar.gz: 7d66fd52f27ceb87085d643cb8f0df8aeda19747e23cf69b9bb2811b610c5283aa87d9bfa6cd9f45017303ef6e9b477c56da57a6aeb7e10e4fec5098822605d9
|
data/lib/pincers/backend/base.rb
CHANGED
@@ -8,6 +8,10 @@ module Pincers::Backend
|
|
8
8
|
@document = _document
|
9
9
|
end
|
10
10
|
|
11
|
+
def javascript_enabled?
|
12
|
+
false
|
13
|
+
end
|
14
|
+
|
11
15
|
def document_root
|
12
16
|
ensure_implementation :document_root
|
13
17
|
end
|
@@ -40,11 +44,11 @@ module Pincers::Backend
|
|
40
44
|
ensure_implementation :refresh_document
|
41
45
|
end
|
42
46
|
|
43
|
-
def search_by_css(_element, _selector)
|
47
|
+
def search_by_css(_element, _selector, _limit)
|
44
48
|
ensure_implementation :search_by_css
|
45
49
|
end
|
46
50
|
|
47
|
-
def search_by_xpath(_element, _selector)
|
51
|
+
def search_by_xpath(_element, _selector, _limit)
|
48
52
|
ensure_implementation :search_by_xpath
|
49
53
|
end
|
50
54
|
|
@@ -68,14 +72,34 @@ module Pincers::Backend
|
|
68
72
|
ensure_implementation :clear_input
|
69
73
|
end
|
70
74
|
|
75
|
+
def element_is_actionable?(_element)
|
76
|
+
return true
|
77
|
+
end
|
78
|
+
|
71
79
|
def set_element_text(_element, _value)
|
72
80
|
ensure_implementation :set_element_text
|
73
81
|
end
|
74
82
|
|
75
|
-
def click_on_element(_element)
|
83
|
+
def click_on_element(_element, _modifiers)
|
76
84
|
ensure_implementation :click_on_element
|
77
85
|
end
|
78
86
|
|
87
|
+
def right_click_on_element(_element)
|
88
|
+
ensure_implementation :right_click_on_element
|
89
|
+
end
|
90
|
+
|
91
|
+
def double_click_on_element(_element)
|
92
|
+
ensure_implementation :double_click_on_element
|
93
|
+
end
|
94
|
+
|
95
|
+
def hover_over_element(_element)
|
96
|
+
ensure_implementation :hover_over_element
|
97
|
+
end
|
98
|
+
|
99
|
+
def drag_and_drop(_element, _into)
|
100
|
+
ensure_implementation :drag_and_drop
|
101
|
+
end
|
102
|
+
|
79
103
|
def switch_to_frame(_element)
|
80
104
|
ensure_implementation :switch_to_frame
|
81
105
|
end
|
@@ -12,11 +12,13 @@ module Pincers::Backend
|
|
12
12
|
document.title
|
13
13
|
end
|
14
14
|
|
15
|
-
def search_by_css(_element, _selector)
|
15
|
+
def search_by_css(_element, _selector, _limit)
|
16
|
+
# nokogiri does not do any query level optimization when searching just one node
|
16
17
|
_element.css _selector
|
17
18
|
end
|
18
19
|
|
19
|
-
def search_by_xpath(_element, _selector)
|
20
|
+
def search_by_xpath(_element, _selector, _limit)
|
21
|
+
# nokogiri does not do any query level optimization when searching just one node
|
20
22
|
_element.xpath _selector
|
21
23
|
end
|
22
24
|
|
@@ -11,6 +11,10 @@ module Pincers::Backend
|
|
11
11
|
@driver = _driver
|
12
12
|
end
|
13
13
|
|
14
|
+
def javascript_enabled?
|
15
|
+
true
|
16
|
+
end
|
17
|
+
|
14
18
|
def document_root
|
15
19
|
[@driver]
|
16
20
|
end
|
@@ -43,12 +47,12 @@ module Pincers::Backend
|
|
43
47
|
@driver.navigate.refresh
|
44
48
|
end
|
45
49
|
|
46
|
-
def search_by_css(_element, _selector)
|
47
|
-
_element
|
50
|
+
def search_by_css(_element, _selector, _limit)
|
51
|
+
search _element, { css: _selector }, _limit
|
48
52
|
end
|
49
53
|
|
50
|
-
def search_by_xpath(_element, _selector)
|
51
|
-
_element
|
54
|
+
def search_by_xpath(_element, _selector, _limit)
|
55
|
+
search _element, { xpath: _selector }, _limit
|
52
56
|
end
|
53
57
|
|
54
58
|
def extract_element_tag(_element)
|
@@ -71,37 +75,61 @@ module Pincers::Backend
|
|
71
75
|
_element[_name]
|
72
76
|
end
|
73
77
|
|
78
|
+
def element_is_actionable?(_element)
|
79
|
+
# this is the base requisite in webdriver for actionable elements:
|
80
|
+
# non displayed items will always error on action
|
81
|
+
_element.displayed?
|
82
|
+
end
|
83
|
+
|
74
84
|
def set_element_text(_element, _value)
|
75
|
-
_element =
|
85
|
+
_element = ensure_ready_for_input _element
|
76
86
|
_element.clear
|
77
87
|
_element.send_keys _value
|
78
88
|
end
|
79
89
|
|
80
|
-
def click_on_element(_element)
|
81
|
-
_element =
|
82
|
-
|
90
|
+
def click_on_element(_element, _modifiers)
|
91
|
+
_element = ensure_ready_for_input _element
|
92
|
+
if _modifiers.length == 0
|
93
|
+
_element.click
|
94
|
+
else
|
95
|
+
click_with_modifiers(_element, _modifiers)
|
96
|
+
end
|
83
97
|
end
|
84
98
|
|
85
|
-
def
|
86
|
-
|
99
|
+
def right_click_on_element(_element)
|
100
|
+
assert_has_input_devices_for :right_click_on_element
|
101
|
+
_element = ensure_ready_for_input _element
|
102
|
+
actions.context_click(_element).perform
|
87
103
|
end
|
88
104
|
|
89
|
-
def
|
90
|
-
|
105
|
+
def double_click_on_element(_element)
|
106
|
+
assert_has_input_devices_for :double_click_on_element
|
107
|
+
_element = ensure_ready_for_input _element
|
108
|
+
actions.double_click(_element).perform
|
91
109
|
end
|
92
110
|
|
93
|
-
|
111
|
+
def hover_over_element(_element)
|
112
|
+
assert_has_input_devices_for :hover_over_element
|
113
|
+
_element = ensure_ready_for_input _element
|
114
|
+
actions.move_to(_element).perform
|
115
|
+
end
|
94
116
|
|
95
|
-
def
|
96
|
-
|
117
|
+
def drag_and_drop(_element, _on)
|
118
|
+
assert_has_input_devices_for :drag_and_drop
|
119
|
+
_element = ensure_input_element _element
|
120
|
+
actions.drag_and_drop(_element, _on).perform
|
97
121
|
end
|
98
122
|
|
99
|
-
def
|
100
|
-
|
123
|
+
def switch_to_frame(_element)
|
124
|
+
@driver.switch_to.frame _element
|
125
|
+
end
|
126
|
+
|
127
|
+
def switch_to_top_frame
|
128
|
+
@driver.switch_to.default_content
|
101
129
|
end
|
102
130
|
|
103
131
|
def check_visible(_elements)
|
104
|
-
|
132
|
+
_elements.first.displayed?
|
105
133
|
end
|
106
134
|
|
107
135
|
def check_enabled(_elements)
|
@@ -114,11 +142,47 @@ module Pincers::Backend
|
|
114
142
|
|
115
143
|
private
|
116
144
|
|
145
|
+
def search(_element, _query, _limit)
|
146
|
+
if _limit == 1
|
147
|
+
begin
|
148
|
+
[_element.find_element(_query)]
|
149
|
+
rescue Selenium::WebDriver::Error::NoSuchElementError
|
150
|
+
[]
|
151
|
+
end
|
152
|
+
else
|
153
|
+
_element.find_elements _query
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def actions
|
158
|
+
@driver.action
|
159
|
+
end
|
160
|
+
|
161
|
+
def click_with_modifiers(_element, _modifiers)
|
162
|
+
assert_has_input_devices_for :click_with_modifiers
|
163
|
+
_modifiers.each { |m| actions.key_down m }
|
164
|
+
actions.click _element
|
165
|
+
_modifiers.each { |m| actions.key_up m }
|
166
|
+
actions.perform
|
167
|
+
end
|
168
|
+
|
169
|
+
def assert_has_input_devices_for(_name)
|
170
|
+
unless @driver.kind_of? Selenium::WebDriver::DriverExtensions::HasInputDevices
|
171
|
+
raise MissingFeatureError, _name
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
117
175
|
def ensure_element(_element)
|
118
176
|
return @driver.find_element tag_name: 'html' if _element == @driver
|
119
177
|
_element
|
120
178
|
end
|
121
179
|
|
180
|
+
def ensure_ready_for_input(_element)
|
181
|
+
_element = ensure_element _element
|
182
|
+
Selenium::WebDriver::Wait.new.until { _element.displayed? }
|
183
|
+
_element
|
184
|
+
end
|
185
|
+
|
122
186
|
end
|
123
187
|
|
124
188
|
end
|
@@ -7,7 +7,7 @@ module Pincers::Core
|
|
7
7
|
attr_reader :config
|
8
8
|
|
9
9
|
def initialize(_backend, _config={})
|
10
|
-
super _backend.document_root, nil
|
10
|
+
super _backend.document_root, nil, nil
|
11
11
|
@backend = _backend
|
12
12
|
@config = Pincers.config.values.merge _config
|
13
13
|
end
|
@@ -76,8 +76,17 @@ module Pincers::Core
|
|
76
76
|
@config[:wait_interval]
|
77
77
|
end
|
78
78
|
|
79
|
+
def advanced_mode?
|
80
|
+
@config[:advanced_mode]
|
81
|
+
end
|
82
|
+
|
79
83
|
private
|
80
84
|
|
85
|
+
def wrap_siblings(_elements)
|
86
|
+
# root node siblings behave like childs
|
87
|
+
SearchContext.new _elements, self, nil
|
88
|
+
end
|
89
|
+
|
81
90
|
def goto_url(_url)
|
82
91
|
_url = "http://#{_url}" unless /^(https?|file|ftp):\/\// === _url
|
83
92
|
backend.navigate_to _url
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'pincers/extension/queries'
|
2
2
|
require 'pincers/extension/actions'
|
3
|
+
require 'pincers/support/query'
|
3
4
|
|
4
5
|
module Pincers::Core
|
5
6
|
class SearchContext
|
@@ -8,13 +9,19 @@ module Pincers::Core
|
|
8
9
|
include Pincers::Extension::Queries
|
9
10
|
include Pincers::Extension::Actions
|
10
11
|
|
11
|
-
|
12
|
+
attr_reader :parent, :query
|
12
13
|
|
13
14
|
def_delegators :elements, :length, :count, :empty?
|
14
15
|
|
15
|
-
def initialize(_elements, _parent)
|
16
|
+
def initialize(_elements, _parent, _query)
|
16
17
|
@elements = _elements
|
18
|
+
@scope = if @elements.nil? then nil else :all end
|
17
19
|
@parent = _parent
|
20
|
+
@query = _query
|
21
|
+
end
|
22
|
+
|
23
|
+
def frozen?
|
24
|
+
!backend.javascript_enabled? || @query.nil?
|
18
25
|
end
|
19
26
|
|
20
27
|
def root
|
@@ -29,17 +36,31 @@ module Pincers::Core
|
|
29
36
|
backend.document
|
30
37
|
end
|
31
38
|
|
39
|
+
def elements
|
40
|
+
reload_elements :all
|
41
|
+
@elements
|
42
|
+
end
|
43
|
+
|
32
44
|
def element
|
33
|
-
|
45
|
+
reload_elements :single
|
46
|
+
@elements.first
|
34
47
|
end
|
35
48
|
|
36
49
|
def element!
|
50
|
+
wait?(:present) unless frozen? or advanced_mode?
|
37
51
|
raise Pincers::EmptySetError.new self if empty?
|
38
52
|
element
|
39
53
|
end
|
40
54
|
|
55
|
+
def reload
|
56
|
+
raise Pincers::FrozenSetError.new self if frozen?
|
57
|
+
parent.reload if parent_needs_reload?
|
58
|
+
wrap_errors { reload_elements }
|
59
|
+
self
|
60
|
+
end
|
61
|
+
|
41
62
|
def each
|
42
|
-
elements.each { |el| yield
|
63
|
+
elements.each { |el| yield wrap_siblings [el] }
|
43
64
|
end
|
44
65
|
|
45
66
|
def [](*args)
|
@@ -48,31 +69,33 @@ module Pincers::Core
|
|
48
69
|
backend.extract_element_attribute element!, args[0]
|
49
70
|
end
|
50
71
|
else
|
51
|
-
|
72
|
+
wrap_siblings Array(elements.send(:[],*args))
|
52
73
|
end
|
53
74
|
end
|
54
75
|
|
55
76
|
def first
|
56
|
-
if
|
77
|
+
if element.nil? then nil else wrap_siblings [element] end
|
57
78
|
end
|
58
79
|
|
59
80
|
def first!
|
60
|
-
|
81
|
+
wrap_siblings [element!]
|
61
82
|
end
|
62
83
|
|
63
84
|
def last
|
64
|
-
if elements.last.nil? then nil else
|
85
|
+
if elements.last.nil? then nil else wrap_siblings [elements.last] end
|
65
86
|
end
|
66
87
|
|
67
88
|
def css(_selector, _options={})
|
68
|
-
|
69
|
-
|
89
|
+
wrap_errors do
|
90
|
+
query = Pincers::Support::Query.new backend, :css, _selector, _options[:limit]
|
91
|
+
wrap_childs query
|
70
92
|
end
|
71
93
|
end
|
72
94
|
|
73
95
|
def xpath(_selector, _options={})
|
74
|
-
|
75
|
-
|
96
|
+
wrap_errors do
|
97
|
+
query = Pincers::Support::Query.new backend, :xpath, _selector, _options[:limit]
|
98
|
+
wrap_childs query
|
76
99
|
end
|
77
100
|
end
|
78
101
|
|
@@ -84,7 +107,7 @@ module Pincers::Core
|
|
84
107
|
|
85
108
|
def text
|
86
109
|
wrap_errors do
|
87
|
-
backend.extract_element_text
|
110
|
+
elements.map { |e| backend.extract_element_text e }.join
|
88
111
|
end
|
89
112
|
end
|
90
113
|
|
@@ -94,18 +117,38 @@ module Pincers::Core
|
|
94
117
|
end
|
95
118
|
end
|
96
119
|
|
97
|
-
#
|
120
|
+
# input related
|
98
121
|
|
99
122
|
def set_text(_value)
|
100
|
-
|
101
|
-
|
102
|
-
|
123
|
+
perform_action { |el| backend.set_element_text el, _value }
|
124
|
+
end
|
125
|
+
|
126
|
+
def click(*_modifiers)
|
127
|
+
perform_action { |el| backend.click_on_element el, _modifiers }
|
128
|
+
end
|
129
|
+
|
130
|
+
def right_click
|
131
|
+
perform_action { |el| backend.right_click_on_element el }
|
103
132
|
end
|
104
133
|
|
105
|
-
def
|
134
|
+
def double_click
|
135
|
+
perform_action { |el| backend.double_click_on_element el }
|
136
|
+
end
|
137
|
+
|
138
|
+
def hover
|
139
|
+
perform_action { |el| backend.hover_over_element el }
|
140
|
+
end
|
141
|
+
|
142
|
+
def drag_to(_element)
|
106
143
|
wrap_errors do
|
107
|
-
|
144
|
+
if advanced_mode?
|
145
|
+
wait_actionable
|
146
|
+
_element.wait_actionable
|
147
|
+
end
|
148
|
+
|
149
|
+
backend.drag_and_drop element!, _element.element!
|
108
150
|
end
|
151
|
+
self
|
109
152
|
end
|
110
153
|
|
111
154
|
# context related
|
@@ -114,8 +157,43 @@ module Pincers::Core
|
|
114
157
|
root.goto frame: self
|
115
158
|
end
|
116
159
|
|
160
|
+
# waiting
|
161
|
+
|
162
|
+
def wait?(_condition, _options={}, &_block)
|
163
|
+
poll_until(_condition, _options) do
|
164
|
+
case _condition
|
165
|
+
when :present
|
166
|
+
ensure_present
|
167
|
+
when :actionable
|
168
|
+
ensure_present and ensure_actionable
|
169
|
+
else
|
170
|
+
check_method = "check_#{_condition}"
|
171
|
+
raise Pincers::MissingFeatureError.new check_method unless backend.respond_to? check_method
|
172
|
+
ensure_present and check_method.call(elements)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def wait(_condition, _options)
|
178
|
+
raise Pincers::ConditionTimeoutError.new self, _condition unless wait?(_condition, _options)
|
179
|
+
return self
|
180
|
+
end
|
181
|
+
|
117
182
|
private
|
118
183
|
|
184
|
+
def advanced_mode?
|
185
|
+
root.advanced_mode?
|
186
|
+
end
|
187
|
+
|
188
|
+
def ensure_present
|
189
|
+
reload if element.nil?
|
190
|
+
not element.nil?
|
191
|
+
end
|
192
|
+
|
193
|
+
def ensure_actionable
|
194
|
+
backend.element_is_actionable? element
|
195
|
+
end
|
196
|
+
|
119
197
|
def wrap_errors
|
120
198
|
begin
|
121
199
|
yield
|
@@ -126,39 +204,57 @@ module Pincers::Core
|
|
126
204
|
end
|
127
205
|
end
|
128
206
|
|
129
|
-
def
|
130
|
-
SearchContext.new _elements, self
|
131
|
-
end
|
132
|
-
|
133
|
-
def search_with_options(_options, &_block)
|
207
|
+
def perform_action
|
134
208
|
wrap_errors do
|
135
|
-
|
136
|
-
|
137
|
-
|
209
|
+
wait?(:actionable) unless advanced_mode?
|
210
|
+
raise Pincers::EmptySetError.new self if empty?
|
211
|
+
yield elements.first
|
138
212
|
end
|
213
|
+
self
|
139
214
|
end
|
140
215
|
|
141
|
-
def
|
142
|
-
|
143
|
-
|
144
|
-
|
216
|
+
def wrap_siblings(_elements)
|
217
|
+
SearchContext.new _elements, parent, nil
|
218
|
+
end
|
219
|
+
|
220
|
+
def wrap_childs(_query)
|
221
|
+
child_elements = if advanced_mode? then _query.execute(elements) else nil end
|
222
|
+
SearchContext.new child_elements, self, _query
|
223
|
+
end
|
224
|
+
|
225
|
+
def parent_needs_reload?
|
226
|
+
!parent.frozen? && parent.elements.count == 0
|
145
227
|
end
|
146
228
|
|
147
|
-
def
|
148
|
-
|
149
|
-
|
229
|
+
def reload_elements(_scope=nil)
|
230
|
+
case _scope
|
231
|
+
when :all
|
232
|
+
return if @scope == :all
|
233
|
+
@scope = :all
|
234
|
+
when :single
|
235
|
+
return unless @scope.nil?
|
236
|
+
@scope = :single
|
237
|
+
end
|
238
|
+
|
239
|
+
if @scope == :single
|
240
|
+
@elements = @query.execute parent.elements, 1 # force single record
|
241
|
+
else
|
242
|
+
@elements = @query.execute parent.elements
|
243
|
+
@scope = :all if @scope.nil?
|
244
|
+
end
|
245
|
+
end
|
150
246
|
|
247
|
+
def poll_until(_description, _options={})
|
151
248
|
timeout = _options.fetch :timeout, root.default_timeout
|
152
249
|
interval = _options.fetch :interval, root.default_interval
|
153
250
|
end_time = Time.now + timeout
|
154
251
|
|
155
|
-
|
156
|
-
|
157
|
-
return new_elements if backend.send check_method, new_elements
|
252
|
+
while Time.now <= end_time
|
253
|
+
return true if !!yield
|
158
254
|
sleep interval
|
159
255
|
end
|
160
256
|
|
161
|
-
|
257
|
+
return false
|
162
258
|
end
|
163
259
|
|
164
260
|
end
|
data/lib/pincers/errors.rb
CHANGED
@@ -0,0 +1,31 @@
|
|
1
|
+
module Pincers::Support
|
2
|
+
class Query
|
3
|
+
|
4
|
+
attr_reader :lang, :query, :limit
|
5
|
+
|
6
|
+
def initialize(_backend, _lang, _query, _limit)
|
7
|
+
@backend = _backend
|
8
|
+
@lang = _lang
|
9
|
+
@query = _query
|
10
|
+
@limit = _limit
|
11
|
+
end
|
12
|
+
|
13
|
+
def execute(_elements, _force_limit=nil)
|
14
|
+
fun = case @lang
|
15
|
+
when :xpath then :search_by_xpath
|
16
|
+
else :search_by_css end
|
17
|
+
|
18
|
+
explode_elements _elements, fun, _force_limit || @limit
|
19
|
+
end
|
20
|
+
|
21
|
+
def explode_elements(_elements, _fun, _limit)
|
22
|
+
_elements.inject([]) do |r, element|
|
23
|
+
limit = _limit.nil? ? nil : _limit - r.count
|
24
|
+
r = r + @backend.send(_fun, element, @query, limit)
|
25
|
+
break r if !_limit.nil? && r.length >= _limit
|
26
|
+
next r
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
data/lib/pincers/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pincers
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ignacio Baixas
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-08-
|
11
|
+
date: 2015-08-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: selenium-webdriver
|
@@ -224,6 +224,7 @@ files:
|
|
224
224
|
- lib/pincers/factory.rb
|
225
225
|
- lib/pincers/support/configuration.rb
|
226
226
|
- lib/pincers/support/cookie_jar.rb
|
227
|
+
- lib/pincers/support/query.rb
|
227
228
|
- lib/pincers/version.rb
|
228
229
|
- lib/pincers.rb
|
229
230
|
homepage: https://github.com/platanus/pincers
|