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.
- data/lib/dill/checkpoint.rb +117 -0
- data/lib/dill/version.rb +1 -1
- data/lib/dill/widget.rb +170 -36
- data/lib/dill.rb +1 -0
- metadata +19 -18
@@ -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
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
|
-
#
|
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
|
-
|
45
|
+
wname = :"#{name}_widget"
|
46
|
+
|
47
|
+
widget wname, selector
|
35
48
|
|
36
49
|
define_method name do
|
37
|
-
widget(
|
50
|
+
widget(wname).click
|
38
51
|
|
39
52
|
self
|
40
53
|
end
|
41
54
|
end
|
42
55
|
|
43
|
-
# Declares a new
|
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
|
-
#
|
46
|
-
#
|
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
|
-
# @
|
49
|
-
#
|
50
|
-
#
|
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,
|
56
|
-
|
57
|
-
|
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
|
-
|
145
|
+
const_set(Dill::WidgetName.new(name).to_sym, child)
|
61
146
|
|
62
|
-
|
147
|
+
define_method name do
|
148
|
+
widget(name)
|
149
|
+
end
|
63
150
|
end
|
64
151
|
|
65
|
-
# Creates a delegator for one
|
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
|
71
|
-
# @param widget_message the name of the message to be sent to the
|
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
|
-
|
178
|
-
test
|
286
|
+
old_root = root
|
287
|
+
test = ->{ old_root != root }
|
179
288
|
end
|
180
289
|
|
181
|
-
|
182
|
-
|
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
|
-
|
187
|
-
# raised on timeout
|
299
|
+
end
|
188
300
|
|
189
|
-
|
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
metadata
CHANGED
@@ -2,31 +2,15 @@
|
|
2
2
|
name: dill
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 0.4.
|
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-
|
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
|