dill 0.4.1 → 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,117 @@
1
+ module Dill
2
+ # A point in time where some condition, or some set of conditions, should be
3
+ # verified.
4
+ #
5
+ # @see http://rubydoc.info/github/jnicklas/capybara/master/Capybara/Node/Base#synchronize-instance_method,
6
+ # which inspired this class.
7
+ class Checkpoint
8
+ class ConditionNotMet < Capybara::ElementNotFound; end
9
+ class TimeFrozen < StandardError; end
10
+
11
+ # @return the configured wait time, in seconds.
12
+ attr_reader :wait_time
13
+
14
+ # @return the Capybara driver in use.
15
+ def self.driver
16
+ Capybara.current_session.driver
17
+ end
18
+
19
+ # Initializes a new Checkpoint.
20
+ #
21
+ # @param wait_time how long this checkpoint will wait for its conditions to
22
+ # be met, in seconds.
23
+ def initialize(wait_time = Capybara.default_wait_time)
24
+ @wait_time = wait_time
25
+ end
26
+
27
+ # Waits until the condition encapsulated by the block is met.
28
+ #
29
+ # Automatically rescues some exceptions ({Capybara::ElementNotFound}, and
30
+ # driver specific exceptions) until {wait_time} is exceeded. At that point
31
+ # it raises whatever exception was raised in the condition block, or
32
+ # {ConditionNotMet}, if no exception was raised inside the block. However,
33
+ # if +raise_errors+ is set to +false+, returns +false+ instead of
34
+ # propagating any of the automatically rescued exceptions.
35
+ #
36
+ # If an "unknown" exception is raised, it is propagated immediately, without
37
+ # waiting for {wait_time} to expire.
38
+ #
39
+ # If a driver that doesn't support waiting is used, any exception raised is
40
+ # immediately propagated.
41
+ #
42
+ # @param raise_errors [Boolean] whether to propagate exceptions that are
43
+ # "rescuable" when {wait_time} expires.
44
+ #
45
+ # @yield a block encapsulating the condition to be evaluated.
46
+ # @yieldreturn a truthy value, if condition is met, a falsey value otherwise.
47
+ #
48
+ # @return whatever the condition block returns if the condition is
49
+ # successful. If the condition is not met, returns +false+ if
50
+ # +rescue_errors+ is false.
51
+ def wait_until(raise_errors = true, &condition)
52
+ start
53
+
54
+ begin
55
+ yield or raise ConditionNotMet
56
+ rescue *rescuable_errors => e
57
+ if immediate?
58
+ raise e if raise_errors
59
+
60
+ return false
61
+ end
62
+
63
+ if expired?
64
+ raise e if raise_errors
65
+
66
+ return false
67
+ end
68
+
69
+ wait
70
+
71
+ raise TimeFrozen, 'time appears to be frozen' if time_frozen?
72
+
73
+ retry
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ attr_reader :start_time
80
+
81
+ def driver
82
+ self.class.driver
83
+ end
84
+
85
+ def driver_errors
86
+ driver.invalid_element_errors
87
+ end
88
+
89
+ def expired?
90
+ remaining_time > wait_time
91
+ end
92
+
93
+ def immediate?
94
+ ! driver.wait?
95
+ end
96
+
97
+ def remaining_time
98
+ Time.now - start_time
99
+ end
100
+
101
+ def rescuable_errors
102
+ @rescuable_errors ||= [Capybara::ElementNotFound, *driver_errors]
103
+ end
104
+
105
+ def start
106
+ @start_time = Time.now
107
+ end
108
+
109
+ def wait
110
+ sleep 0.05
111
+ end
112
+
113
+ def time_frozen?
114
+ Time.now == start_time
115
+ end
116
+ end
117
+ end
data/lib/dill/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Dill
2
- VERSION = "0.4.1"
2
+ VERSION = "0.4.2"
3
3
  end
data/lib/dill/widget.rb CHANGED
@@ -4,6 +4,8 @@ module Dill
4
4
 
5
5
  include WidgetContainer
6
6
 
7
+ class Removed < StandardError; end
8
+
7
9
  # @!group Widget macros
8
10
 
9
11
  # Defines a new action.
@@ -12,6 +14,9 @@ module Dill
12
14
  # on that widget. You can then send a widget instance the message given
13
15
  # by +name+.
14
16
  #
17
+ # You can access the underlying widget by appending "_widget" to the
18
+ # action name.
19
+ #
15
20
  # @example
16
21
  # # Consider the widget will encapsulate the following HTML
17
22
  # #
@@ -25,50 +30,132 @@ module Dill
25
30
  # action :edit, '[rel = edit]'
26
31
  # end
27
32
  #
33
+ # pirate_profile = widget(:pirate_profile)
34
+ #
35
+ # # Access the action widget
36
+ # action_widget = pirate_profile.widget(:edit_widget)
37
+ # action_widget = pirate_profile.edit_widget
38
+ #
28
39
  # # Click the link
29
- # widget(:pirate_profile).edit
40
+ # pirate_profile.edit
30
41
  #
31
42
  # @param name the name of the action
32
43
  # @param selector the selector for the widget that will be clicked
33
44
  def self.action(name, selector)
34
- widget name, selector
45
+ wname = :"#{name}_widget"
46
+
47
+ widget wname, selector
35
48
 
36
49
  define_method name do
37
- widget(name).click
50
+ widget(wname).click
38
51
 
39
52
  self
40
53
  end
41
54
  end
42
55
 
43
- # Declares a new sub-widget.
56
+ # Declares a new child widget.
57
+ #
58
+ # Child widgets are accessible inside the container widget using the
59
+ # {#widget} message, or by sending a message +name+. They
60
+ # are automatically scoped to the parent widget's root node.
61
+ #
62
+ # @example Defining a widget
63
+ # # Given the following HTML:
64
+ # #
65
+ # # <div id="root">
66
+ # # <span id="child">Child</span>
67
+ # # </div>
68
+ # class Container < Dill::Widget
69
+ # root '#root'
70
+ #
71
+ # widget :my_widget, '#child'
72
+ # end
73
+ #
74
+ # container = widget(:container)
75
+ #
76
+ # # accessing using #widget
77
+ # my_widget = container.widget(:my_widget)
78
+ #
79
+ # # accessing using #my_widget
80
+ # my_widget = container.my_widget
81
+ #
82
+ # @overload widget(name, selector, type = Widget)
83
+ #
84
+ # The most common form, it allows you to pass in a selector as well as a
85
+ # type for the child widget. The selector will override +type+'s
86
+ # root selector, if +type+ has one defined.
44
87
  #
45
- # Sub-widgets are accessible inside the container widget using the
46
- # +widget+ message.
88
+ # @param name the child widget's name.
89
+ # @param selector the child widget's selector.
90
+ # @param type the child widget's parent class.
47
91
  #
48
- # @param name the name of the sub-widget
49
- # @param selector the sub-widget selector
50
- # @param parent [Class] the parent class of the new sub-widget
92
+ # @overload widget(name, type)
93
+ #
94
+ # This form allows you to omit +selector+ from the arguments. It will
95
+ # reuse +type+'s root selector.
96
+ #
97
+ # @param name the child widget's name.
98
+ # @param type the child widget's parent class.
99
+ #
100
+ # @raise ArgumentError if +type+ has no root selector defined.
51
101
  #
52
102
  # @yield A block allowing you to further customize the widget behavior.
53
103
  #
54
104
  # @see #widget
55
- def self.widget(name, selector, parent = Widget, &block)
56
- type = Class.new(parent) {
57
- root selector
58
- }
105
+ def self.widget(name, *rest, &block)
106
+ raise ArgumentError, "`#{name}' is a reserved name" \
107
+ if WidgetContainer.instance_methods.include?(name.to_sym)
108
+
109
+ case rest.first
110
+ when Class
111
+ arg_count = rest.size + 1
112
+ raise ArgumentError, "wrong number of arguments (#{arg_count} for 2)" \
113
+ unless arg_count == 2
114
+
115
+ type = rest.first
116
+ raise TypeError, "can't convert `#{type}' to Widget" \
117
+ unless type.methods.include?(:selector)
118
+ raise ArgumentError, "missing root selector for `#{type}'" \
119
+ unless type.selector
120
+
121
+ selector = type.selector
122
+ when String
123
+ arg_count = rest.size + 1
124
+
125
+ case arg_count
126
+ when 0, 1
127
+ raise ArgumentError, "wrong number of arguments (#{arg_count} for 2)"
128
+ when 2
129
+ selector, type = [*rest, Widget]
130
+ when 3
131
+ selector, type = rest
132
+
133
+ raise TypeError, "can't convert `#{type}' to Widget" \
134
+ unless Class === type
135
+ else
136
+ raise ArgumentError, "wrong number of arguments (#{arg_count} for 3)"
137
+ end
138
+ else
139
+ raise ArgumentError, "unknown method signature: #{rest.inspect}"
140
+ end
141
+
142
+ child = Class.new(type) { root selector }
143
+ child.class_eval(&block) if block_given?
59
144
 
60
- type.class_eval(&block) if block_given?
145
+ const_set(Dill::WidgetName.new(name).to_sym, child)
61
146
 
62
- const_set(Dill::WidgetName.new(name).to_sym, type)
147
+ define_method name do
148
+ widget(name)
149
+ end
63
150
  end
64
151
 
65
- # Creates a delegator for one sub-widget message.
152
+ # Creates a delegator for one child widget message.
66
153
  #
67
154
  # Since widgets are accessed through {WidgetContainer#widget}, we can't
68
155
  # use {Forwardable} to delegate messages to widgets.
69
156
  #
70
- # @param name the name of the receiver sub-widget
71
- # @param widget_message the name of the message to be sent to the sub-widget
157
+ # @param name the name of the receiver child widget
158
+ # @param widget_message the name of the message to be sent to the child widget
72
159
  # @param method_name the name of the delegator. If +nil+ the method will
73
160
  # have the same name as the message it will send.
74
161
  def self.widget_delegator(name, widget_message, method_name = nil)
@@ -87,16 +174,6 @@ module Dill
87
174
 
88
175
  # @!endgroup
89
176
 
90
- # Determines if an instance of this widget class exists in
91
- # +parent_node+.
92
- #
93
- # @param parent_node [Capybara::Node] the node we want to search in
94
- #
95
- # @return +true+ if a widget instance is found, +false+ otherwise.
96
- def self.present_in?(parent_node)
97
- parent_node.has_selector?(selector)
98
- end
99
-
100
177
  # Finds a single instance of the current widget in +node+.
101
178
  #
102
179
  # @param node the node we want to search in
@@ -108,6 +185,16 @@ module Dill
108
185
  new(options.merge(root: node.find(selector)))
109
186
  end
110
187
 
188
+ # Determines if an instance of this widget class exists in
189
+ # +parent_node+.
190
+ #
191
+ # @param parent_node [Capybara::Node] the node we want to search in
192
+ #
193
+ # @return +true+ if a widget instance is found, +false+ otherwise.
194
+ def self.present_in?(parent_node)
195
+ parent_node.has_selector?(selector)
196
+ end
197
+
111
198
  # Sets this widget's default selector.
112
199
  #
113
200
  # @param selector [String] a CSS or XPath query
@@ -129,6 +216,22 @@ module Dill
129
216
  self.root = settings.fetch(:root)
130
217
  end
131
218
 
219
+ # Compares the current widget with +value+, waiting for the comparison
220
+ # to return +true+.
221
+ def ==(value)
222
+ checkpoint.wait_until(false) { cast_to(value) == value }
223
+ end
224
+
225
+ # Compares the current widget with +value+, waiting for the comparison
226
+ # to return +false+.
227
+ def !=(value)
228
+ checkpoint.wait_until(false) { cast_to(value) != value }
229
+ end
230
+
231
+ def checkpoint(wait_time)
232
+ Checkpoint.new(wait_time)
233
+ end
234
+
132
235
  # Determines if the widget underlying an action exists.
133
236
  #
134
237
  # @param name the name of the action
@@ -140,14 +243,18 @@ module Dill
140
243
  def has_action?(name)
141
244
  raise Missing, "couldn't find `#{name}' action" unless respond_to?(name)
142
245
 
143
- has_widget?(name)
246
+ has_widget?(:"#{name}_widget")
144
247
  end
145
248
 
146
249
  def inspect
250
+ root = self.root
251
+
147
252
  xml = Nokogiri::HTML(page.body).at(root.path).to_xml
148
253
 
149
254
  "<!-- #{self.class.name}: -->\n" <<
150
255
  Nokogiri::XML(xml, &:noblanks).to_xhtml
256
+ rescue Capybara::NotSupportedByDriverError
257
+ root.inspect
151
258
  end
152
259
 
153
260
  class Reload < Capybara::ElementNotFound; end
@@ -172,21 +279,31 @@ module Dill
172
279
  # reloaded, +false+ otherwise.
173
280
  #
174
281
  # @return the current widget
282
+ #
283
+ # @see Checkpoint
175
284
  def reload(wait_time = Capybara.default_wait_time, &test)
176
285
  unless test
177
- old_inspect = inspect
178
- test = ->{ old_inspect != inspect }
286
+ old_root = root
287
+ test = ->{ old_root != root }
179
288
  end
180
289
 
181
- root.synchronize(wait_time) do
182
- raise Reload unless test.()
290
+ checkpoint(wait_time).wait_until(false, &test)
291
+
292
+ begin
293
+ root.inspect
294
+ rescue
295
+ raise Removed, "widget was removed"
183
296
  end
184
297
 
185
298
  self
186
- rescue Reload
187
- # raised on timeout
299
+ end
188
300
 
189
- self
301
+ def to_i
302
+ to_s.to_i
303
+ end
304
+
305
+ def to_f
306
+ to_s.to_f
190
307
  end
191
308
 
192
309
  def to_s
@@ -197,6 +314,19 @@ module Dill
197
314
 
198
315
  protected
199
316
 
317
+ def cast_to(value)
318
+ case value
319
+ when Float
320
+ to_f
321
+ when Integer
322
+ to_i
323
+ when String
324
+ to_s
325
+ else
326
+ raise TypeError, "can't convert this widget to `#{klass}'"
327
+ end
328
+ end
329
+
200
330
  def node_text(node)
201
331
  NodeText.new(node)
202
332
  end
@@ -205,6 +335,10 @@ module Dill
205
335
 
206
336
  attr_writer :root
207
337
 
338
+ def checkpoint(wait_time = Capybara.default_wait_time)
339
+ Checkpoint.new(wait_time)
340
+ end
341
+
208
342
  def page
209
343
  Capybara.current_session
210
344
  end
data/lib/dill.rb CHANGED
@@ -2,6 +2,7 @@ require 'chronic'
2
2
  require 'nokogiri'
3
3
  require 'capybara'
4
4
 
5
+ require 'dill/checkpoint'
5
6
  require 'dill/widget_container'
6
7
  require 'dill/conversions'
7
8
  require 'dill/instance_conversions'
metadata CHANGED
@@ -2,31 +2,15 @@
2
2
  name: dill
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.4.1
5
+ version: 0.4.2
6
6
  platform: ruby
7
7
  authors:
8
8
  - David Leal
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-07-30 00:00:00.000000000 Z
12
+ date: 2013-08-05 00:00:00.000000000 Z
13
13
  dependencies:
14
- - !ruby/object:Gem::Dependency
15
- version_requirements: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ! '>='
18
- - !ruby/object:Gem::Version
19
- version: '0'
20
- none: false
21
- name: cucumber
22
- type: :runtime
23
- prerelease: false
24
- requirement: !ruby/object:Gem::Requirement
25
- requirements:
26
- - - ! '>='
27
- - !ruby/object:Gem::Version
28
- version: '0'
29
- none: false
30
14
  - !ruby/object:Gem::Dependency
31
15
  version_requirements: !ruby/object:Gem::Requirement
32
16
  requirements:
@@ -107,6 +91,22 @@ dependencies:
107
91
  - !ruby/object:Gem::Version
108
92
  version: 1.0.0
109
93
  none: false
94
+ - !ruby/object:Gem::Dependency
95
+ version_requirements: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ~>
98
+ - !ruby/object:Gem::Version
99
+ version: 1.3.0
100
+ none: false
101
+ name: poltergeist
102
+ type: :development
103
+ prerelease: false
104
+ requirement: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ~>
107
+ - !ruby/object:Gem::Version
108
+ version: 1.3.0
109
+ none: false
110
110
  description: See https://github.com/mojotech/dill/README.md
111
111
  email:
112
112
  - dleal@mojotech.com
@@ -117,6 +117,7 @@ files:
117
117
  - lib/dill.rb
118
118
  - lib/dill/auto_table.rb
119
119
  - lib/dill/base_table.rb
120
+ - lib/dill/checkpoint.rb
120
121
  - lib/dill/conversions.rb
121
122
  - lib/dill/cucumber.rb
122
123
  - lib/dill/document.rb