Some initial infrastructure for UI tests
The uitree module exposes the at-spi tree. We can use it to do functional tests of the UI, by checking if the expected widgets exists, clicking them etc. A simple example of how this can be used is in the test, which runs a window and check that it has the expected button.
This commit is contained in:
parent
5ad6c4e236
commit
e6397cf2db
@ -53,5 +53,6 @@ src/sugar3/event-controller/Makefile
|
||||
src/sugar3/presence/Makefile
|
||||
src/sugar3/datastore/Makefile
|
||||
src/sugar3/dispatch/Makefile
|
||||
src/sugar3/test/Makefile
|
||||
po/Makefile.in
|
||||
])
|
||||
|
@ -1,4 +1,11 @@
|
||||
SUBDIRS = activity bundle event-controller graphics presence datastore dispatch
|
||||
SUBDIRS = activity \
|
||||
bundle \
|
||||
event-controller \
|
||||
graphics \
|
||||
presence \
|
||||
datastore \
|
||||
dispatch \
|
||||
test
|
||||
|
||||
sugardir = $(pythondir)/sugar3
|
||||
sugar_PYTHON = \
|
||||
|
4
src/sugar3/test/Makefile.am
Normal file
4
src/sugar3/test/Makefile.am
Normal file
@ -0,0 +1,4 @@
|
||||
sugardir = $(pythondir)/sugar3/test
|
||||
sugar_PYTHON = \
|
||||
__init__.py \
|
||||
uitree.py
|
0
src/sugar3/test/__init__.py
Normal file
0
src/sugar3/test/__init__.py
Normal file
153
src/sugar3/test/uitree.py
Normal file
153
src/sugar3/test/uitree.py
Normal file
@ -0,0 +1,153 @@
|
||||
# Copyright (C) 2012, Daniel Narvaez
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
"""
|
||||
UNSTABLE.
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
from gi.repository import Atspi
|
||||
|
||||
Atspi.set_timeout(-1, -1)
|
||||
|
||||
def get_root():
|
||||
return Node(Atspi.get_desktop(0))
|
||||
|
||||
def _retry_find(func):
|
||||
def wrapped(*args, **kwargs):
|
||||
result = None
|
||||
n_retries = 1
|
||||
|
||||
while n_retries <= 10:
|
||||
print "Try %d, name=%s role_name=%s" % \
|
||||
(n_retries,
|
||||
kwargs.get("name", None),
|
||||
kwargs.get("role_name", None))
|
||||
|
||||
result = func(*args, **kwargs)
|
||||
expect_none = kwargs.get("expect_none", False)
|
||||
if (not expect_none and result) or \
|
||||
(expect_none and not result):
|
||||
return result
|
||||
|
||||
time.sleep(5)
|
||||
n_retries = n_retries + 1
|
||||
|
||||
get_root().dump()
|
||||
|
||||
return result
|
||||
|
||||
return wrapped
|
||||
|
||||
class Node:
|
||||
def __init__(self, accessible):
|
||||
self._accessible = accessible
|
||||
|
||||
def dump(self):
|
||||
self._crawl_accessible(self, 0)
|
||||
|
||||
def do_action(self, name):
|
||||
for i in range(self._accessible.get_n_actions()):
|
||||
if Atspi.Action.get_name(self._accessible, i) == name:
|
||||
self._accessible.do_action(i)
|
||||
|
||||
def click(self, button=1):
|
||||
point = self._accessible.get_position(Atspi.CoordType.SCREEN)
|
||||
Atspi.generate_mouse_event(point.x, point.y, "b%sc" % button)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._accessible.get_name()
|
||||
|
||||
@property
|
||||
def role_name(self):
|
||||
return self._accessible.get_role_name()
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
return Atspi.Text.get_text(self._accessible, 0, -1)
|
||||
|
||||
def get_children(self):
|
||||
children = []
|
||||
|
||||
for i in range(self._accessible.get_child_count()):
|
||||
child = self._accessible.get_child_at_index(i)
|
||||
|
||||
# We sometimes get none children from atspi
|
||||
if child is not None:
|
||||
children.append(Node(child))
|
||||
|
||||
return children
|
||||
|
||||
@_retry_find
|
||||
def find_children(self, name=None, role_name=None):
|
||||
def predicate(node):
|
||||
return self._predicate(node, name, role_name)
|
||||
|
||||
descendants = []
|
||||
self._find_all_descendants(self, predicate, descendants)
|
||||
if not descendants:
|
||||
return []
|
||||
|
||||
return descendants
|
||||
|
||||
@_retry_find
|
||||
def find_child(self, name=None, role_name=None, expect_none=False):
|
||||
def predicate(node):
|
||||
return self._predicate(node, name, role_name)
|
||||
|
||||
node = self._find_descendant(self, predicate)
|
||||
if node is None:
|
||||
return None
|
||||
|
||||
return node
|
||||
|
||||
def __str__(self):
|
||||
return "[%s | %s]" % (self.name, self.role_name)
|
||||
|
||||
def _predicate(self, node, name, role_name):
|
||||
if name is not None and name != node.name:
|
||||
return False
|
||||
|
||||
if role_name is not None and role_name != node.role_name:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _find_descendant(self, node, predicate):
|
||||
if predicate(node):
|
||||
return node
|
||||
|
||||
for child in node.get_children():
|
||||
descendant = self._find_descendant(child, predicate)
|
||||
if descendant is not None:
|
||||
return descendant
|
||||
|
||||
return None
|
||||
|
||||
def _find_all_descendants(self, node, predicate, matches):
|
||||
if predicate(node):
|
||||
matches.append(node)
|
||||
|
||||
for child in node.get_children():
|
||||
self._find_all_descendants(child, predicate, matches)
|
||||
|
||||
def _crawl_accessible(self, node, depth):
|
||||
print " " * depth + str(node)
|
||||
|
||||
for child in node.get_children():
|
||||
self._crawl_accessible(child, depth + 1)
|
52
tests/test_uitree.py
Normal file
52
tests/test_uitree.py
Normal file
@ -0,0 +1,52 @@
|
||||
# Copyright (C) 2012, Daniel Narvaez
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
import sys
|
||||
import subprocess
|
||||
import unittest
|
||||
|
||||
from sugar3.test import uitree
|
||||
|
||||
class TestUITree(unittest.TestCase):
|
||||
def test_tree(self):
|
||||
process = subprocess.Popen(["python", __file__, "show_window1"])
|
||||
|
||||
try:
|
||||
root = uitree.get_root()
|
||||
window = root.find_child(name="window1", role_name="frame")
|
||||
button = window.find_child(name="button1", role_name="push button")
|
||||
finally:
|
||||
process.terminate()
|
||||
|
||||
self.assertIsNotNone(button)
|
||||
|
||||
def show_window1():
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import GLib
|
||||
|
||||
window = Gtk.Window()
|
||||
window.set_title("window1")
|
||||
|
||||
button = Gtk.Button(label="button1")
|
||||
window.add(button)
|
||||
button.show()
|
||||
|
||||
window.show()
|
||||
|
||||
Gtk.main()
|
||||
|
||||
if __name__ == '__main__':
|
||||
globals()[sys.argv[1]]()
|
Loading…
Reference in New Issue
Block a user