dill 0.5.2 → 0.6.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.
@@ -11,5 +11,14 @@ module Dill
11
11
  set attributes
12
12
  submit
13
13
  end
14
+
15
+ def to_table
16
+ info = self.
17
+ class.
18
+ field_names.
19
+ each_with_object({}) { |e, a| a[e.to_s] = widget(e).to_cell }
20
+
21
+ [info]
22
+ end
14
23
  end
15
24
  end
@@ -78,7 +78,7 @@ module Dill
78
78
  class List < Widget
79
79
  include Enumerable
80
80
 
81
- def_delegators :items, :size, :include?, :each, :empty?, :first, :last
81
+ def_delegators :items, :each, :first, :last
82
82
 
83
83
  class << self
84
84
  # Configures the List item selector and class.
@@ -142,19 +142,53 @@ module Dill
142
142
  end
143
143
 
144
144
  def selector
145
- super ||
146
- begin
147
- root 'ul'
145
+ begin
146
+ super
147
+ rescue Widget::MissingSelector
148
+ root 'ul'
148
149
 
149
- super
150
- end
150
+ super
151
+ end
151
152
  end
152
153
  end
153
154
 
155
+ def count
156
+ items.count
157
+ end
158
+
159
+ # TODO: Convert value to primitive data structures.
160
+ def empty?
161
+ items.empty?
162
+ end
163
+
164
+ def exclude?(element)
165
+ ! include?(element)
166
+ end
167
+
168
+ def include?(element)
169
+ value.include?(element)
170
+ end
171
+
172
+ def length
173
+ items.length
174
+ end
175
+
176
+ def size
177
+ items.size
178
+ end
179
+
180
+ def to_row
181
+ items.map(&:to_cell)
182
+ end
183
+
154
184
  def to_table
155
185
  items.map(&:to_row)
156
186
  end
157
187
 
188
+ def value
189
+ items.map(&:value)
190
+ end
191
+
158
192
  protected
159
193
 
160
194
  def_delegator 'self.class', :item_factory
@@ -0,0 +1,29 @@
1
+ module Dill
2
+ module WidgetParts
3
+ module Container
4
+ def has_no_widget?(name)
5
+ widget(name).absent?
6
+ end
7
+
8
+ def has_widget?(name, *args)
9
+ widget(name, *args).present?
10
+ end
11
+
12
+ def widget(name, *args)
13
+ widget_class(name).find_in(self, *args)
14
+ end
15
+
16
+ private
17
+
18
+ attr_writer :widget_lookup_scope
19
+
20
+ def widget_class(name)
21
+ WidgetName.new(name).to_class(widget_lookup_scope)
22
+ end
23
+
24
+ def widget_lookup_scope
25
+ @widget_lookup_scope || self.class
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,117 @@
1
+ module Dill
2
+ module WidgetParts
3
+ module Struct
4
+ def self.included(target)
5
+ target.extend ClassMethods
6
+ end
7
+
8
+ module ClassMethods
9
+ def attribute(name, selector, &block)
10
+ child = widget(name, selector, &block)
11
+
12
+ class_eval <<-WIDGET
13
+ def #{name}
14
+ widget(:#{name}).value
15
+ end
16
+ WIDGET
17
+
18
+ child
19
+ end
20
+
21
+ def boolean(name, selector, &block)
22
+ child = widget(name, selector, &block)
23
+
24
+ class_eval <<-WIDGET
25
+ def #{name}?
26
+ widget(:#{name}).value
27
+ end
28
+ WIDGET
29
+
30
+ child.class_eval <<-VALUE
31
+ def value
32
+ Dill::Conversions::Boolean(text)
33
+ end
34
+ VALUE
35
+
36
+ child
37
+ end
38
+
39
+ def date(name, selector, &block)
40
+ child = attribute(name, selector, &block)
41
+
42
+ child.class_eval <<-VALUE
43
+ def value
44
+ Date.parse(text)
45
+ end
46
+ VALUE
47
+
48
+ child
49
+ end
50
+
51
+ def float(name, selector, &block)
52
+ child = attribute(name, selector, &block)
53
+
54
+ child.class_eval <<-VALUE
55
+ def value
56
+ Float(text)
57
+ end
58
+ VALUE
59
+
60
+ child
61
+ end
62
+
63
+ def integer(name, selector, &block)
64
+ child = attribute(name, selector, &block)
65
+
66
+ child.class_eval <<-VALUE
67
+ def value
68
+ Integer(text)
69
+ end
70
+ VALUE
71
+
72
+ child
73
+ end
74
+
75
+ def list(name, selector, options = {}, &block)
76
+ child = widget(name, selector, Dill::List) do
77
+ item options[:item_selector], options[:item_class] || ListItem
78
+ end
79
+
80
+ class_eval <<-WIDGET
81
+ def #{name}
82
+ widget(:#{name}).value
83
+ end
84
+ WIDGET
85
+
86
+ child.class_eval(&block) if block_given?
87
+
88
+ child
89
+ end
90
+
91
+ def string(name, *args, &block)
92
+ child = attribute(name, *args, &block)
93
+
94
+ child.class_eval <<-VALUE
95
+ def value
96
+ text
97
+ end
98
+ VALUE
99
+
100
+ child
101
+ end
102
+
103
+ def time(name, *args, &block)
104
+ child = attribute(name, *args, &block)
105
+
106
+ child.class_eval <<-VALUE
107
+ def value
108
+ Time.parse(text)
109
+ end
110
+ VALUE
111
+
112
+ child
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,29 @@
1
+ module Dill
2
+ # A select.
3
+ class Select < Field
4
+ # @return [String] The text of the selected option.
5
+ def get
6
+ option = root.find('[selected]') rescue nil
7
+
8
+ option && option.text
9
+ end
10
+
11
+ # Selects the given +option+.
12
+ #
13
+ # You may pass in the option text or value.
14
+ def set(option)
15
+ root.
16
+ find(:xpath, "option[@value = '#{option}' or . = '#{option}']").
17
+ select_option
18
+ end
19
+
20
+ # @!method to_s
21
+ # @return the text of the selected option, or the empty string if
22
+ # no option is selected.
23
+ def_delegator :get, :to_s
24
+
25
+ def to_cell
26
+ get
27
+ end
28
+ end
29
+ end
@@ -24,7 +24,7 @@ module Dill
24
24
  attr_writer :header, :transform
25
25
 
26
26
  def node_text(node)
27
- NodeText.new(node)
27
+ node.text.strip
28
28
  end
29
29
 
30
30
  def transform
@@ -0,0 +1,23 @@
1
+ module Dill
2
+ # A text field.
3
+ class TextField < Field
4
+ # @!method get
5
+ # @return The text field value.
6
+ def_delegator :root, :value, :get
7
+
8
+ # @!method set(value)
9
+ # Sets the text field value.
10
+ #
11
+ # @param value [String] the value to set.
12
+ def_delegator :root, :set
13
+
14
+ # @!method to_s
15
+ # @return the text field value, or the empty string if the field is
16
+ # empty.
17
+ def_delegator :get, :to_s
18
+
19
+ def to_cell
20
+ get
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,404 @@
1
+ module Dill
2
+ class Widget
3
+ extend Forwardable
4
+
5
+ include WidgetParts::Struct
6
+ include WidgetParts::Container
7
+
8
+ class Removed < StandardError; end
9
+
10
+ # @!group Widget macros
11
+
12
+ # Defines a new action.
13
+ #
14
+ # This is a shortcut to help defining a widget and a method that clicks
15
+ # on that widget. You can then send a widget instance the message given
16
+ # by +name+.
17
+ #
18
+ # You can access the underlying widget by appending "_widget" to the
19
+ # action name.
20
+ #
21
+ # @example
22
+ # # Consider the widget will encapsulate the following HTML
23
+ # #
24
+ # # <div id="profile">
25
+ # # <a href="/profiles/1/edit" rel="edit">Edit</a>
26
+ # # </div>
27
+ # class PirateProfile < Dill::Widget
28
+ # root "#profile"
29
+ #
30
+ # # Declare the action
31
+ # action :edit, '[rel = edit]'
32
+ # end
33
+ #
34
+ # pirate_profile = widget(:pirate_profile)
35
+ #
36
+ # # Access the action widget
37
+ # action_widget = pirate_profile.widget(:edit_widget)
38
+ # action_widget = pirate_profile.edit_widget
39
+ #
40
+ # # Click the link
41
+ # pirate_profile.edit
42
+ #
43
+ # @param name the name of the action
44
+ # @param selector the selector for the widget that will be clicked
45
+ def self.action(name, selector)
46
+ wname = :"#{name}_widget"
47
+
48
+ widget wname, selector
49
+
50
+ define_method name do
51
+ widget(wname).click
52
+
53
+ self
54
+ end
55
+ end
56
+
57
+ # Declares a new child widget.
58
+ #
59
+ # Child widgets are accessible inside the container widget using the
60
+ # {#widget} message, or by sending a message +name+. They
61
+ # are automatically scoped to the parent widget's root node.
62
+ #
63
+ # @example Defining a widget
64
+ # # Given the following HTML:
65
+ # #
66
+ # # <div id="root">
67
+ # # <span id="child">Child</span>
68
+ # # </div>
69
+ # class Container < Dill::Widget
70
+ # root '#root'
71
+ #
72
+ # widget :my_widget, '#child'
73
+ # end
74
+ #
75
+ # container = widget(:container)
76
+ #
77
+ # # accessing using #widget
78
+ # my_widget = container.widget(:my_widget)
79
+ #
80
+ # # accessing using #my_widget
81
+ # my_widget = container.my_widget
82
+ #
83
+ # @overload widget(name, selector, type = Widget)
84
+ #
85
+ # The most common form, it allows you to pass in a selector as well as a
86
+ # type for the child widget. The selector will override +type+'s
87
+ # root selector, if +type+ has one defined.
88
+ #
89
+ # @param name the child widget's name.
90
+ # @param selector the child widget's selector. You can pass either a
91
+ # String or, if you want to use a composite selector, an Array.
92
+ # @param type the child widget's parent class.
93
+ #
94
+ # @overload widget(name, type)
95
+ #
96
+ # This form allows you to omit +selector+ from the arguments. It will
97
+ # reuse +type+'s root selector.
98
+ #
99
+ # @param name the child widget's name.
100
+ # @param type the child widget's parent class.
101
+ #
102
+ # @raise ArgumentError if +type+ has no root selector defined.
103
+ #
104
+ # @yield A block allowing you to further customize the widget behavior.
105
+ #
106
+ # @see #widget
107
+ def self.widget(name, *rest, &block)
108
+ raise ArgumentError, "`#{name}' is a reserved name" \
109
+ if WidgetParts::Container.instance_methods.include?(name.to_sym)
110
+
111
+ case rest.first
112
+ when Class
113
+ arg_count = rest.size + 1
114
+ raise ArgumentError, "wrong number of arguments (#{arg_count} for 2)" \
115
+ unless arg_count == 2
116
+
117
+ type = rest.first
118
+ raise TypeError, "can't convert `#{type}' to Widget" \
119
+ unless type.methods.include?(:selector)
120
+ raise ArgumentError, "missing root selector for `#{type}'" \
121
+ unless type.selector
122
+
123
+ selector = type.selector
124
+ when String, Array, Proc
125
+ arg_count = rest.size + 1
126
+
127
+ case arg_count
128
+ when 0, 1
129
+ raise ArgumentError, "wrong number of arguments (#{arg_count} for 2)"
130
+ when 2
131
+ selector, type = [*rest, Widget]
132
+ when 3
133
+ selector, type = rest
134
+
135
+ raise TypeError, "can't convert `#{type}' to Widget" \
136
+ unless Class === type
137
+ else
138
+ raise ArgumentError, "wrong number of arguments (#{arg_count} for 3)"
139
+ end
140
+ else
141
+ raise ArgumentError, "unknown method signature: #{rest.inspect}"
142
+ end
143
+
144
+ child = WidgetClass.new(selector, type, &block)
145
+
146
+ const_set(Dill::WidgetName.new(name).to_sym, child)
147
+
148
+ child
149
+ end
150
+
151
+ # Creates a delegator for one child widget message.
152
+ #
153
+ # Since widgets are accessed through {WidgetParts::Container#widget}, we
154
+ # can't use {Forwardable} to delegate messages to widgets.
155
+ #
156
+ # @param name the name of the receiver child widget
157
+ # @param widget_message the name of the message to be sent to the child widget
158
+ # @param method_name the name of the delegator. If +nil+ the method will
159
+ # have the same name as the message it will send.
160
+ def self.widget_delegator(name, widget_message, method_name = nil)
161
+ method_name = method_name || widget_message
162
+
163
+ class_eval <<-RUBY
164
+ def #{method_name}(*args)
165
+ if args.size == 1
166
+ widget(:#{name}).#{widget_message} args.first
167
+ else
168
+ widget(:#{name}).#{widget_message} *args
169
+ end
170
+ end
171
+ RUBY
172
+ end
173
+
174
+ # @!endgroup
175
+
176
+ # Finds a single instance of the current widget in +node+.
177
+ #
178
+ # @param node the node we want to search in
179
+ #
180
+ # @return a new instance of the current widget class.
181
+ #
182
+ # @raise [Capybara::ElementNotFoundError] if the widget can't be found
183
+ def self.find_in(parent, *args)
184
+ new { parent.root.find(*selector(*args)) }
185
+ end
186
+
187
+ # Determines if an instance of this widget class exists in
188
+ # +parent_node+.
189
+ #
190
+ # @param parent_node [Capybara::Node] the node we want to search in
191
+ #
192
+ # @return +true+ if a widget instance is found, +false+ otherwise.
193
+ def self.present_in?(parent)
194
+ find_in(parent).present?
195
+ end
196
+
197
+ # Sets this widget's default selector.
198
+ #
199
+ # You can pass more than one argument to it, or a single Array. Any valid
200
+ # Capybara selector accepted by Capybara::Node::Finders#find will work.
201
+ #
202
+ # === Examples
203
+ #
204
+ # Most of the time, your selectors will be Strings:
205
+ #
206
+ # class MyWidget < Dill::Widget
207
+ # root '.selector'
208
+ # end
209
+ #
210
+ # This will match any element with a class of "selector". For example:
211
+ #
212
+ # <span class="selector">Pick me!</span>
213
+ #
214
+ # ==== Composite selectors
215
+ #
216
+ # If you're using CSS as the query language, it's useful to be able to use
217
+ # +text: 'Some text'+ to zero in on a specific node:
218
+ #
219
+ # class MySpecificWidget < Dill::Widget
220
+ # root '.selector', text: 'Pick me!'
221
+ # end
222
+ #
223
+ # This is especially useful, e.g., when you want to create a widget
224
+ # to match a specific error or notification:
225
+ #
226
+ # class NoFreeSpace < Dill::Widget
227
+ # root '.error', text: 'No free space left!'
228
+ # end
229
+ #
230
+ # So, given the following HTML:
231
+ #
232
+ # <body>
233
+ # <div class="error">No free space left!</div>
234
+ #
235
+ # <!-- ... -->
236
+ # </body>
237
+ #
238
+ # You can test for the error's present using the following code:
239
+ #
240
+ # document.has_widget?(:no_free_space) #=> true
241
+ #
242
+ # Note: When you want to match text, consider using +I18n.t+ instead of
243
+ # hard-coding the text, so that your tests don't break when the text changes.
244
+ #
245
+ # Finally, you may want to override the query language:
246
+ #
247
+ # class MyWidgetUsesXPath < Dill::Widget
248
+ # root :xpath, '//some/node'
249
+ # end
250
+ def self.root(*selector, &block)
251
+ @selector = block ? [block] : selector.flatten
252
+ end
253
+
254
+ class MissingSelector < StandardError
255
+ end
256
+
257
+ # Returns the selector specified with +root+.
258
+ def self.selector(*args)
259
+ if @selector
260
+ fst = @selector.first
261
+
262
+ fst.respond_to?(:call) ? fst.call(*args) : @selector
263
+ else
264
+ if superclass.respond_to?(:selector)
265
+ superclass.selector
266
+ else
267
+ raise MissingSelector, 'no selector defined'
268
+ end
269
+ end
270
+ end
271
+
272
+ def initialize(node = nil, &query)
273
+ self.node = node
274
+ self.query = query
275
+ end
276
+
277
+ # Alias for #gone?
278
+ def absent?
279
+ gone?
280
+ end
281
+
282
+ # Clicks the current widget, or the child widget given by +name+.
283
+ #
284
+ # === Usage
285
+ #
286
+ # Given the following widget definition:
287
+ #
288
+ # class Container < Dill::Widget
289
+ # root '#container'
290
+ #
291
+ # widget :link, 'a'
292
+ # end
293
+ #
294
+ # Send +click+ with no arguments to trigger a +click+ event on +#container+.
295
+ #
296
+ # widget(:container).click
297
+ #
298
+ # This is the equivalent of doing the following using Capybara:
299
+ #
300
+ # find('#container').click
301
+ #
302
+ # Send +click :link+ to trigger a +click+ event on +a+:
303
+ #
304
+ # widget(:container).click :link
305
+ #
306
+ # This is the equivalent of doing the following using Capybara:
307
+ #
308
+ # find('#container a').click
309
+ def click(*args)
310
+ if args.empty?
311
+ root.click
312
+ else
313
+ widget(*args).click
314
+ end
315
+ end
316
+
317
+ # Compares this widget with the given Cucumber +table+.
318
+ #
319
+ # === Example
320
+ #
321
+ # Then(/^some step that takes in a cucumber table$/) do |table|
322
+ # widget(:my_widget).diff table
323
+ # end
324
+ def diff(table, wait_time = Capybara.default_wait_time)
325
+ table.diff!(to_table) || true
326
+ end
327
+
328
+ # Returns +true+ if the widget is not visible, or has been removed from the
329
+ # DOM.
330
+ def gone?
331
+ ! root rescue true
332
+ end
333
+
334
+ # Determines if the widget underlying an action exists.
335
+ #
336
+ # @param name the name of the action
337
+ #
338
+ # @raise Missing if an action with +name+ can't be found.
339
+ #
340
+ # @return [Boolean] +true+ if the action widget is found, +false+
341
+ # otherwise.
342
+ def has_action?(name)
343
+ raise Missing, "couldn't find `#{name}' action" unless respond_to?(name)
344
+
345
+ has_widget?(:"#{name}_widget")
346
+ end
347
+
348
+ def inspect
349
+ inspection = "<!-- #{self.class.name}: -->\n"
350
+
351
+ begin
352
+ root = self.root
353
+ xml = Nokogiri::HTML(page.body).at(root.path).to_xml
354
+
355
+ inspection << Nokogiri::XML(xml, &:noblanks).to_xhtml
356
+ rescue Capybara::NotSupportedByDriverError
357
+ inspection << "<#{root.tag_name}>\n#{to_s}"
358
+ rescue Capybara::ElementNotFound, *page.driver.invalid_element_errors
359
+ "#<DETACHED>"
360
+ end
361
+ end
362
+
363
+ # Returns +true+ if widget is visible.
364
+ def present?
365
+ !! root rescue false
366
+ end
367
+
368
+ def root
369
+ node || query.()
370
+ end
371
+
372
+ def text
373
+ root.text.strip
374
+ end
375
+
376
+ # Converts this widget into a string representation suitable to be displayed
377
+ # in a Cucumber table cell. By default calls #text.
378
+ #
379
+ # This method will be called by methods that build tables or rows (usually
380
+ # #to_table or #to_row) so, in general, you won't call it directly, but feel
381
+ # free to override it when needed.
382
+ #
383
+ # Returns a String.
384
+ def to_cell
385
+ text
386
+ end
387
+
388
+ def to_s
389
+ text
390
+ end
391
+
392
+ def value
393
+ text
394
+ end
395
+
396
+ private
397
+
398
+ attr_accessor :node, :query
399
+
400
+ def page
401
+ Capybara.current_session
402
+ end
403
+ end
404
+ end