3 # Gcalctool Automated Tests
7 # Copyright (c) 1987-2008 Sun Microsystems, Inc.
11 """Gcalctool Automated Tests. This standalone script talks
12 directly with the AT-SPI Registry via its IDL interfaces.
14 It's based on the bug scripts created by Will Walker for accessibility
15 problems found with various applications when interacting with Orca,
16 the screen reader/magnifier.
18 To perform the gcalctool automated tests, follow these steps:
20 1) Run the runtests.py script in a terminal window.
21 Results are written to standard output. For example:
23 runtests.py > output.txt
25 2) Start the play_keystrokes.py script in a terminal window.
26 The input file should be provided on standard input. For example:
28 play_keystrokes.py < input.txt
32 4) Give focus to gcalctool.
34 That's it! The tests will now be automatically run. Currently you will
35 need to type Control-C to terminal the runtest.py script when the
36 play_keystrokes.py script automatically terminates.
46 ORBit
.load_typelib("Accessibility")
47 ORBit
.CORBA
.ORB_init()
50 import Accessibility__POA
53 keystrokeListeners
= []
55 registry
= bonobo
.get_object("OAFIID:Accessibility_Registry:1.0",
56 "Accessibility/Registry")
58 applicationName
= "gcalctool"
64 ########################################################################
66 # Event listener classes for global and keystroke events #
68 ########################################################################
70 class EventListener(Accessibility__POA
.EventListener
):
71 """Registers a callback directly with the AT-SPI Registry for the
72 given event type. Most users of this module will not use this
73 class directly, but will instead use the addEventListener method.
76 def __init__(self
, registry
, callback
, eventType
):
77 self
.registry
= registry
78 self
.callback
= callback
79 self
.eventType
= eventType
91 def queryInterface(self
, repo_id
):
93 if repo_id
== "IDL:Accessibility/EventListener:1.0":
100 self
._default
_POA
().the_POAManager
.activate()
101 self
.registry
.registerGlobalEventListener(self
._this
(),
103 self
.__registered
= True
105 return self
.__registered
108 def deregister(self
):
109 if not self
.__registered
:
111 self
.registry
.deregisterGlobalEventListener(self
._this
(),
113 self
.__registered
= False
116 def notifyEvent(self
, event
):
124 class KeystrokeListener(Accessibility__POA
.DeviceEventListener
):
125 """Registers a callback directly with the AT-SPI Registry for the
126 given keystroke. Most users of this module will not use this
127 class directly, but will instead use the registerKeystrokeListeners
130 def keyEventToString(event
):
131 return ("KEYEVENT: type=%d\n" % event
.type) \
132 + (" hw_code=%d\n" % event
.hw_code
) \
133 + (" modifiers=%d\n" % event
.modifiers
) \
134 + (" event_string=(%s)\n" % event
.event_string
) \
135 + (" is_text=%s\n" % event
.is_text
) \
136 + (" time=%f" % time
.time())
139 keyEventToString
= staticmethod(keyEventToString
)
142 def __init__(self
, registry
, callback
,
143 keyset
, mask
, type, synchronous
, preemptive
, isGlobal
):
144 self
._default
_POA
().the_POAManager
.activate()
146 self
.registry
= registry
147 self
.callback
= callback
151 self
.mode
= Accessibility
.EventListenerMode()
152 self
.mode
.synchronous
= synchronous
153 self
.mode
.preemptive
= preemptive
154 self
.mode
._global
= isGlobal
166 def queryInterface(self
, repo_id
):
168 if repo_id
== "IDL:Accessibility/EventListener:1.0":
175 d
= self
.registry
.getDeviceEventController()
176 if d
.registerKeystrokeListener(self
._this
(), self
.keyset
,
177 self
.mask
, self
.type, self
.mode
):
178 self
.__registered
= True
180 self
.__registered
= False
182 return self
.__registered
185 def deregister(self
):
186 if not self
.__registered
:
188 d
= self
.registry
.getDeviceEventController()
189 d
.deregisterKeystrokeListener(self
._this
(), self
.keyset
,
190 self
.mask
, self
.type)
191 self
.__registered
= False
194 def notifyEvent(self
, event
):
195 """Called by the at-spi registry when a key is pressed or released.
198 - event: an at-spi DeviceEvent
200 Returns True if the event has been consumed.
203 return self
.callback(event
)
210 ########################################################################
212 # Testing functions. #
214 ########################################################################
217 """Starts event notification with the AT-SPI Registry. This method
218 only returns after 'stop' has been called.
225 """Unregisters any event or keystroke listeners registered with
226 the AT-SPI Registry and then stops event notification with the
230 for listener
in (listeners
+ keystrokeListeners
):
231 listener
.deregister()
235 def registerEventListener(callback
, eventType
):
238 listener
= EventListener(registry
, callback
, eventType
)
239 listeners
.append(listener
)
242 def registerKeystrokeListeners(callback
):
243 """Registers a single callback for all possible keystrokes.
246 global keystrokeListeners
248 for i
in range(0, (1 << (Accessibility
.MODIFIER_NUMLOCK
+ 1))):
249 keystrokeListeners
.append(
250 KeystrokeListener(registry
,
254 [Accessibility
.KEY_PRESSED_EVENT
,
255 Accessibility
.KEY_RELEASED_EVENT
],
261 ########################################################################
263 # Helper utilities. #
265 ########################################################################
267 def getStateString(acc
):
268 """Returns a space-delimited string composed of the given object's
269 Accessible state attribute. This is for debug purposes.
273 s
= s
._narrow
(Accessibility
.StateSet
)
274 stateSet
= s
.getStates()
277 if stateSet
.count(Accessibility
.STATE_INVALID
):
278 stateString
+= "INVALID "
279 if stateSet
.count(Accessibility
.STATE_ACTIVE
):
280 stateString
+= "ACTIVE "
281 if stateSet
.count(Accessibility
.STATE_ARMED
):
282 stateString
+= "ARMED "
283 if stateSet
.count(Accessibility
.STATE_BUSY
):
284 stateString
+= "BUSY "
285 if stateSet
.count(Accessibility
.STATE_CHECKED
):
286 stateString
+= "CHECKED "
287 if stateSet
.count(Accessibility
.STATE_COLLAPSED
):
288 stateString
+= "COLLAPSED "
289 if stateSet
.count(Accessibility
.STATE_DEFUNCT
):
290 stateString
+= "DEFUNCT "
291 if stateSet
.count(Accessibility
.STATE_EDITABLE
):
292 stateString
+= "EDITABLE "
293 if stateSet
.count(Accessibility
.STATE_ENABLED
):
294 stateString
+= "ENABLED "
295 if stateSet
.count(Accessibility
.STATE_EXPANDABLE
):
296 stateString
+= "EXPANDABLE "
297 if stateSet
.count(Accessibility
.STATE_EXPANDED
):
298 stateString
+= "EXPANDED "
299 if stateSet
.count(Accessibility
.STATE_FOCUSABLE
):
300 stateString
+= "FOCUSABLE "
301 if stateSet
.count(Accessibility
.STATE_FOCUSED
):
302 stateString
+= "FOCUSED "
303 if stateSet
.count(Accessibility
.STATE_HAS_TOOLTIP
):
304 stateString
+= "HAS_TOOLTIP "
305 if stateSet
.count(Accessibility
.STATE_HORIZONTAL
):
306 stateString
+= "HORIZONTAL "
307 if stateSet
.count(Accessibility
.STATE_ICONIFIED
):
308 stateString
+= "ICONIFIED "
309 if stateSet
.count(Accessibility
.STATE_MODAL
):
310 stateString
+= "MODAL "
311 if stateSet
.count(Accessibility
.STATE_MULTI_LINE
):
312 stateString
+= "MULTI_LINE "
313 if stateSet
.count(Accessibility
.STATE_MULTISELECTABLE
):
314 stateString
+= "MULTISELECTABLE "
315 if stateSet
.count(Accessibility
.STATE_OPAQUE
):
316 stateString
+= "OPAQUE "
317 if stateSet
.count(Accessibility
.STATE_PRESSED
):
318 stateString
+= "PRESSED "
319 if stateSet
.count(Accessibility
.STATE_RESIZABLE
):
320 stateString
+= "RESIZABLE "
321 if stateSet
.count(Accessibility
.STATE_SELECTABLE
):
322 stateString
+= "SELECTABLE "
323 if stateSet
.count(Accessibility
.STATE_SELECTED
):
324 stateString
+= "SELECTED "
325 if stateSet
.count(Accessibility
.STATE_SENSITIVE
):
326 stateString
+= "SENSITIVE "
327 if stateSet
.count(Accessibility
.STATE_SHOWING
):
328 stateString
+= "SHOWING "
329 if stateSet
.count(Accessibility
.STATE_SINGLE_LINE
):
330 stateString
+= "SINGLE_LINE "
331 if stateSet
.count(Accessibility
.STATE_STALE
):
332 stateString
+= "STALE "
333 if stateSet
.count(Accessibility
.STATE_TRANSIENT
):
334 stateString
+= "TRANSIENT "
335 if stateSet
.count(Accessibility
.STATE_VERTICAL
):
336 stateString
+= "VERTICAL "
337 if stateSet
.count(Accessibility
.STATE_VISIBLE
):
338 stateString
+= "VISIBLE "
339 if stateSet
.count(Accessibility
.STATE_MANAGES_DESCENDANTS
):
340 stateString
+= "MANAGES_DESCENDANTS "
341 if stateSet
.count(Accessibility
.STATE_INDETERMINATE
):
342 stateString
+= "INDETERMINATE "
344 return stateString
.strip()
347 def getNameString(acc
):
348 """Return the name string for the given accessible object.
351 - acc: the accessible object
353 Returns the name of this accessible object (or "None" if not set).
357 return "'%s'" % acc
.name
362 def getAccessibleString(acc
):
363 return "name=%s role='%s' state='%s'" \
364 % (getNameString(acc
), acc
.getRoleName(), getStateString(acc
))
367 # List of event types that we are interested in.
374 ## "keyboard:modifiers",
375 ## "object:property-change",
376 ## "object:property-change:accessible-name",
377 ## "object:property-change:accessible-description",
378 ## "object:property-change:accessible-parent",
379 ## "object:state-changed",
380 ## "object:state-changed:focused",
381 ## "object:selection-changed",
382 ## "object:children-changed"
383 ## "object:active-descendant-changed"
384 ## "object:visible-data-changed"
385 ## "object:text-selection-changed",
386 ## "object:text-caret-moved",
387 ## "object:text-changed",
388 "object:text-changed:insert",
389 ## "object:column-inserted",
390 ## "object:row-inserted",
391 ## "object:column-reordered",
392 ## "object:row-reordered",
393 ## "object:column-deleted",
394 ## "object:row-deleted",
395 ## "object:model-changed",
396 ## "object:link-selected",
397 ## "object:bounds-changed",
398 ## "window:minimize",
399 ## "window:maximize",
403 ## "window:deactivate",
410 ## "object:property-change:accessible-table-summary",
411 ## "object:property-change:accessible-table-row-header",
412 ## "object:property-change:accessible-table-column-header",
413 ## "object:property-change:accessible-table-summary",
414 ## "object:property-change:accessible-table-row-description",
415 ## "object:property-change:accessible-table-column-description",
418 ## "window:desktop-create",
419 ## "window:desktop-destroy"
423 def getObjects(root
):
424 """Returns a list of all objects under the given root. Objects
425 are returned in no particular order - this function does a simple
426 tree traversal, ignoring the children of objects which report the
427 MANAGES_DESCENDANTS state is active.
429 NOTE: this will throw an InvalidObjectError exception if the
430 AT-SPI Accessibility_Accessible can no longer be reached via
434 - root: the Accessible object to traverse
436 Returns: a list of all objects under the specified object
439 # The list of object we'll return
443 # Start at the first child of the given object
445 if root
.childCount
<= 0:
448 for i
in range(0, root
.childCount
):
449 child
= root
.getChildAtIndex(i
)
451 objlist
.append(child
)
453 s
= s
._narrow
(Accessibility
.StateSet
)
454 state
= s
.getStates()
455 if (state
.count(Accessibility
.STATE_MANAGES_DESCENDANTS
) == 0) \
456 and (child
.childCount
> 0):
457 objlist
.extend(getObjects(child
))
462 def findByRole(root
, role
):
463 """Returns a list of all objects of a specific role beneath the
466 NOTE: this will throw an InvalidObjectError exception if the
467 AT-SPI Accessibility_Accessible can no longer be reached via
471 - root the Accessible object to traverse
472 - role the string describing the Accessible role of the object
474 Returns a list of descendants of the root that are of the given role.
478 allobjs
= getObjects(root
)
480 if o
.getRoleName() == role
:
485 def getDisplayText(obj
):
486 """Returns the calculator display value.
489 - obj a handle to the calculator display component
491 Returns a string containing the current value of the calculator display.
496 text
= obj
.queryInterface("IDL:Accessibility/Text:1.0")
498 text
= text
._narrow
(Accessibility
.Text
)
499 return text
.getText(0, -1)
502 def notifyEvent(event
):
503 global applicationName
, debug
, display
, lastKeyEvent
, registry
505 # We are interested in two types of events.
507 # 2) object:text-changed:insert
511 # If we get a "window:activate" event for the gcalctool application
512 # then (we we haven't already done it), get a handle to the "edit bar"
513 # gcalctool component. This will contain the calculator display.
514 # If we can't find it, terminate.
516 if event
.type == "window:activate":
517 if isApplication(event
, applicationName
):
518 if (display
is None) and (event
.source
.getRoleName() == "frame"):
519 d
= findByRole(event
.source
, "edit bar")
522 sys
.stderr
.write("Unable to get calculator display.\n")
526 sys
.stderr
.write("Caching display component handle.\n")
530 # 2) object:text-changed:insert
532 # If we get a "object:text-changed:insert" for the gcalctool application
533 # and the source of the event is the display component, then, if the
534 # last event was "Return" (and this isn't the first time), get the
535 # contents of the display and print it to standard output.
537 if event
.type == "object:text-changed:insert":
538 if isApplication(event
, applicationName
):
539 if event
.source
== display
:
540 if lastKeyEvent
== None:
544 sys
.stderr
.write("MATCH: Last key event %s\n" % \
545 lastKeyEvent
.event_string
)
547 if lastKeyEvent
.event_string
== "Return":
549 sys
.stderr
.write("Printing result: %s\n" % \
550 getDisplayText(display
))
551 print getDisplayText(display
)
554 def notifyKeystroke(event
):
555 """Process keyboard events.
558 - event: the keyboard event to process
561 global debug
, lastKeyEvent
563 # print KeystrokeListener.keyEventToString(event)
565 # If this is a "pressed" keyboard event (and not an "F12" event),
566 # print its value to standard output.
568 # This is hopefully make the output the same as the input, to allow
569 # the user to easy determine incorrect results by comparing the two
574 sys
.stderr
.write("notifyKeystroke: %s\n" % event
.event_string
)
575 if (event
.event_string
!= "F12") and \
576 (event
.event_string
!= "SunF37"):
577 print event
.event_string
,
580 # If the user has deliberately hit the F12 key, then terminate the
583 if (event
.event_string
== "F12") or (event
.event_string
== "SunF37"):
589 def shutdownAndExit(signum
=None, frame
=None):
593 def isApplication(event
, appName
):
594 """Check to see if this event is for the desired application, by
595 getting the component at the top of the object hierarchy (which
596 should have a role of "application", and comparing its name against
600 - event: the event to process
601 - appName: the application name to test against
604 parent
= event
.source
606 if parent
.getRoleName() == "application":
608 parent
= parent
.parent
609 if parent
and parent
.name
== appName
:
616 for eventType
in eventTypes
:
617 registerEventListener(notifyEvent
, eventType
)
618 registerKeystrokeListeners(notifyKeystroke
)
622 if __name__
== "__main__":
624 signal
.signal(signal
.SIGINT
, shutdownAndExit
)
625 signal
.signal(signal
.SIGQUIT
, shutdownAndExit
)