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 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