diff --git a/src/bitmessageqt/tests/__init__.py b/src/bitmessageqt/tests/__init__.py index a81ddb047..27189e7a2 100644 --- a/src/bitmessageqt/tests/__init__.py +++ b/src/bitmessageqt/tests/__init__.py @@ -4,8 +4,15 @@ from .main import TestMain, TestUISignaler from .settings import TestSettings from .support import TestSupport +from .test_import import TestImports +from .test_startup import TestStartup +from .test_widgets import ( + TestAddressValidator, TestLanguageBox, TestMessageView, + TestSafeHTMLParser +) __all__ = [ - "TestAddressbook", "TestMain", "TestSettings", "TestSupport", - "TestUISignaler" + "TestAddressbook", "TestAddressValidator", "TestLanguageBox", + "TestImports", "TestMain", "TestMessageView", "TestSafeHTMLParser", + "TestSettings", "TestStartup", "TestSupport", "TestUISignaler" ] diff --git a/src/bitmessageqt/tests/main.py b/src/bitmessageqt/tests/main.py index daf5d6d22..f8637b2b7 100644 --- a/src/bitmessageqt/tests/main.py +++ b/src/bitmessageqt/tests/main.py @@ -10,6 +10,36 @@ from bitmessageqt import _translate, config, queues +# pylint: disable=too-few-public-methods +class TestApp(QtGui.QApplication): + """Lightweight QApplication subclass for tests, without the heavy + BitmessageQtApplication init (QLocalSocket singleton check, + organisation metadata, etc.).""" + + @staticmethod + def get_windowstyle(): + """Get window style set in config or default""" + return config.safeGet( + 'bitmessagesettings', 'windowstyle', + 'Windows' if sys.platform.startswith('win') else 'GTK+' + ) + + +def get_test_app(): + """Return the existing QApplication or create a TestApp. + + If the running app is a plain QApplication (missing get_windowstyle), + patch in the required methods from TestApp.""" + app = QtGui.QApplication.instance() + if app is None: + return TestApp(sys.argv) + if not hasattr(app, 'get_windowstyle'): + # Bolt on the methods the tests expect; this happens when + # another test already created a bare QApplication. + app.get_windowstyle = TestApp.get_windowstyle + return app + + class TestBase(unittest.TestCase): """Base class for bitmessageqt test case""" @@ -17,11 +47,10 @@ class TestBase(unittest.TestCase): def setUpClass(cls): """Provide the UI test cases with common settings""" cls.config = config + cls.app = get_test_app() def setUp(self): - self.app = ( - QtGui.QApplication.instance() - or bitmessageqt.BitmessageQtApplication(sys.argv)) + self.app = self.__class__.app self.window = self.app.activeWindow() if not self.window: self.window = bitmessageqt.MyForm() diff --git a/src/bitmessageqt/tests/test_import.py b/src/bitmessageqt/tests/test_import.py new file mode 100644 index 000000000..2e2bdb0da --- /dev/null +++ b/src/bitmessageqt/tests/test_import.py @@ -0,0 +1,67 @@ +""" +Smoke tests: verify that bitmessageqt modules can be imported. + +These tests require PyQt4 to be installed but do NOT need a running +X server, database, or any bitmessage backend threads. +""" +import unittest + + +# pylint: disable=import-error,unused-variable +class TestImports(unittest.TestCase): + """Verify that key bitmessageqt modules are importable""" + + @staticmethod + def test_import_bitmessageqt(): + """The main bitmessageqt package should be importable""" + import bitmessageqt + + @staticmethod + def test_import_bitmessageui(): + """The generated UI module should be importable""" + from bitmessageqt import bitmessageui + + @staticmethod + def test_import_settings(): + """The settings dialog module should be importable""" + from bitmessageqt import settings + + @staticmethod + def test_import_address_dialogs(): + """The address dialogs module should be importable""" + from bitmessageqt import address_dialogs + + @staticmethod + def test_import_networkstatus(): + """The network status module should be importable""" + from bitmessageqt import networkstatus + + @staticmethod + def test_import_safehtmlparser(): + """safehtmlparser should be importable""" + from bitmessageqt import safehtmlparser + + @staticmethod + def test_import_support(): + """The support module should be importable""" + from bitmessageqt import support + + @staticmethod + def test_import_foldertree(): + """The foldertree module should be importable""" + from bitmessageqt import foldertree + + @staticmethod + def test_import_messageview(): + """The messageview module should be importable""" + from bitmessageqt import messageview + + @staticmethod + def test_import_utils(): + """The utils module should be importable""" + from bitmessageqt import utils + + @staticmethod + def test_import_account(): + """The account module should be importable""" + from bitmessageqt import account diff --git a/src/bitmessageqt/tests/test_startup.py b/src/bitmessageqt/tests/test_startup.py new file mode 100644 index 000000000..232702014 --- /dev/null +++ b/src/bitmessageqt/tests/test_startup.py @@ -0,0 +1,54 @@ +""" +Smoke test: verify the main window can be created and shut down. + +This test requires the full bitmessage backend to be running +(it is designed to run via ``bitmessagemain.py -t`` or from +``src/tests/core.py``). It also needs a display (Xvfb is fine). +""" +import sys +import unittest + +try: + from PyQt4 import QtCore, QtGui + has_qt = True +except ImportError: + has_qt = False + + +@unittest.skipUnless(has_qt, "requires PyQt4") +class TestStartup(unittest.TestCase): + """Verify the main window starts and has expected structure""" + + def setUp(self): + import bitmessageqt + self.app = ( + QtGui.QApplication.instance() + or bitmessageqt.BitmessageQtApplication(sys.argv)) + self.window = self.app.activeWindow() + if not self.window: + self.window = bitmessageqt.MyForm() + self.window.appIndicatorInit(self.app) + + def test_window_exists(self): + """The main window should be created successfully""" + self.assertIsNotNone(self.window) + self.assertIsNotNone(self.window.ui) + + def test_window_has_tabs(self): + """The main window should have the expected tab widget""" + tabs = self.window.ui.tabWidget + self.assertIsNotNone(tabs) + self.assertGreater(tabs.count(), 0) + + def test_window_title(self): + """The main window should have a non-empty title""" + self.assertTrue(len(self.window.windowTitle()) > 0) + + def test_status_bar(self): + """The main window should have a status bar""" + self.assertIsNotNone(self.window.statusBar()) + + def test_quit_cycle(self): + """The event loop should start and stop without crashing""" + QtCore.QTimer.singleShot(50, self.app.quit) + self.app.exec_() diff --git a/src/bitmessageqt/tests/test_widgets.py b/src/bitmessageqt/tests/test_widgets.py new file mode 100644 index 000000000..6652072ff --- /dev/null +++ b/src/bitmessageqt/tests/test_widgets.py @@ -0,0 +1,92 @@ +""" +Unit tests for individual bitmessageqt widgets. + +These tests need a display (real or virtual via Xvfb/xvfb-run) and PyQt4, +but do NOT require the full bitmessage backend, database, or network. +Each test creates only the minimal widget under test. +""" +import sys +import unittest + +try: + from PyQt4 import QtCore, QtGui, QtTest + has_qt = True +except ImportError: + has_qt = False + + +def get_app(): + """Return existing QApplication or create a new one""" + return QtGui.QApplication.instance() or QtGui.QApplication(sys.argv) + + +@unittest.skipUnless(has_qt, "requires PyQt4") +class TestSafeHTMLParser(unittest.TestCase): + """Test the SafeHTMLParser used for message rendering""" + + def test_sanitise(self): + """Check that dangerous HTML is stripped""" + from bitmessageqt.safehtmlparser import SafeHTMLParser + parser = SafeHTMLParser() + parser.reset() + parser.reset_safe() + parser.feed("hello & world") + self.assertIn("hello", parser.sanitised) + self.assertNotIn("test message.") + self.assertIn("test", widget.toPlainText()) + + +@unittest.skipUnless(has_qt, "requires PyQt4") +class TestAddressValidator(unittest.TestCase): + """Test the AddressValidator""" + + def setUp(self): + self.app = get_app() + + def test_create_validator(self): + """AddressValidator can be instantiated with a buttonBox""" + from bitmessageqt.addressvalidator import AddressValidator + line_edit = QtGui.QLineEdit() + # AddressValidator.setParams() reads the Ok button label on init, + # so a real QDialogButtonBox is the simplest way to satisfy that. + button_box = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok) + validator = AddressValidator( + line_edit, buttonBox=button_box) + self.assertIsNotNone(validator) + self.assertFalse(validator.isValid) + + +@unittest.skipUnless(has_qt, "requires PyQt4") +class TestLanguageBox(unittest.TestCase): + """Test the language selection combobox""" + + def setUp(self): + self.app = get_app() + + def test_create_languagebox(self): + """LanguageBox can be instantiated""" + from bitmessageqt.languagebox import LanguageBox + parent = QtGui.QWidget() + combo = LanguageBox(parent) + self.assertIsNotNone(combo) + self.assertGreater(combo.count(), 0)