[Sugar-devel] [PATCH sugar-toolkit-gtk3] Some initial infrastructure for UI tests

Daniel Narvaez dwnarvaez at gmail.com
Sat Dec 8 09:48:24 EST 2012


Forgot to add the test directory to the makefile, resending.

On 7 December 2012 17:00, Daniel Narvaez <dwnarvaez at gmail.com> wrote:
> 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
>



-- 
Daniel Narvaez


More information about the Sugar-devel mailing list