dill 0.4.1 → 0.4.2

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