[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