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 alert('x') & world")
+ self.assertIn("hello", parser.sanitised)
+ self.assertNotIn("