[Sugar-devel] [PATCH sugar-toolkit-gtk3] Some initial infrastructure for UI tests
Daniel Narvaez
dwnarvaez at gmail.com
Fri Dec 7 11:00:14 EST 2012
From: Daniel Narvaez <dwnarvaez at gmail.com>
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.
---
configure.ac | 1 +
src/sugar3/test/Makefile.am | 4 ++
src/sugar3/test/uitree.py | 153 +++++++++++++++++++++++++++++++++++++++++++
tests/test_uitree.py | 52 +++++++++++++++
4 files changed, 210 insertions(+)
create mode 100644 src/sugar3/test/Makefile.am
create mode 100644 src/sugar3/test/__init__.py
create mode 100644 src/sugar3/test/uitree.py
create mode 100644 tests/test_uitree.py
diff --git a/configure.ac b/configure.ac
index 579754b..fd147f7 100644
--- a/configure.ac
+++ b/configure.ac
@@ -52,5 +52,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
])
diff --git a/src/sugar3/test/Makefile.am b/src/sugar3/test/Makefile.am
new file mode 100644
index 0000000..0748c13
--- /dev/null
+++ b/src/sugar3/test/Makefile.am
@@ -0,0 +1,4 @@
+sugardir = $(pythondir)/sugar3/test
+sugar_PYTHON = \
+ __init__.py \
+ uitree.py
diff --git a/src/sugar3/test/__init__.py b/src/sugar3/test/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/sugar3/test/uitree.py b/src/sugar3/test/uitree.py
new file mode 100644
index 0000000..34611a4
--- /dev/null
+++ b/src/sugar3/test/uitree.py
@@ -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)
diff --git a/tests/test_uitree.py b/tests/test_uitree.py
new file mode 100644
index 0000000..d640e10
--- /dev/null
+++ b/tests/test_uitree.py
@@ -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]]()
--
1.7.10.4
More information about the Sugar-devel
mailing list