capybara-ui 0.10.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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/lib/capybara-ui.rb +31 -0
  3. data/lib/capybara-ui/assertions.rb +19 -0
  4. data/lib/capybara-ui/capybara.rb +7 -0
  5. data/lib/capybara-ui/checkpoint.rb +111 -0
  6. data/lib/capybara-ui/conversions.rb +31 -0
  7. data/lib/capybara-ui/cucumber.rb +5 -0
  8. data/lib/capybara-ui/dsl.rb +107 -0
  9. data/lib/capybara-ui/instance_conversions.rb +19 -0
  10. data/lib/capybara-ui/matchers.rb +28 -0
  11. data/lib/capybara-ui/optional_dependencies.rb +5 -0
  12. data/lib/capybara-ui/rails.rb +5 -0
  13. data/lib/capybara-ui/rails/role.rb +9 -0
  14. data/lib/capybara-ui/role.rb +19 -0
  15. data/lib/capybara-ui/text_table.rb +107 -0
  16. data/lib/capybara-ui/text_table/cell_text.rb +7 -0
  17. data/lib/capybara-ui/text_table/mapping.rb +40 -0
  18. data/lib/capybara-ui/text_table/transformations.rb +13 -0
  19. data/lib/capybara-ui/text_table/void_mapping.rb +8 -0
  20. data/lib/capybara-ui/version.rb +3 -0
  21. data/lib/capybara-ui/widgets.rb +61 -0
  22. data/lib/capybara-ui/widgets/check_box.rb +26 -0
  23. data/lib/capybara-ui/widgets/cucumber_methods.rb +73 -0
  24. data/lib/capybara-ui/widgets/document.rb +19 -0
  25. data/lib/capybara-ui/widgets/dsl.rb +47 -0
  26. data/lib/capybara-ui/widgets/field.rb +22 -0
  27. data/lib/capybara-ui/widgets/field_group.rb +329 -0
  28. data/lib/capybara-ui/widgets/form.rb +26 -0
  29. data/lib/capybara-ui/widgets/list.rb +200 -0
  30. data/lib/capybara-ui/widgets/list_item.rb +22 -0
  31. data/lib/capybara-ui/widgets/parts/container.rb +46 -0
  32. data/lib/capybara-ui/widgets/parts/struct.rb +117 -0
  33. data/lib/capybara-ui/widgets/radio_button.rb +62 -0
  34. data/lib/capybara-ui/widgets/select.rb +57 -0
  35. data/lib/capybara-ui/widgets/string_value.rb +43 -0
  36. data/lib/capybara-ui/widgets/table.rb +76 -0
  37. data/lib/capybara-ui/widgets/text_field.rb +27 -0
  38. data/lib/capybara-ui/widgets/widget.rb +392 -0
  39. data/lib/capybara-ui/widgets/widget/node_filter.rb +48 -0
  40. data/lib/capybara-ui/widgets/widget_class.rb +11 -0
  41. data/lib/capybara-ui/widgets/widget_name.rb +56 -0
  42. metadata +240 -0
@@ -0,0 +1,57 @@
1
+ module CapybaraUI
2
+ # A select.
3
+ class Select < Field
4
+ def selected
5
+ root.all(:xpath, ".//option", visible: true).select(&:selected?).first
6
+ end
7
+
8
+ module Selectable
9
+ def select
10
+ root.select_option
11
+ end
12
+ end
13
+
14
+ widget :option, -> (opt) {
15
+ opt.is_a?(Regexp) ? ["option", text: opt] : [:option, opt]
16
+ } do
17
+ include Selectable
18
+ end
19
+
20
+ widget :option_by_value, -> (opt) { "option[value = #{opt.inspect}]" } do
21
+ include Selectable
22
+ end
23
+
24
+ # @return [String] The text of the selected option.
25
+ def get
26
+ selected.text unless selected.nil?
27
+ end
28
+
29
+ # @return [String] The value of the selected option.
30
+ def value
31
+ selected.value unless selected.nil?
32
+ end
33
+
34
+ # Selects the given +option+.
35
+ #
36
+ # You may pass in the option text or value.
37
+ def set(option)
38
+ widget(:option, option).select
39
+ rescue
40
+ begin
41
+ widget(:option_by_value, option).select
42
+ rescue CapybaraUI::MissingWidget => e
43
+ raise InvalidOption.new(e.message).
44
+ tap { |x| x.set_backtrace e.backtrace }
45
+ end
46
+ end
47
+
48
+ # @!method to_s
49
+ # @return the text of the selected option, or the empty string if
50
+ # no option is selected.
51
+ def_delegator :get, :to_s
52
+
53
+ def to_cell
54
+ get
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,43 @@
1
+ module CapybaraUI
2
+ class StringValue < String
3
+ def to_date(format = nil)
4
+ format ? Date.strptime(self, format) : super()
5
+ end
6
+
7
+ def to_key
8
+ fst, rest = first, self[1..-1]
9
+ decamelized = fst + rest.gsub(/([A-Z])/, '_\1')
10
+ underscored = decamelized.gsub(/[\W_]+/, '_')
11
+ stripped = underscored.gsub(/^_|_$/, '')
12
+ downcased = stripped.downcase
13
+ key = downcased.to_sym
14
+
15
+ key
16
+ end
17
+
18
+ class Money
19
+ extend Forwardable
20
+
21
+ delegate %w(to_i to_f) => :str
22
+
23
+ def initialize(str)
24
+ fail ArgumentError, "can't convert `#{str}` to money" \
25
+ unless str =~ /^-?\$\d+(?:,\d{3})*(?:\.\d+)?/
26
+
27
+ @str = (str =~ /^-/ ? '-' : '') + str.gsub(/^-?\$|,/, '')
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :str
33
+ end
34
+
35
+ def to_usd
36
+ Money.new(self)
37
+ end
38
+
39
+ def to_split
40
+ split(',').map(&:strip).map { |e| self.class.new(e) }
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,76 @@
1
+ module CapybaraUI
2
+ class Table < CapybaraUI::Widget
3
+ root 'table'
4
+
5
+ class Row < CapybaraUI::List
6
+ def self.column(*args, &block)
7
+ item(*args, &block)
8
+ end
9
+ end
10
+
11
+ def self.header_row(selector, &block)
12
+ widget :header_row, selector, Row, &block
13
+ end
14
+
15
+ header_row 'thead tr' do
16
+ column 'th'
17
+ end
18
+
19
+ def self.data_row(selector, &block)
20
+ widget :data_row, selector, Row, &block
21
+ end
22
+
23
+ data_row 'tbody tr' do
24
+ column 'td'
25
+ end
26
+
27
+ class Columns
28
+ include Enumerable
29
+
30
+ def initialize(parent)
31
+ @parent = parent
32
+ end
33
+
34
+ def [](header_or_index)
35
+ case header_or_index
36
+ when Integer
37
+ values_by_index(header_or_index)
38
+ when String
39
+ values_by_header(header_or_index)
40
+ else
41
+ raise TypeError,
42
+ "can't convert #{header_or_index.inspect} to Integer or String"
43
+ end
44
+ end
45
+
46
+ def each(&block)
47
+ parent.each(&block)
48
+ end
49
+
50
+ private
51
+
52
+ attr_reader :parent
53
+
54
+ def values_by_index(index)
55
+ parent.rows.transpose[index]
56
+ end
57
+
58
+ def values_by_header(header)
59
+ values_by_index(find_header_index(header))
60
+ end
61
+
62
+ def find_header_index(header)
63
+ parent.widget(:header_row).value.find_index(header) or
64
+ raise ArgumentError, "header not found: #{header.inspect}"
65
+ end
66
+ end
67
+
68
+ def columns
69
+ Columns.new(self)
70
+ end
71
+
72
+ def rows
73
+ widgets(:data_row).map(&:value)
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,27 @@
1
+ module CapybaraUI
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
+
23
+ def content?
24
+ get.respond_to?(:empty?) ? ! get.empty? : !! get
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,392 @@
1
+ module CapybaraUI
2
+ class Widget
3
+ extend Forwardable
4
+ extend Widgets::DSL
5
+
6
+ include WidgetParts::Struct
7
+ include WidgetParts::Container
8
+ include CucumberMethods
9
+
10
+ class Removed < StandardError; end
11
+
12
+ attr_reader :root
13
+
14
+ # @!group Widget macros
15
+
16
+ # Defines a new action.
17
+ #
18
+ # This is a shortcut to help defining a widget and a method that clicks
19
+ # on that widget. You can then send a widget instance the message given
20
+ # by +name+.
21
+ #
22
+ # You can access the underlying widget by appending "_widget" to the
23
+ # action name.
24
+ #
25
+ # @example
26
+ # # Consider the widget will encapsulate the following HTML
27
+ # #
28
+ # # <div id="profile">
29
+ # # <a href="/profiles/1/edit" rel="edit">Edit</a>
30
+ # # </div>
31
+ # class PirateProfile < CapybaraUI::Widget
32
+ # root "#profile"
33
+ #
34
+ # # Declare the action
35
+ # action :edit, '[rel = edit]'
36
+ # end
37
+ #
38
+ # pirate_profile = widget(:pirate_profile)
39
+ #
40
+ # # Access the action widget
41
+ # action_widget = pirate_profile.widget(:edit_widget)
42
+ # action_widget = pirate_profile.edit_widget
43
+ #
44
+ # # Click the link
45
+ # pirate_profile.edit
46
+ #
47
+ # @param name the name of the action
48
+ # @param selector the selector for the widget that will be clicked
49
+ def self.action(name, selector = nil)
50
+ block = if selector
51
+ wname = :"#{name}_widget"
52
+
53
+ widget wname, selector
54
+
55
+ -> { widget(wname).click; self }
56
+ else
57
+ -> { click; self }
58
+ end
59
+
60
+ define_method name, &block
61
+ end
62
+
63
+ # Creates a delegator for one child widget message.
64
+ #
65
+ # Since widgets are accessed through {WidgetParts::Container#widget}, we
66
+ # can't use {Forwardable} to delegate messages to widgets.
67
+ #
68
+ # @param name the name of the receiver child widget
69
+ # @param widget_message the name of the message to be sent to the child widget
70
+ # @param method_name the name of the delegator. If +nil+ the method will
71
+ # have the same name as the message it will send.
72
+ def self.widget_delegator(name, widget_message, method_name = nil)
73
+ method_name = method_name || widget_message
74
+
75
+ class_eval <<-RUBY
76
+ def #{method_name}(*args)
77
+ if args.size == 1
78
+ widget(:#{name}).#{widget_message} args.first
79
+ else
80
+ widget(:#{name}).#{widget_message} *args
81
+ end
82
+ end
83
+ RUBY
84
+ end
85
+
86
+ # @!endgroup
87
+
88
+ # Finds a single instance of the current widget in +node+.
89
+ #
90
+ # @param node the node we want to search in
91
+ #
92
+ # @return a new instance of the current widget class.
93
+ #
94
+ # @raise [Capybara::ElementNotFoundError] if the widget can't be found
95
+ def self.find_in(parent, *args)
96
+ new(filter.node(parent, *args))
97
+ rescue Capybara::Ambiguous => e
98
+ raise AmbiguousWidget.new(e.message).
99
+ tap { |x| x.set_backtrace e.backtrace }
100
+ rescue Capybara::ElementNotFound => e
101
+ raise MissingWidget.new(e.message).
102
+ tap { |x| x.set_backtrace e.backtrace }
103
+ end
104
+
105
+ def self.find_all_in(parent, *args)
106
+ filter.nodes(parent, *args).map { |e| new(e) }
107
+ end
108
+
109
+ # Determines if an instance of this widget class exists in
110
+ # +parent_node+.
111
+ #
112
+ # @param parent_node [Capybara::Node] the node we want to search in
113
+ #
114
+ # @return +true+ if a widget instance is found, +false+ otherwise.
115
+ def self.present_in?(parent, *args)
116
+ filter.node?(parent, *args)
117
+ end
118
+
119
+ def self.not_present_in?(parent, *args)
120
+ filter.nodeless?(parent, *args)
121
+ end
122
+
123
+ # Sets this widget's default selector.
124
+ #
125
+ # You can pass more than one argument to it, or a single Array. Any valid
126
+ # Capybara selector accepted by Capybara::Node::Finders#find will work.
127
+ #
128
+ # === Examples
129
+ #
130
+ # Most of the time, your selectors will be Strings:
131
+ #
132
+ # class MyWidget < CapybaraUI::Widget
133
+ # root '.selector'
134
+ # end
135
+ #
136
+ # This will match any element with a class of "selector". For example:
137
+ #
138
+ # <span class="selector">Pick me!</span>
139
+ #
140
+ # ==== Composite selectors
141
+ #
142
+ # If you're using CSS as the query language, it's useful to be able to use
143
+ # +text: 'Some text'+ to zero in on a specific node:
144
+ #
145
+ # class MySpecificWidget < CapybaraUI::Widget
146
+ # root '.selector', text: 'Pick me!'
147
+ # end
148
+ #
149
+ # This is especially useful, e.g., when you want to create a widget
150
+ # to match a specific error or notification:
151
+ #
152
+ # class NoFreeSpace < CapybaraUI::Widget
153
+ # root '.error', text: 'No free space left!'
154
+ # end
155
+ #
156
+ # So, given the following HTML:
157
+ #
158
+ # <body>
159
+ # <div class="error">No free space left!</div>
160
+ #
161
+ # <!-- ... -->
162
+ # </body>
163
+ #
164
+ # You can test for the error's present using the following code:
165
+ #
166
+ # document.visible?(:no_free_space) #=> true
167
+ #
168
+ # Note: When you want to match text, consider using +I18n.t+ instead of
169
+ # hard-coding the text, so that your tests don't break when the text changes.
170
+ #
171
+ # Finally, you may want to override the query language:
172
+ #
173
+ # class MyWidgetUsesXPath < CapybaraUI::Widget
174
+ # root :xpath, '//some/node'
175
+ # end
176
+ def self.root(*selector, &block)
177
+ @filter = NodeFilter.new(block || selector)
178
+ end
179
+
180
+ class MissingSelector < StandardError
181
+ end
182
+
183
+ def self.filter
184
+ @filter || superclass.filter
185
+ rescue NoMethodError
186
+ raise MissingSelector, 'no selector defined'
187
+ end
188
+
189
+ def self.filter?
190
+ filter rescue false
191
+ end
192
+
193
+ def self.selector
194
+ filter.selector
195
+ end
196
+
197
+ def initialize(root)
198
+ @root = root
199
+ end
200
+
201
+ # Clicks the current widget, or the child widget given by +name+.
202
+ #
203
+ # === Usage
204
+ #
205
+ # Given the following widget definition:
206
+ #
207
+ # class Container < CapybaraUI::Widget
208
+ # root '#container'
209
+ #
210
+ # widget :link, 'a'
211
+ # end
212
+ #
213
+ # Send +click+ with no arguments to trigger a +click+ event on +#container+.
214
+ #
215
+ # widget(:container).click
216
+ #
217
+ # This is the equivalent of doing the following using Capybara:
218
+ #
219
+ # find('#container').click
220
+ #
221
+ # Send +click :link+ to trigger a +click+ event on +a+:
222
+ #
223
+ # widget(:container).click :link
224
+ #
225
+ # This is the equivalent of doing the following using Capybara:
226
+ #
227
+ # find('#container a').click
228
+ def click(*args)
229
+ if args.empty?
230
+ root.click
231
+ else
232
+ widget(*args).click
233
+ end
234
+ end
235
+
236
+ # Hovers over the current widget, or the child widget given by +name+.
237
+ #
238
+ # === Usage
239
+ #
240
+ # Given the following widget definition:
241
+ #
242
+ # class Container < CapybaraUI::Widget
243
+ # root '#container'
244
+ #
245
+ # widget :link, 'a'
246
+ # end
247
+ #
248
+ # Send +hover+ with no arguments to trigger a +hover+ event on +#container+.
249
+ #
250
+ # widget(:container).hover
251
+ #
252
+ # This is the equivalent of doing the following using Capybara:
253
+ #
254
+ # find('#container').hover
255
+ #
256
+ # Send +hover :link+ to trigger a +hover+ event on +a+:
257
+ #
258
+ # widget(:container).hover :link
259
+ #
260
+ # This is the equivalent of doing the following using Capybara:
261
+ #
262
+ # find('#container a').hover
263
+ def hover(*args)
264
+ if args.empty?
265
+ root.hover
266
+ else
267
+ widget(*args).hover
268
+ end
269
+ end
270
+
271
+ # Double clicks the current widget, or the child widget given by +name+.
272
+ #
273
+ # === Usage
274
+ #
275
+ # Given the following widget definition:
276
+ #
277
+ # class Container < CapybaraUI::Widget
278
+ # root '#container'
279
+ #
280
+ # widget :link, 'a'
281
+ # end
282
+ #
283
+ # Send +double_click+ with no arguments to trigger an +ondblclick+ event on +#container+.
284
+ #
285
+ # widget(:container).double_click
286
+ #
287
+ # This is the equivalent of doing the following using Capybara:
288
+ #
289
+ # find('#container').double_click
290
+ def double_click(*args)
291
+ if args.empty?
292
+ root.double_click
293
+ else
294
+ widget(*args).double_click
295
+ end
296
+ end
297
+
298
+ # Right clicks the current widget, or the child widget given by +name+.
299
+ #
300
+ # === Usage
301
+ #
302
+ # Given the following widget definition:
303
+ #
304
+ # class Container < CapybaraUI::Widget
305
+ # root '#container'
306
+ #
307
+ # widget :link, 'a'
308
+ # end
309
+ #
310
+ # Send +right_click+ with no arguments to trigger an +oncontextmenu+ event on +#container+.
311
+ #
312
+ # widget(:container).right_click
313
+ #
314
+ # This is the equivalent of doing the following using Capybara:
315
+ #
316
+ # find('#container').right_click
317
+ def right_click(*args)
318
+ if args.empty?
319
+ root.right_click
320
+ else
321
+ widget(*args).right_click
322
+ end
323
+ end
324
+
325
+ # Determines if the widget underlying an action exists.
326
+ #
327
+ # @param name the name of the action
328
+ #
329
+ # @raise Missing if an action with +name+ can't be found.
330
+ #
331
+ # @return [Boolean] +true+ if the action widget is found, +false+
332
+ # otherwise.
333
+ def has_action?(name)
334
+ raise Missing, "couldn't find `#{name}' action" unless respond_to?(name)
335
+
336
+ visible?(:"#{name}_widget")
337
+ end
338
+
339
+ def id
340
+ root['id']
341
+ end
342
+
343
+ def classes
344
+ root['class'].split
345
+ end
346
+
347
+ # Determines if the widget has a specific class
348
+ #
349
+ # @param name the name of the class
350
+ #
351
+ # @return [Boolean] +true+ if the class is found, +false+ otherwise
352
+ def class?(name)
353
+ classes.include?(name)
354
+ end
355
+
356
+ def html
357
+ xml = Nokogiri::HTML(page.body).at(root.path).to_xml
358
+
359
+ Nokogiri::XML(xml, &:noblanks).to_xhtml.gsub("\n", "")
360
+ end
361
+
362
+ def text
363
+ StringValue.new(root.text.strip)
364
+ end
365
+
366
+ # Converts this widget into a string representation suitable to be displayed
367
+ # in a Cucumber table cell. By default calls #text.
368
+ #
369
+ # This method will be called by methods that build tables or rows (usually
370
+ # #to_table or #to_row) so, in general, you won't call it directly, but feel
371
+ # free to override it when needed.
372
+ #
373
+ # Returns a String.
374
+ def to_cell
375
+ text
376
+ end
377
+
378
+ def to_s
379
+ text
380
+ end
381
+
382
+ def value
383
+ text
384
+ end
385
+
386
+ private
387
+
388
+ def page
389
+ Capybara.current_session
390
+ end
391
+ end
392
+ end