AXElements 0.9.0 → 1.0.0.alpha

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