More work on bug #488694 - Move ASCII, accuracy, function and constant dialogs into...
[gcalctool.git] / test / runtests.py
blobbbf7ef023b675e5b7e186d21003f4afacd5736c5
1 #!/bin/python
3 # Gcalctool Automated Tests
5 # $Header$
7 # Copyright (c) 1987-2007 Sun Microsystems, Inc.
8 # All Rights Reserved.
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
30 3) Run gcalctool.
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.
37 """
39 import time
40 import bonobo
41 import ORBit
42 import threading
43 import gtk
44 import sys
46 ORBit.load_typelib("Accessibility")
47 ORBit.CORBA.ORB_init()
49 import Accessibility
50 import Accessibility__POA
52 listeners = []
53 keystrokeListeners = []
55 registry = bonobo.get_object("OAFIID:Accessibility_Registry:1.0",
56 "Accessibility/Registry")
58 applicationName = "gcalctool"
59 debug = True
60 display = None
61 lastKeyEvent = None
64 ########################################################################
65 # #
66 # Event listener classes for global and keystroke events #
67 # #
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.
74 """
76 def __init__(self, registry, callback, eventType):
77 self.registry = registry
78 self.callback = callback
79 self.eventType = eventType
80 self.register()
83 def ref(self):
84 pass
87 def unref(self):
88 pass
91 def queryInterface(self, repo_id):
92 thiz = None
93 if repo_id == "IDL:Accessibility/EventListener:1.0":
94 thiz = self._this()
96 return thiz
99 def register(self):
100 self._default_POA().the_POAManager.activate()
101 self.registry.registerGlobalEventListener(self._this(),
102 self.eventType)
103 self.__registered = True
105 return self.__registered
108 def deregister(self):
109 if not self.__registered:
110 return
111 self.registry.deregisterGlobalEventListener(self._this(),
112 self.eventType)
113 self.__registered = False
116 def notifyEvent(self, event):
117 self.callback(event)
120 def __del__(self):
121 self.deregister()
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
128 method."""
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
148 self.keyset = keyset
149 self.mask = mask
150 self.type = type
151 self.mode = Accessibility.EventListenerMode()
152 self.mode.synchronous = synchronous
153 self.mode.preemptive = preemptive
154 self.mode._global = isGlobal
155 self.register()
158 def ref(self):
159 pass
162 def unref(self):
163 pass
166 def queryInterface(self, repo_id):
167 thiz = None
168 if repo_id == "IDL:Accessibility/EventListener:1.0":
169 thiz = self._this()
171 return thiz
174 def register(self):
175 d = self.registry.getDeviceEventController()
176 if d.registerKeystrokeListener(self._this(), self.keyset,
177 self.mask, self.type, self.mode):
178 self.__registered = True
179 else:
180 self.__registered = False
182 return self.__registered
185 def deregister(self):
186 if not self.__registered:
187 return
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.
197 Arguments:
198 - event: an at-spi DeviceEvent
200 Returns True if the event has been consumed.
203 return self.callback(event)
206 def __del__(self):
207 self.deregister()
210 ########################################################################
212 # Testing functions. #
214 ########################################################################
216 def start():
217 """Starts event notification with the AT-SPI Registry. This method
218 only returns after 'stop' has been called.
221 bonobo.main()
224 def stop():
225 """Unregisters any event or keystroke listeners registered with
226 the AT-SPI Registry and then stops event notification with the
227 AT-SPI Registry.
230 for listener in (listeners + keystrokeListeners):
231 listener.deregister()
232 bonobo.main_quit()
235 def registerEventListener(callback, eventType):
236 global listeners
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,
251 callback, # callback
252 [], # keyset
253 i, # modifier mask
254 [Accessibility.KEY_PRESSED_EVENT,
255 Accessibility.KEY_RELEASED_EVENT],
256 True, # synchronous
257 True, # preemptive
258 False)) # global
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.
272 s = acc.getState()
273 s = s._narrow(Accessibility.StateSet)
274 stateSet = s.getStates()
276 stateString = " "
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.
350 Arguments:
351 - acc: the accessible object
353 Returns the name of this accessible object (or "None" if not set).
356 if acc.name:
357 return "'%s'" % acc.name
358 else:
359 return "None"
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.
369 eventTypes = [
370 ## "focus:",
371 ## "mouse:rel",
372 ## "mouse:button",
373 ## "mouse:abs",
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",
400 ## "window:restore",
401 "window:activate",
402 ## "window:create",
403 ## "window:deactivate",
404 ## "window:close",
405 ## "window:lower",
406 ## "window:raise",
407 ## "window:resize",
408 ## "window:shade",
409 ## "window:unshade",
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",
416 ## "object:test",
417 ## "window:restyle",
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
431 CORBA.
433 Arguments:
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
441 objlist = []
443 # Start at the first child of the given object
445 if root.childCount <= 0:
446 return objlist
448 for i in range(0, root.childCount):
449 child = root.getChildAtIndex(i)
450 if child:
451 objlist.append(child)
452 s = child.getState()
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))
459 return objlist
462 def findByRole(root, role):
463 """Returns a list of all objects of a specific role beneath the
464 given root.
466 NOTE: this will throw an InvalidObjectError exception if the
467 AT-SPI Accessibility_Accessible can no longer be reached via
468 CORBA.
470 Arguments:
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.
477 objlist = []
478 allobjs = getObjects(root)
479 for o in allobjs:
480 if o.getRoleName() == role:
481 objlist.append(o)
482 return objlist
485 def getDisplayText(obj):
486 """Returns the calculator display value.
488 Arguments:
489 - obj a handle to the calculator display component
491 Returns a string containing the current value of the calculator display.
494 if not obj:
495 return
496 text = obj.queryInterface("IDL:Accessibility/Text:1.0")
497 if text:
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.
506 # 1) window:activate
507 # 2) object:text-changed:insert
509 # 1) window:activate
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")
521 if len(d) == 0:
522 sys.stderr.write("Unable to get calculator display.\n")
523 shutdownAndExit()
525 if debug:
526 sys.stderr.write("Caching display component handle.\n")
527 display = d[0]
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:
541 return
543 if debug:
544 sys.stderr.write("MATCH: Last key event %s\n" % \
545 lastKeyEvent.event_string)
547 if lastKeyEvent.event_string == "Return":
548 if debug:
549 sys.stderr.write("Printing result: %s\n" % \
550 getDisplayText(display))
551 print getDisplayText(display)
554 def notifyKeystroke(event):
555 """Process keyboard events.
557 Arguments:
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
570 # files.
572 if event.type == 0:
573 if debug:
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,
578 lastKeyEvent = event
580 # If the user has deliberately hit the F12 key, then terminate the
581 # application.
583 if (event.event_string == "F12") or (event.event_string == "SunF37"):
584 shutdownAndExit()
586 return False
589 def shutdownAndExit(signum=None, frame=None):
590 stop()
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
597 the one given.
599 Arguments:
600 - event: the event to process
601 - appName: the application name to test against
604 parent = event.source
605 while parent:
606 if parent.getRoleName() == "application":
607 break
608 parent = parent.parent
609 if parent and parent.name == appName:
610 return True
612 return False
615 def test():
616 for eventType in eventTypes:
617 registerEventListener(notifyEvent, eventType)
618 registerKeystrokeListeners(notifyKeystroke)
619 start()
622 if __name__ == "__main__":
623 import signal
624 signal.signal(signal.SIGINT, shutdownAndExit)
625 signal.signal(signal.SIGQUIT, shutdownAndExit)
626 test()