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