diff --git a/pyface/ui/wx/python_shell.py b/pyface/ui/wx/python_shell.py index 423a8380e..fb10c5de5 100644 --- a/pyface/ui/wx/python_shell.py +++ b/pyface/ui/wx/python_shell.py @@ -13,6 +13,8 @@ """ Enthought pyface package component """ +from __future__ import absolute_import + # Standard library imports. import six.moves.builtins import os diff --git a/pyface/ui/wx/util/__init__.py b/pyface/ui/wx/util/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pyface/ui/wx/util/event_loop_helper.py b/pyface/ui/wx/util/event_loop_helper.py new file mode 100644 index 000000000..3026fe881 --- /dev/null +++ b/pyface/ui/wx/util/event_loop_helper.py @@ -0,0 +1,157 @@ + +# (C) Copyright 2014-15 Enthought, Inc., Austin, TX +# All right reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in enthought/LICENSE.txt and may be redistributed only +# under the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# Code modified from pyface.ui.qt4.util.event_loop_helper +import contextlib +import threading + +import wx +from traits.api import HasStrictTraits, Instance +from pyface.api import GUI +from pyface.util.guisupport import start_event_loop_wx + + +class ConditionTimeoutError(RuntimeError): + pass + + +@contextlib.contextmanager +def dont_quit_when_last_window_closed(wx_app): + """ + Suppress exit of the application when the last window is closed. + + """ + flag = wx_app.GetExitOnFrameDelete() + wx_app.SetExitOnFrameDelete(False) + try: + yield + finally: + wx_app.SetExitOnFrameDelete(flag) + + +class EventLoopHelper(HasStrictTraits): + """ A helper class to provide basic event loop functionality. """ + + #: The gui toolkit application. + gui_app = Instance(wx.App) + + #: The pyface gui implementation + gui = Instance(GUI, ()) + + #: Reference to a dummy wx event handler that will help bind events for + #: the timers. + _handler = Instance(wx.EvtHandler, ()) + + @contextlib.contextmanager + def event_loop(self, repeat=1): + """ Emulates an event loop `repeat` times. + + Parameters + ---------- + repeat : int + Number of times to process events. Default is 1. + + """ + yield + for i in range(repeat): + self.gui_app.ProcessPendingEvents() + + def event_loop_until_condition(self, condition, start=None, timeout=10.0): + """ Runs the real event loop until the provided condition evaluates + to True. + + Raises ConditionTimeoutError if the timeout occurs before the condition + is satisfied. + + Parameters + ---------- + condition : callable + A callable to determine if the stop criteria have been met. This + should accept no arguments. + + start : callable + A callable to use in order to start the event loop. Default is + to create a small frame or reuse the top level window if it is + still available. + + timeout : float + Number of seconds to run the event loop in the case that the trait + change does not occur. + + """ + def handler(event): + if condition(): + self.gui_app.Exit() + + # Make sure we don't get a premature exit from the event loop. + with dont_quit_when_last_window_closed(self.gui_app): + condition_timer = wx.Timer(self._handler) + timeout_timer = wx.Timer(self._handler) + self._handler.Bind(wx.EVT_TIMER, handler, condition_timer) + self._handler.Bind( + wx.EVT_TIMER, + lambda event: self.gui_app.Exit(), + timeout_timer + ) + timeout_timer.Start(int(timeout * 1000), True) + condition_timer.Start(50) + try: + if start is None: + self._start_event_loop() + else: + start() + if not condition(): + raise ConditionTimeoutError( + 'Timed out waiting for condition') + finally: + timeout_timer.Stop() + condition_timer.Stop() + + @contextlib.contextmanager + def delete_window(self, window, timeout=2.0): + """ Runs the real event loop until the window provided has been deleted. + + Parameters + ---------- + window : + The toolkit window widget whose deletion will stop the event loop. + + timeout : float + Number of seconds to run the event loop in the case that the + widget is not deleted. + + Raises + ------ + ConditionTimeoutError: + Raised on timeout. + + """ + timer = wx.Timer(self._handler) + self._handler.Bind( + wx.EVT_TIMER, + lambda event: self.gui_app.Exit(), + timer + ) + yield + timer.Start(int(timeout * 1000), True) + self._start_event_loop() + if not timer.IsRunning(): + # We exited the event loop on timeout + raise ConditionTimeoutError( + 'Could not destroy widget before timeout: {!r}'.format(window)) + + def _start_event_loop(self): + app = self.gui_app + window = app.GetTopWindow() + if window is None: + # The wx EventLoop needs an active Window in order to work. + window = wx.Frame(None, size=(1, 1)) + app.SetTopWindow(window) + window.Show(True) + start_event_loop_wx(app) diff --git a/pyface/ui/wx/util/gui_test_assistant.py b/pyface/ui/wx/util/gui_test_assistant.py new file mode 100644 index 000000000..ca7d233c3 --- /dev/null +++ b/pyface/ui/wx/util/gui_test_assistant.py @@ -0,0 +1,287 @@ +# -------------------------------------------------------------------------- +# +# Copyright (c) 2012-14, Enthought, Inc. +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in /LICENSE.txt and may be redistributed only +# under the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# +# code based on the traits-enaml.testing.gui_test_assistant +# +# --------------------------------------------------------------------------- +import contextlib +import threading + +import wx + +from traits.testing.unittest_tools import UnittestTools +from traits.testing.unittest_tools import _TraitsChangeCollector as \ + TraitsChangeCollector +from pyface.util.guisupport import get_app_wx + +from .event_loop_helper import EventLoopHelper, ConditionTimeoutError + + +def find_widget(start, type_, test=None): + """Recursively walks the widget tree from widget `start` until it + finds a widget of type `type_` (a toolkit widget subclass) that + satisfies the provided `test` method. + + Parameters + ---------- + start : + The toolkit widget from which to start walking the tree + type_ : type + A subclass of toolkit widget to use for an initial type filter + while walking the tree + test : callable + A filter function that takes one argument (the current widget being + evaluated) and returns either True or False to determine if the + widget matches the required criteria. + + """ + if test is None: + def test(): + return True + if isinstance(start, type_): + if test(start): + return start + for child in start.GetChildren(): + widget = find_widget(child, type_, test=test) + if widget: + return widget + return None + + +def print_widget_tree(widget, level=0): + """ Debugging helper to print out the widget tree starting at a + particular `widget`. + + Parameters + ---------- + widget : + The root widget in the tree to print. + level : int + The current level in the tree. Used internally for displaying the + tree level. + """ + level = level + 4 + if level == 0: + print + print ' '*level, widget + for child in widget.GetChildren(): + print_widget_tree(child, level=level) + if level == 0: + print + + +class GuiTestAssistant(UnittestTools): + + def setUp(self): + # Make sure that we have a running application. + self.gui_app = get_app_wx() + wx.Log.SetActiveTarget(wx.LogStderr()) + self.event_loop_helper = EventLoopHelper(gui_app=self.gui_app) + + def tearDown(self): + windows = wx.GetTopLevelWindows() + titles = [] + if len(windows) > 0: + for window in windows: + titles.append(window.GetTitle()) + + def _cleanup(): + for window in wx.GetTopLevelWindows(): + window.Destroy() + wx.WakeUpIdle() + + wx.CallLater(50, _cleanup) + + handler = wx.EvtHandler() + kill_timer = wx.Timer(handler) + handler.Bind( + wx.EVT_TIMER, + lambda event: self.gui_app.Exit(), + kill_timer + ) + kill_timer.Start(30000, True) + try: + self.gui_app.MainLoop() + finally: + kill_timer.Stop() + + del self.event_loop_helper + del self.gui_app + + if len(titles) > 0: + raise RuntimeError( + 'Not all windows closed after test method, {}'.format(titles)) + + @contextlib.contextmanager + def event_loop(self, repeat=1): + """ Artificially replicate the event loop by processing events. + + If the events to be processed place more events in the queue, + begin increasing the value of ``repeat``, or consider using + ``event_loop_until_condition`` instead. + + Parameters + ---------- + repeat : int + Number of times to process events. + + """ + yield + self.event_loop_helper.event_loop(repeat=repeat) + + @contextlib.contextmanager + def delete_window(self, window, timeout=1.0): + """Runs the real event loop until the widget provided has been deleted. + + Parameters + ---------- + widget : + The widget whose deletion will stop the event loop. + + timeout : float + Number of seconds to run the event loop in the case that the + widget is not deleted. + + """ + try: + with self.event_loop_helper.delete_window(window, timeout=timeout): + yield + except ConditionTimeoutError: + self.fail('Could not destroy window before timeout: {!r}'.format( + window)) + + @contextlib.contextmanager + def event_loop_until_condition(self, condition, timeout=10.0): + """Runs the real event loop until the provided condition evaluates + to True. + + This should not be used to wait for widget deletion. Use + delete_widget() instead. + + Parameters + ---------- + condition : callable + A callable to determine if the stop criteria have been met. This + should accept no arguments. + + timeout : float + Number of seconds to run the event loop in the case that the + condition is not satisfied. + + """ + try: + yield + self.event_loop_helper.event_loop_until_condition( + condition, timeout=timeout) + except ConditionTimeoutError: + self.fail('Timed out waiting for condition') + + @contextlib.contextmanager + def assertTraitChangesInEventLoop( + self, obj, trait, condition, count=1, timeout=10.0): + """Runs the real event loop, collecting trait change events until + the provided condition evaluates to True. + + Parameters + ---------- + obj : HasTraits + The HasTraits instance whose trait will change. + + trait : str + The extended trait name of trait changes to listen too. + + condition : callable + A callable to determine if the stop criteria have been met. This + should accept no arguments. + + count : int + The expected number of times the event should be fired. The default + is to expect one event. + + timeout : float + Number of seconds to run the event loop in the case that the trait + change does not occur. + + """ + condition_ = lambda: condition(obj) + collector = TraitsChangeCollector(obj=obj, trait=trait) + + collector.start_collecting() + try: + try: + yield collector + self.event_loop_helper.event_loop_until_condition( + condition_, timeout=timeout) + except ConditionTimeoutError: + actual_event_count = collector.event_count + msg = ("Expected {} event on {} to be fired at least {} " + "times, but the event was only fired {} times " + "before timeout ({} seconds).") + msg = msg.format( + trait, obj, count, actual_event_count, timeout) + self.fail(msg) + finally: + collector.stop_collecting() + + @contextlib.contextmanager + def event_loop_until_traits_change(self, traits_object, *traits, **kw): + """Run the real application event loop until a change notification for + all of the specified traits is received. + + Parameters + ---------- + traits_object: HasTraits instance + The object on which to listen for a trait events + traits: one or more str + The names of the traits to listen to for events + timeout: float, optional, keyword only + Number of seconds to run the event loop in the case that the trait + change does not occur. Default value is 10.0. + + """ + timeout = kw.pop('timeout', 10.0) + condition = threading.Event() + + traits = set(traits) + recorded_changes = set() + + def set_event(trait): + recorded_changes.add(trait) + if recorded_changes == traits: + condition.set() + + handlers = {} + for trait in traits: + handlers[trait] = lambda: set_event(trait) + + for trait, handler in handlers.iteritems(): + traits_object.on_trait_change(handler, trait) + try: + with self.event_loop_until_condition( + condition=condition.is_set, timeout=timeout): + yield + finally: + for trait, handler in handlers.iteritems(): + traits_object.on_trait_change(handler, trait, remove=True) + + @contextlib.contextmanager + def wait_until_window_is_closed(self, timeout=10.0): + """ Wait until the top window has closed and the application exits. + + """ + def condition(self): + return self.gui_app.GetTopWindow() is None + + try: + with self.event_loop_helper.event_loop_until_condition( + condition, timeout=timeout): + yield + except ConditionTimeoutError: + self.fail('Timed out waiting for the application to exit') diff --git a/pyface/ui/wx/util/helpers.py b/pyface/ui/wx/util/helpers.py new file mode 100644 index 000000000..957c7c30a --- /dev/null +++ b/pyface/ui/wx/util/helpers.py @@ -0,0 +1,63 @@ +from contextlib import contextmanager +import os +from threading import Timer, Event +import sys + + +def wait_until(condition, timeout, *args, **kwargs): + """ Wait until ``condition`` returns true or timeout elapses. + + Parameters + ---------- + condition : callable + A callable checking the desired ``condition`` and returns + True or False. + timeout : float + The timeout, in seconds, to wait for the ``condition`` to return True + *args : + Arguments to pass to the ``condition`` callable + *kwargs : + Arguments to pass to the ``condition`` callable + + """ + check_now = Event() + + def check_timer(): + timer = Timer(0.5, check_now.set) + timer.start() + + heartbeats = timeout / 0.5 + while heartbeats >= 1: + check_now.clear() + check_timer() + check_now.wait(timeout) + if condition(*args, **kwargs): + break + else: + heartbeats -= 1 + + if heartbeats == 0: + raise RuntimeError('Timed out waiting for condition') + +@contextmanager +def silence_output(out=None, err=None): + """ Re-direct the stderr and stdout streams while in the block. """ + + if out is None: + out = open(os.devnull, 'w') + if err is None: + err = open(os.devnull, 'w') + + _old_stderr = sys.stderr + _old_stderr.flush() + + _old_stdout = sys.stdout + _old_stdout.flush() + + try: + sys.stdout = out + sys.stderr = err + yield + finally: + sys.stdout = _old_stdout + sys.stderr = _old_stderr diff --git a/pyface/ui/wx/util/modal_dialog_tester.py b/pyface/ui/wx/util/modal_dialog_tester.py new file mode 100644 index 000000000..031f29fdc --- /dev/null +++ b/pyface/ui/wx/util/modal_dialog_tester.py @@ -0,0 +1,370 @@ +# (C) Copyright 2014 Enthought, Inc., Austin, TX +# All right reserved. +""" A class to facilitate testing components that use TraitsUI or Wx Dialogs. + +""" +import contextlib +import sys +import traceback +from threading import Timer, Thread, Event + +import wx + +from pyface.api import OK, CANCEL, YES, NO +from pyface.util.guisupport import get_app_wx +from traits.api import Undefined + +from .event_loop_helper import EventLoopHelper +from .gui_test_assistant import find_widget +from .helpers import wait_until + + +is_win32 = (sys.platform == 'win32') +if is_win32: + from win32gui import ( + EndDialog, EnumWindows, GetActiveWindow, IsWindowEnabled, + IsWindowVisible) + from win32process import GetWindowThreadProcessId + + +BUTTON_TEXT = { + OK: 'OK', + CANCEL: 'Cancel', + YES: '&Yes', + NO: '&No', +} + + +class Dummy(wx.Frame): + + def __init__(self, function, *args, **kwargs): + wx.Frame.__init__(self, None, wx.ID_ANY, "ModalDialogTester") + wx.CallAfter(function, *args, **kwargs) + + +class ModalDialogTester(object): + """ Test helper for code that open a traits ui or QDialog window. + + Usage + ----- + :: + + # Common usage calling a `function` that will open a dialog and then + # accept the dialog info. + tester = ModalDialogTester(function) + tester.open_and_run(when_opened=lambda x: x.close(accept=True)) + self.assertEqual(tester.result, ) + + + .. note:: + + - Proper operation assumes that at all times the dialog is a modal + window. + - Errors and failures during the when_opened call do not register with + the unittest testcases because they take place on a deferred call in + the event loop. It is advised that the `capture_error` context + manager is used from the GuiTestAssistant when necessary. + + """ + def __init__(self, function): + #: The command to call that will cause a dialog to open. + self.function = function + self._assigned = False + self._result = Undefined + self._gui_app = get_app_wx() + self._event_loop_error = [] + self._helper = EventLoopHelper(gui_app=self._gui_app) + self._gui = self._helper.gui + self._dialog_widget = None + self._handler = wx.EvtHandler() + + @property + def result(self): + """ The return value of the provided function. + + """ + return self._result + + @result.setter + def result(self, value): + """ Setter methods for the result attribute. + + """ + self._assigned = True + self._result = value + + def open_and_run(self, when_opened, *args, **kwargs): + """ Execute the function to open the dialog and run ``when_opened``. + + Parameters + ---------- + when_opened : callable + A callable to be called when the dialog has been created and + opened. The callable with be called with the tester instance + as argument. + + *args, **kwargs : + Additional arguments to be passed to the `function` + attribute of the tester. + + Raises + ------ + AssertionError : + if an assertion error was captured during the + deferred calls that open and close the dialog. + RuntimeError : + if a result value has not been assigned within 15 + seconds after calling `self.function` + Any other exception that was captured during the deferred calls + that open and close the dialog. + + .. note:: This method is synchronous + + """ + condition_timer = wx.Timer(self._handler) + + def handler(): + """ Run the when_opened as soon as the dialog has opened. """ + if self.dialog_opened(): + wx.CallAfter(when_opened, self) + else: + condition_timer.Start(100, True) + + # Setup and start the timer to signal the handler every 100 msec. + self._handler.Bind( + wx.EVT_TIMER, lambda event: handler(), condition_timer) + condition_timer.Start(100, True) + + self._assigned = False + try: + self._helper.event_loop_until_condition( + condition=self.value_assigned, + start=lambda: self.open(*args, **kwargs), + timeout=5) + finally: + condition_timer.Stop() + self.assert_no_errors_collected() + + def open_and_wait(self, when_opened, *args, **kwargs): + """ Execute the function to open the dialog and wait to be closed. + + Parameters + ---------- + when_opened : callable + A callable to be called when the dialog has been created and + opened. The callable with be called with the tester instance + as argument. + + *args, **kwargs : + Additional arguments to be passed to the `function` + attribute of the tester. + + Raises + ------ + AssertionError if an assertion error was captured during the + deferred calls that open and close the dialog. + RuntimeError if the dialog has not been closed within 15 seconds after + calling `self.function`. + Any other exception that was captured during the deferred calls + that open and close the dialog. + + .. note:: This method is synchronous + + """ + condition_timer = wx.Timer(self._handler) + + def handler(): + """ Run the when_opened as soon as the dialog has opened. """ + if self.dialog_opened(): + wx.CallAfter(when_opened, self) + else: + condition_timer.Start(100, True) + + def condition(): + if self._dialog_widget is None: + return False + else: + return self.get_dialog_widget() != self._dialog_widget + + # Setup and start the timer to signal the handler every 100 msec. + self._handler.Bind( + wx.EVT_TIMER, lambda event: handler(), condition_timer) + condition_timer.Start(100, True) + + self._assigned = False + try: + self._helper.event_loop_until_condition( + condition=condition, + start=lambda: self.open(*args, **kwargs), + timeout=5) + finally: + condition_timer.Stop() + self.assert_no_errors_collected() + + def open(self, *args, **kwargs): + """ Execute the function that will cause a dialog to be opened. + + Parameters + ---------- + *args, **kwargs : + Arguments to be passed to the `function` attribute of the + tester. + + .. note:: This method is synchronous + + """ + with self.capture_error(): + self.result = self.function(*args, **kwargs) + + def close(self, accept=False): + """ Close the dialog by accepting or rejecting. + + """ + def close_dialog(dialog, accept): + if is_win32 and isinstance(dialog, long): + if accept: + self._gui.invoke_later(EndDialog, dialog, 1) + else: + self._gui.invoke_later(EndDialog, dialog, 2) + else: + if accept: + button = self.find_widget( + test=lambda x: x.GetLabelText() == BUTTON_TEXT[OK]) + else: + button = self.find_widget( + test=lambda x: x.GetLabelText() == BUTTON_TEXT[CANCEL]) + if button is not None: + click_event = wx.CommandEvent( + wx.wxEVT_COMMAND_BUTTON_CLICKED, + button.GetId()) + button.ProcessEvent(click_event) + else: + if accept: + dialog.EndModal(0) + else: + dialog.EndModal(1) + self._gui.invoke_later(dialog.Close) + self._gui.invoke_later(dialog.Destroy) + + with self.capture_error(): + widget = self.get_dialog_widget() + self._gui.invoke_later(close_dialog, widget, accept) + wx.Yield() + + @contextlib.contextmanager + def capture_error(self): + """ Capture exceptions, to be used while running inside an event loop. + + When errors and failures take place through an invoke later command + they might not be caught by the unittest machinery. This context + manager when used inside a deferred call, will capture the fact that + an error has occurred and the user can later use the `check for errors` + command which will raise an error or failure if necessary. + + """ + try: + yield + except Exception: + self._event_loop_error.append( + (sys.exc_info()[0], traceback.format_exc())) + raise + + def assert_no_errors_collected(self): + """ Assert that the tester has not collected any errors. + + """ + if len(self._event_loop_error) > 0: + msg = 'The following error(s) were detected:\n\n{0}' + tracebacks = [] + for type_, message in self._event_loop_error: + if isinstance(type_, AssertionError): + msg = 'The following failure(s) were detected:\n\n{0}' + tracebacks.append(message) + + raise type_(msg.format('\n\n'.join(tracebacks))) + + def click_widget(self, text, type_=wx.Button): + """ Execute click on the widget of `type_` with `text`. + + """ + control = self.get_dialog_widget() + widget = find_widget( + control, + type_, + test=lambda widget: widget.GetText() == text) + widget.click() + + def click_button(self, button_id): + text = BUTTON_TEXT[button_id] + self.click_widget(text) + + def value_assigned(self): + """ A value was assigned to the result attribute. + + """ + return self._assigned + + def dialog_opened(self): + """ Check that the dialog has opened. + + """ + dialog = self.get_dialog_widget() + if dialog is None: + opened = False + elif is_win32 and isinstance(dialog, long): + # We have a windows handle. + opened = IsWindowVisible(dialog) and IsWindowEnabled(dialog) + else: + # This is a simple wx.Dialog. + opened = dialog.IsShown() + return opened + + def get_dialog_widget(self): + """ Get a reference to the active window widget. + + """ + import os + + window = self._gui_app.GetTopWindow() + if window is not None and window.GetTitle() != "ModalDialogTester": + return window + elif is_win32: + # Native dialogs do not appear in the wx lists + handles = [] + + def callback(hwnd, extra): + info = GetWindowThreadProcessId(hwnd) + if ( + info[1] == os.getpid() and + IsWindowVisible(hwnd) and + IsWindowEnabled(hwnd)): + extra.append(hwnd) + + EnumWindows(callback, handles) + active = GetActiveWindow() + if active in handles: + return active + else: + return None + else: + # XXX Todo: support for OS X and Gtk/Linux? + return None + + def has_widget(self, text=None, type_=wx.Button): + """ Return true if there is a widget of `type_` with `text`. + + """ + if text is None: + test = None + else: + test = lambda widget: widget.GetLabelText() == text + return self.find_widget(type_=type_, test=test) is not None + + def find_widget(self, type_=wx.Button, test=None): + """ Return the widget of `type_` for which `test` returns true. + + """ + if test is None: + test = lambda x: True + window = self.get_dialog_widget() + return find_widget(window, type_, test=test) diff --git a/pyface/ui/wx/util/tests/__init__.py b/pyface/ui/wx/util/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pyface/ui/wx/util/tests/test_modal_dialog_tester.py b/pyface/ui/wx/util/tests/test_modal_dialog_tester.py new file mode 100644 index 000000000..e7d3d7f2b --- /dev/null +++ b/pyface/ui/wx/util/tests/test_modal_dialog_tester.py @@ -0,0 +1,144 @@ +# Copyright (c) 2013-2015 by Enthought, Inc., Austin, TX +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in enthought/LICENSE.txt and may be redistributed only +# under the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# Thanks for using Enthought open source! + +import unittest +import cStringIO + +import wx + +from pyface.api import MessageDialog, OK, CANCEL +from traits.api import HasStrictTraits +from traitsui.api import CancelButton, OKButton, View + +from ..gui_test_assistant import GuiTestAssistant +from ..modal_dialog_tester import ModalDialogTester +from ..helpers import silence_output + + +class MyClass(HasStrictTraits): + + def default_traits_view(self): + view = View( + buttons=[OKButton, CancelButton], + resizable=False, + title='My class dialog') + return view + + def run(self): + ui = self.edit_traits(kind='livemodal') + + if ui.result: + return 'accepted' + else: + return 'rejected' + + +class TestModalDialogTester(GuiTestAssistant, unittest.TestCase): + + def test_on_message_dialog(self): + dialog = MessageDialog() + tester = ModalDialogTester(dialog.open) + + # accept + tester.open_and_run(when_opened=lambda x: x.close(accept=True)) + self.assertTrue(tester.value_assigned()) + self.assertEqual(tester.result, OK) + + # reject will return OK again since we do not have a cancel button + # This is probably not the same with QT. + tester.open_and_run(when_opened=lambda x: x.close()) + self.assertTrue(tester.value_assigned()) + self.assertEqual(tester.result, OK) + + def test_on_traitsui_dialog(self): + my_class = MyClass() + tester = ModalDialogTester(my_class.run) + + tester.open() + # accept + tester.open_and_run(when_opened=lambda x: x.close(accept=True)) + self.assertTrue(tester.value_assigned()) + self.assertEqual(tester.result, 'accepted') + + # reject will return OK again since we do not have a cancel button + # This is probably not the same with QT. + tester.open_and_run(when_opened=lambda x: x.close()) + self.assertTrue(tester.value_assigned()) + self.assertEqual(tester.result, 'rejected') + + def test_capture_errors_on_failure(self): + dialog = MessageDialog() + tester = ModalDialogTester(dialog.open) + + def failure(tester): + try: + with tester.capture_error(): + # this failure will appear in the console and get recorded + self.fail() + finally: + tester.close() + + with self.assertRaises(AssertionError): + alt_stderr = cStringIO.StringIO + with silence_output(err=alt_stderr): + tester.open_and_run(when_opened=failure) + self.assertIn('raise self.failureException(msg)', alt_stderr) + + def test_capture_errors_on_error(self): + dialog = MessageDialog() + tester = ModalDialogTester(dialog.open) + + def raise_error(tester): + try: + with tester.capture_error(): + # this error will appear in the console and get recorded + 1 / 0 + finally: + tester.close() + + with self.assertRaises(ZeroDivisionError): + alt_stderr = cStringIO.StringIO() + with silence_output(err=alt_stderr): + tester.open_and_run(when_opened=raise_error) + self.assertIn('ZeroDivisionError', alt_stderr) + + def test_has_widget(self): + my_class = MyClass() + tester = ModalDialogTester(my_class.run) + + def check_and_close(tester): + try: + with tester.capture_error(): + self.assertTrue(tester.has_widget('OK', wx.Button)) + self.assertFalse( + tester.has_widget(text='I am a virtual button')) + finally: + tester.close() + + tester.open_and_run(when_opened=check_and_close) + + def test_find_widget(self): + my_class = MyClass() + tester = ModalDialogTester(my_class.run) + + def check_and_close(tester): + try: + with tester.capture_error(): + widget = tester.find_widget( + type_=wx.Button, + test=lambda x: x.GetLabelText() == 'OK') + self.assertIsInstance(widget, wx.Button) + finally: + tester.close() + + tester.open_and_run(when_opened=check_and_close) + + +if __name__ == '__main__': + unittest.main()