AXElements 0.9.0 → 1.0.0.alpha

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 (60) hide show
  1. data/.yardopts +0 -4
  2. data/README.markdown +22 -17
  3. data/Rakefile +1 -1
  4. data/ext/accessibility/key_coder/extconf.rb +1 -1
  5. data/ext/accessibility/key_coder/key_coder.c +2 -4
  6. data/lib/accessibility.rb +3 -3
  7. data/lib/accessibility/core.rb +948 -0
  8. data/lib/accessibility/dsl.rb +30 -186
  9. data/lib/accessibility/enumerators.rb +1 -0
  10. data/lib/accessibility/factory.rb +78 -134
  11. data/lib/accessibility/graph.rb +5 -9
  12. data/lib/accessibility/highlighter.rb +86 -0
  13. data/lib/accessibility/{pretty_printer.rb → pp_inspector.rb} +4 -3
  14. data/lib/accessibility/qualifier.rb +3 -5
  15. data/lib/accessibility/screen_recorder.rb +217 -0
  16. data/lib/accessibility/statistics.rb +57 -0
  17. data/lib/accessibility/translator.rb +23 -32
  18. data/lib/accessibility/version.rb +2 -22
  19. data/lib/ax/application.rb +20 -159
  20. data/lib/ax/element.rb +42 -32
  21. data/lib/ax/scroll_area.rb +5 -6
  22. data/lib/ax/systemwide.rb +1 -33
  23. data/lib/ax_elements.rb +1 -9
  24. data/lib/ax_elements/core_graphics_workaround.rb +5 -0
  25. data/lib/ax_elements/nsarray_compat.rb +17 -97
  26. data/lib/ax_elements/vendor/inflection_data.rb +66 -0
  27. data/lib/ax_elements/vendor/inflections.rb +176 -0
  28. data/lib/ax_elements/vendor/inflector.rb +306 -0
  29. data/lib/minitest/ax_elements.rb +180 -0
  30. data/lib/mouse.rb +227 -0
  31. data/lib/rspec/expectations/ax_elements.rb +234 -0
  32. data/rakelib/gem.rake +3 -12
  33. data/rakelib/test.rake +15 -0
  34. data/test/helper.rb +20 -10
  35. data/test/integration/accessibility/test_core.rb +18 -0
  36. data/test/integration/accessibility/test_dsl.rb +40 -38
  37. data/test/integration/accessibility/test_enumerators.rb +1 -0
  38. data/test/integration/accessibility/test_graph.rb +0 -1
  39. data/test/integration/accessibility/test_qualifier.rb +2 -2
  40. data/test/integration/ax/test_application.rb +2 -9
  41. data/test/integration/ax/test_element.rb +0 -40
  42. data/test/integration/minitest/test_ax_elements.rb +89 -0
  43. data/test/integration/rspec/expectations/test_ax_elements.rb +102 -0
  44. data/test/sanity/accessibility/test_factory.rb +2 -2
  45. data/test/sanity/accessibility/test_highlighter.rb +56 -0
  46. data/test/sanity/accessibility/{test_pretty_printer.rb → test_pp_inspector.rb} +9 -9
  47. data/test/sanity/accessibility/test_statistics.rb +57 -0
  48. data/test/sanity/ax/test_application.rb +1 -16
  49. data/test/sanity/ax/test_element.rb +2 -2
  50. data/test/sanity/ax_elements/test_nsobject_inspect.rb +2 -4
  51. data/test/sanity/minitest/test_ax_elements.rb +17 -0
  52. data/test/sanity/rspec/expectations/test_ax_elements.rb +15 -0
  53. data/test/sanity/test_mouse.rb +22 -0
  54. data/test/test_core.rb +454 -0
  55. metadata +44 -69
  56. data/History.markdown +0 -41
  57. data/lib/accessibility/system_info.rb +0 -230
  58. data/lib/ax_elements/active_support_selections.rb +0 -10
  59. data/lib/ax_elements/mri.rb +0 -57
  60. data/test/sanity/accessibility/test_version.rb +0 -15
@@ -0,0 +1,306 @@
1
+ module Accessibility
2
+ ##
3
+ # The Inflector is code borrowed from Active Support.
4
+ #
5
+ # The Inflector transforms words from singular to plural, class names to table names, modularized class names to ones without,
6
+ # and class names to foreign keys. The default inflections for pluralization, singularization, and uncountable words are kept
7
+ # in inflections.rb.
8
+ #
9
+ # The Rails core team has stated patches for the inflections library will not be accepted
10
+ # in order to avoid breaking legacy applications which may be relying on errant inflections.
11
+ # If you discover an incorrect inflection and require it for your application, you'll need
12
+ # to correct it yourself (explained below).
13
+ module Inflector
14
+ extend self
15
+
16
+ # Returns the plural form of the word in the string.
17
+ #
18
+ # Examples:
19
+ # "post".pluralize # => "posts"
20
+ # "octopus".pluralize # => "octopi"
21
+ # "sheep".pluralize # => "sheep"
22
+ # "words".pluralize # => "words"
23
+ # "CamelOctopus".pluralize # => "CamelOctopi"
24
+ def pluralize(word)
25
+ apply_inflections(word, inflections.plurals)
26
+ end
27
+
28
+ # The reverse of +pluralize+, returns the singular form of a word in a string.
29
+ #
30
+ # Examples:
31
+ # "posts".singularize # => "post"
32
+ # "octopi".singularize # => "octopus"
33
+ # "sheep".singularize # => "sheep"
34
+ # "word".singularize # => "word"
35
+ # "CamelOctopi".singularize # => "CamelOctopus"
36
+ def singularize(word)
37
+ apply_inflections(word, inflections.singulars)
38
+ end
39
+
40
+ # By default, +camelize+ converts strings to UpperCamelCase. If the argument to +camelize+
41
+ # is set to <tt>:lower</tt> then +camelize+ produces lowerCamelCase.
42
+ #
43
+ # +camelize+ will also convert '/' to '::' which is useful for converting paths to namespaces.
44
+ #
45
+ # Examples:
46
+ # "active_record".camelize # => "ActiveRecord"
47
+ # "active_record".camelize(:lower) # => "activeRecord"
48
+ # "active_record/errors".camelize # => "ActiveRecord::Errors"
49
+ # "active_record/errors".camelize(:lower) # => "activeRecord::Errors"
50
+ #
51
+ # As a rule of thumb you can think of +camelize+ as the inverse of +underscore+,
52
+ # though there are cases where that does not hold:
53
+ #
54
+ # "SSLError".underscore.camelize # => "SslError"
55
+ def camelize(term, uppercase_first_letter = true)
56
+ string = term.to_s
57
+ if uppercase_first_letter
58
+ string = string.sub(/^[a-z\d]*/) { inflections.acronyms[$&] || $&.capitalize }
59
+ else
60
+ string = string.sub(/^(?:#{inflections.acronym_regex}(?=\b|[A-Z_])|\w)/) { $&.downcase }
61
+ end
62
+ string.gsub(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{inflections.acronyms[$2] || $2.capitalize}" }.gsub('/', '::')
63
+ end
64
+
65
+ # Makes an underscored, lowercase form from the expression in the string.
66
+ #
67
+ # Changes '::' to '/' to convert namespaces to paths.
68
+ #
69
+ # Examples:
70
+ # "ActiveRecord".underscore # => "active_record"
71
+ # "ActiveRecord::Errors".underscore # => active_record/errors
72
+ #
73
+ # As a rule of thumb you can think of +underscore+ as the inverse of +camelize+,
74
+ # though there are cases where that does not hold:
75
+ #
76
+ # "SSLError".underscore.camelize # => "SslError"
77
+ def underscore(camel_cased_word)
78
+ word = camel_cased_word.to_s.dup
79
+ word.gsub!(/::/, '/')
80
+ word.gsub!(/(?:([A-Za-z\d])|^)(#{inflections.acronym_regex})(?=\b|[^a-z])/) { "#{$1}#{$1 && '_'}#{$2.downcase}" }
81
+ word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
82
+ word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
83
+ word.tr!("-", "_")
84
+ word.downcase!
85
+ word
86
+ end
87
+
88
+ # Capitalizes the first word and turns underscores into spaces and strips a
89
+ # trailing "_id", if any. Like +titleize+, this is meant for creating pretty output.
90
+ #
91
+ # Examples:
92
+ # "employee_salary" # => "Employee salary"
93
+ # "author_id" # => "Author"
94
+ def humanize(lower_case_and_underscored_word)
95
+ result = lower_case_and_underscored_word.to_s.dup
96
+ inflections.humans.each { |(rule, replacement)| break if result.gsub!(rule, replacement) }
97
+ result.gsub!(/_id$/, "")
98
+ result.gsub(/(_)?([a-z\d]*)/i) { "#{$1 && ' '}#{inflections.acronyms[$2] || $2.downcase}" }.gsub(/^\w/) { $&.upcase }
99
+ end
100
+
101
+ # Capitalizes all the words and replaces some characters in the string to create
102
+ # a nicer looking title. +titleize+ is meant for creating pretty output. It is not
103
+ # used in the Rails internals.
104
+ #
105
+ # +titleize+ is also aliased as as +titlecase+.
106
+ #
107
+ # Examples:
108
+ # "man from the boondocks".titleize # => "Man From The Boondocks"
109
+ # "x-men: the last stand".titleize # => "X Men: The Last Stand"
110
+ # "TheManWithoutAPast".titleize # => "The Man Without A Past"
111
+ # "raiders_of_the_lost_ark".titleize # => "Raiders Of The Lost Ark"
112
+ def titleize(word)
113
+ humanize(underscore(word)).gsub(/\b('?[a-z])/) { $1.capitalize }
114
+ end
115
+
116
+ # Create the name of a table like Rails does for models to table names. This method
117
+ # uses the +pluralize+ method on the last word in the string.
118
+ #
119
+ # Examples
120
+ # "RawScaledScorer".tableize # => "raw_scaled_scorers"
121
+ # "egg_and_ham".tableize # => "egg_and_hams"
122
+ # "fancyCategory".tableize # => "fancy_categories"
123
+ def tableize(class_name)
124
+ pluralize(underscore(class_name))
125
+ end
126
+
127
+ # Create a class name from a plural table name like Rails does for table names to models.
128
+ # Note that this returns a string and not a Class. (To convert to an actual class
129
+ # follow +classify+ with +constantize+.)
130
+ #
131
+ # Examples:
132
+ # "egg_and_hams".classify # => "EggAndHam"
133
+ # "posts".classify # => "Post"
134
+ #
135
+ # Singular names are not handled correctly:
136
+ # "business".classify # => "Busines"
137
+ def classify(table_name)
138
+ # strip out any leading schema name
139
+ camelize(singularize(table_name.to_s.sub(/.*\./, '')))
140
+ end
141
+
142
+ # Replaces underscores with dashes in the string.
143
+ #
144
+ # Example:
145
+ # "puni_puni" # => "puni-puni"
146
+ def dasherize(underscored_word)
147
+ underscored_word.gsub(/_/, '-')
148
+ end
149
+
150
+ # Removes the module part from the expression in the string:
151
+ #
152
+ # "ActiveRecord::CoreExtensions::String::Inflections".demodulize # => "Inflections"
153
+ # "Inflections".demodulize # => "Inflections"
154
+ #
155
+ # See also +deconstantize+.
156
+ def demodulize(path)
157
+ path = path.to_s
158
+ if i = path.rindex('::')
159
+ path[(i+2)..-1]
160
+ else
161
+ path
162
+ end
163
+ end
164
+
165
+ # Removes the rightmost segment from the constant expression in the string:
166
+ #
167
+ # "Net::HTTP".deconstantize # => "Net"
168
+ # "::Net::HTTP".deconstantize # => "::Net"
169
+ # "String".deconstantize # => ""
170
+ # "::String".deconstantize # => ""
171
+ # "".deconstantize # => ""
172
+ #
173
+ # See also +demodulize+.
174
+ def deconstantize(path)
175
+ path.to_s[0...(path.rindex('::') || 0)] # implementation based on the one in facets' Module#spacename
176
+ end
177
+
178
+ # Creates a foreign key name from a class name.
179
+ # +separate_class_name_and_id_with_underscore+ sets whether
180
+ # the method should put '_' between the name and 'id'.
181
+ #
182
+ # Examples:
183
+ # "Message".foreign_key # => "message_id"
184
+ # "Message".foreign_key(false) # => "messageid"
185
+ # "Admin::Post".foreign_key # => "post_id"
186
+ def foreign_key(class_name, separate_class_name_and_id_with_underscore = true)
187
+ underscore(demodulize(class_name)) + (separate_class_name_and_id_with_underscore ? "_id" : "id")
188
+ end
189
+
190
+ # Tries to find a constant with the name specified in the argument string:
191
+ #
192
+ # "Module".constantize # => Module
193
+ # "Test::Unit".constantize # => Test::Unit
194
+ #
195
+ # The name is assumed to be the one of a top-level constant, no matter whether
196
+ # it starts with "::" or not. No lexical context is taken into account:
197
+ #
198
+ # C = 'outside'
199
+ # module M
200
+ # C = 'inside'
201
+ # C # => 'inside'
202
+ # "C".constantize # => 'outside', same as ::C
203
+ # end
204
+ #
205
+ # NameError is raised when the name is not in CamelCase or the constant is
206
+ # unknown.
207
+ def constantize(camel_cased_word) #:nodoc:
208
+ names = camel_cased_word.split('::')
209
+ names.shift if names.empty? || names.first.empty?
210
+
211
+ constant = Object
212
+ names.each do |name|
213
+ constant = constant.const_defined?(name, false) ? constant.const_get(name) : constant.const_missing(name)
214
+ end
215
+ constant
216
+ end
217
+
218
+ # Tries to find a constant with the name specified in the argument string:
219
+ #
220
+ # "Module".safe_constantize # => Module
221
+ # "Test::Unit".safe_constantize # => Test::Unit
222
+ #
223
+ # The name is assumed to be the one of a top-level constant, no matter whether
224
+ # it starts with "::" or not. No lexical context is taken into account:
225
+ #
226
+ # C = 'outside'
227
+ # module M
228
+ # C = 'inside'
229
+ # C # => 'inside'
230
+ # "C".safe_constantize # => 'outside', same as ::C
231
+ # end
232
+ #
233
+ # nil is returned when the name is not in CamelCase or the constant (or part of it) is
234
+ # unknown.
235
+ #
236
+ # "blargle".safe_constantize # => nil
237
+ # "UnknownModule".safe_constantize # => nil
238
+ # "UnknownModule::Foo::Bar".safe_constantize # => nil
239
+ #
240
+ def safe_constantize(camel_cased_word)
241
+ begin
242
+ constantize(camel_cased_word)
243
+ rescue NameError => e
244
+ raise unless e.message =~ /uninitialized constant #{const_regexp(camel_cased_word)}$/ ||
245
+ e.name.to_s == camel_cased_word.to_s
246
+ rescue ArgumentError => e
247
+ raise unless e.message =~ /not missing constant #{const_regexp(camel_cased_word)}\!$/
248
+ end
249
+ end
250
+
251
+ # Turns a number into an ordinal string used to denote the position in an
252
+ # ordered sequence such as 1st, 2nd, 3rd, 4th.
253
+ #
254
+ # Examples:
255
+ # ordinalize(1) # => "1st"
256
+ # ordinalize(2) # => "2nd"
257
+ # ordinalize(1002) # => "1002nd"
258
+ # ordinalize(1003) # => "1003rd"
259
+ # ordinalize(-11) # => "-11th"
260
+ # ordinalize(-1021) # => "-1021st"
261
+ def ordinalize(number)
262
+ if (11..13).include?(number.to_i.abs % 100)
263
+ "#{number}th"
264
+ else
265
+ case number.to_i.abs % 10
266
+ when 1; "#{number}st"
267
+ when 2; "#{number}nd"
268
+ when 3; "#{number}rd"
269
+ else "#{number}th"
270
+ end
271
+ end
272
+ end
273
+
274
+ private
275
+
276
+ # Mount a regular expression that will match part by part of the constant.
277
+ # For instance, Foo::Bar::Baz will generate Foo(::Bar(::Baz)?)?
278
+ def const_regexp(camel_cased_word) #:nodoc:
279
+ parts = camel_cased_word.split("::")
280
+ last = parts.pop
281
+
282
+ parts.reverse.inject(last) do |acc, part|
283
+ part.empty? ? acc : "#{part}(::#{acc})?"
284
+ end
285
+ end
286
+
287
+ # Applies inflection rules for +singularize+ and +pluralize+.
288
+ #
289
+ # Examples:
290
+ # apply_inflections("post", inflections.plurals) # => "posts"
291
+ # apply_inflections("posts", inflections.singulars) # => "post"
292
+ def apply_inflections(word, rules)
293
+ result = word.to_s.dup
294
+
295
+ if word.empty? || inflections.uncountables.any? { |inflection| result =~ /\b#{inflection}\Z/i }
296
+ result
297
+ else
298
+ rules.each { |(rule, replacement)| break if result.gsub!(rule, replacement) }
299
+ result
300
+ end
301
+ end
302
+ end
303
+ end
304
+
305
+ require 'ax_elements/vendor/inflections'
306
+ require 'ax_elements/vendor/inflection_data'
@@ -0,0 +1,180 @@
1
+ require 'ax/element'
2
+ require 'accessibility/qualifier'
3
+ require 'accessibility/dsl'
4
+
5
+ ##
6
+ # AXElements assertions for MiniTest.
7
+ # [Learn more about minitest.](https://github.com/seattlerb/minitest)
8
+ class MiniTest::Assertions
9
+
10
+ ##
11
+ # Test that an element has a specific child. For example, test
12
+ # that a table has a row with certain contents. You can pass any
13
+ # filters that you normally would during a search, including a block.
14
+ #
15
+ # @example
16
+ #
17
+ # assert_has_child table, :row, static_text: { value: 'Mark' }
18
+ #
19
+ # @param parent [AX::Element]
20
+ # @param kind [#to_s]
21
+ # @param filters [Hash]
22
+ # @yield Optional block used for filtering
23
+ # @return [AX::Element]
24
+ def assert_has_child parent, kind, filters = {}, &block
25
+ msg = message {
26
+ child = ax_search_id kind, filters, block
27
+ "Expected #{parent.inspect} to have #{child} as a child"
28
+ }
29
+ result = ax_check_children parent, kind, filters, block
30
+ refute result.blank?, msg
31
+ result
32
+ end
33
+
34
+ ##
35
+ # Test that an element has a specifc descendent. For example, test
36
+ # that a window contains a specific label. You can pass any filters
37
+ # that you normally would during a search, including a block.
38
+ #
39
+ # @example
40
+ #
41
+ # assert_has_descendent window, :static_text, value: /Cake/
42
+ #
43
+ # @param ancestor [AX::Element]
44
+ # @param kind [#to_s]
45
+ # @param filters [Hash]
46
+ # @yield Optional block used for filtering
47
+ # @return [AX::Element]
48
+ def assert_has_descendent ancestor, kind, filters = {}, &block
49
+ msg = message {
50
+ descendent = ax_search_id kind, filters, block
51
+ "Expected #{ancestor.inspect} to have #{descendent} as a descendent"
52
+ }
53
+ result = ax_check_descendent ancestor, kind, filters, block
54
+ refute result.blank?, msg
55
+ result
56
+ end
57
+ alias_method :assert_has_descendant, :assert_has_descendent
58
+
59
+ ##
60
+ # Test that an element will have a child/descendent soon. This method
61
+ # will block until the element is found or a timeout occurs.
62
+ #
63
+ # This is a minitest front end to using {DSL#wait_for}, so any
64
+ # parameters you would normally pass to that method will work here.
65
+ # This also means that you must include either a `parent` key or an
66
+ # `ancestor` key as one of the filters.
67
+ #
68
+ # @param kind [#to_s]
69
+ # @param filters [Hash]
70
+ # @yield An optional block to be used in the search qualifier
71
+ def assert_shortly_has kind, filters = {}, &block
72
+ # need to know if parent/ancestor now because wait_for eats some keys
73
+ (ancest = filters[:ancestor]) || (parent = filters[:parent])
74
+ msg = message {
75
+ descend = ax_search_id kind, filters, block
76
+ if ancest
77
+ "Expected #{ancest.inspect} to have descendent #{descend} before a timeout occurred"
78
+ else
79
+ "Expected #{parent.inspect} to have child #{descend} before a timeout occurred"
80
+ end
81
+ }
82
+ result = wait_for kind, filters, &block
83
+ refute result.blank?, msg
84
+ result
85
+ end
86
+
87
+ ##
88
+ # Test that an element _does not_ have a specific child. For example,
89
+ # test that a row is no longer in a table. You can pass any filters
90
+ # that you normally would during a search, including a block.
91
+ #
92
+ # @example
93
+ #
94
+ # refute_has_child table, :row, id: 'MyRow'
95
+ #
96
+ # @param parent [AX::Element]
97
+ # @param kind [#to_s]
98
+ # @param filters [Hash]
99
+ # @yield An optional block to be used in the search qualifier
100
+ # @return [nil]
101
+ def refute_has_child parent, kind, filters = {}, &block
102
+ result = ax_check_children parent, kind, filters, block
103
+ msg = message {
104
+ "Expected #{parent.inspect} NOT to have #{result} as a child"
105
+ }
106
+ assert result.blank?, msg
107
+ result
108
+ end
109
+
110
+ ##
111
+ # Test that an element _does not_ have a specific descendent. For
112
+ # example, test that a window does not contain a spinning progress
113
+ # indicator anymore.
114
+ #
115
+ # @example
116
+ #
117
+ # refute_has_descendent window, :busy_indicator
118
+ #
119
+ # @param ancestor [AX::Element]
120
+ # @param kind [#to_s]
121
+ # @param filters [Hash]
122
+ # @yield An optional block to be used in the search qualifier
123
+ # @return [nil,Array()]
124
+ def refute_has_descendent ancestor, kind, filters = {}, &block
125
+ result = ax_check_descendent ancestor, kind, filters, block
126
+ msg = message {
127
+ "Expected #{ancestor.inspect} NOT to have #{result} as a descendent"
128
+ }
129
+ assert result.blank?, msg
130
+ result
131
+ end
132
+ alias_method :refute_has_descendant, :refute_has_descendent
133
+
134
+ ##
135
+ # @todo Does having this assertion make sense? I've only added it
136
+ # for the time being because OCD demands it.
137
+ #
138
+ # Test that an element will NOT have a child/descendent soon. This
139
+ # method will block until the element is found or a timeout occurs.
140
+ #
141
+ # This is a minitest front end to using {DSL#wait_for}, so any
142
+ # parameters you would normally pass to that method will work here.
143
+ # This also means that you must include either a `parent` key or an
144
+ # `ancestor` key as one of the filters.
145
+ #
146
+ # @param kind [#to_s]
147
+ # @param filters [Hash]
148
+ # @yield An optional block to be used in the search qualifier
149
+ # @return [nil]
150
+ def refute_shortly_has kind, filters = {}, &block
151
+ result = wait_for kind, filters, &block
152
+ msg = message {
153
+ if ancest = filters[:ancestor]
154
+ "Expected #{ancest.inspect} NOT to have #{result.inspect} as a descendent"
155
+ else
156
+ parent = filters[:parent]
157
+ "Expected #{parent.inspect} NOT to have #{result.inspect} as a child"
158
+ end
159
+ }
160
+ assert result.blank?, msg
161
+ result
162
+ end
163
+
164
+
165
+ private
166
+
167
+ def ax_search_id kind, filters, block
168
+ Accessibility::Qualifier.new(kind, filters, &block).describe
169
+ end
170
+
171
+ def ax_check_children parent, kind, filters, block
172
+ q = Accessibility::Qualifier.new(kind, filters, &block)
173
+ parent.children.find { |x| q.qualifies? x }
174
+ end
175
+
176
+ def ax_check_descendent ancestor, kind, filters, block
177
+ ancestor.search(kind, filters, &block)
178
+ end
179
+
180
+ end