[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