pincers 0.2.1 → 0.3.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 630d90affa2488ca53b79f63643a3257618dfb9f
4
- data.tar.gz: 1b8cf519322c4e69c0f2219a51a1d8e587aecd70
3
+ metadata.gz: 88daefd24fb535d60be1252cb33ca632d6466c0f
4
+ data.tar.gz: abe5bde9ae30c13d6a424268e85606b383179d42
5
5
  SHA512:
6
- metadata.gz: 29398e9d922ad46cab2754b9b249fd5b13588a80d0f8c3ad94208d57df72bf478b6e782da2ba9b9c18bd0f584db66f3f04ae7a10320adb91ca85eadc8b001a16
7
- data.tar.gz: 8008abec35682cf438e7bd7a65a9cdaa3fad368167fb0051fd73b045edfa8ffe8c2f586807d684e33c291b950eb52d1ba6730a5decc66e7488d9aa4911e4f944
6
+ metadata.gz: 2726362f0e0d4dd95bf87c50f795f557ef670007b80990c22f1425a955d23d93796623433b50e0344075fd96260ddfbfbe88988edea541c19caf863894744985
7
+ data.tar.gz: 7d66fd52f27ceb87085d643cb8f0df8aeda19747e23cf69b9bb2811b610c5283aa87d9bfa6cd9f45017303ef6e9b477c56da57a6aeb7e10e4fec5098822605d9
@@ -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.find_elements css: _selector
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.find_elements xpath: _selector
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 = ensure_element _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 = ensure_element _element
82
- _element.click
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 switch_to_frame(_element)
86
- @driver.switch_to.frame _element
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 switch_to_top_frame
90
- @driver.switch_to.default_content
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
- # wait contitions
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 check_present(_elements)
96
- _elements.length > 0
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 check_not_present(_elements)
100
- _elements.length == 0
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
- check_present(_elements) and _elements.first.displayed?
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
- attr_accessor :parent, :elements
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
- elements.first
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 wrap_elements [el] }
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
- wrap_elements Array(elements.send(:[],*args))
72
+ wrap_siblings Array(elements.send(:[],*args))
52
73
  end
53
74
  end
54
75
 
55
76
  def first
56
- if elements.first.nil? then nil else wrap_elements [elements.first] end
77
+ if element.nil? then nil else wrap_siblings [element] end
57
78
  end
58
79
 
59
80
  def first!
60
- first or raise Pincers::EmptySetError.new(self)
81
+ wrap_siblings [element!]
61
82
  end
62
83
 
63
84
  def last
64
- if elements.last.nil? then nil else wrap_elements [elements.last] end
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
- search_with_options _options do
69
- explode_elements { |e| backend.search_by_css e, _selector }
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
- search_with_options _options do
75
- explode_elements { |e| backend.search_by_xpath e, _selector }
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 element!
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
- # Input related
120
+ # input related
98
121
 
99
122
  def set_text(_value)
100
- wrap_errors do
101
- backend.set_element_text element!, _value
102
- end
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 click
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
- backend.click_on_element element!
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 wrap_elements(_elements)
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
- wait_for = _options.delete(:wait)
136
- return wrap_elements _block.call unless wait_for
137
- wrap_elements poll_until(wait_for, _options, &_block)
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 explode_elements
142
- elements.inject([]) do |r, element|
143
- r + yield(element)
144
- end
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 poll_until(_condition, _options, &_search)
148
- check_method = "check_#{_condition}"
149
- raise Pincers::MissingFeatureError.new check_method unless backend.respond_to? check_method
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
- until Time.now > end_time
156
- new_elements = _search.call
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
- raise Pincers::ConditionTimeoutError.new self, _condition
257
+ return false
162
258
  end
163
259
 
164
260
  end
@@ -26,6 +26,14 @@ module Pincers
26
26
 
27
27
  end
28
28
 
29
+ class FrozenSetError < ContextError
30
+
31
+ def initialize(_context)
32
+ super _context, "The set is frozen and cant be modified"
33
+ end
34
+
35
+ end
36
+
29
37
  class EmptySetError < ContextError
30
38
 
31
39
  def initialize(_context)
@@ -5,7 +5,8 @@ module Pincers::Support
5
5
 
6
6
  FIELDS = [
7
7
  [:wait_timeout, 10.0],
8
- [:wait_interval, 0.2]
8
+ [:wait_interval, 0.2],
9
+ [:advanced_mode, false]
9
10
  ];
10
11
 
11
12
  FIELDS.each do |field|
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Pincers
2
- VERSION = "0.2.1"
2
+ VERSION = "0.3.0"
3
3
  end
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.2.1
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-14 00:00:00.000000000 Z
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