dill 0.5.2 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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