capybara-ui 0.10.0 → 1.0.0

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