diff --git a/src/bitmessageqt/tests/__init__.py b/src/bitmessageqt/tests/__init__.py
index a81ddb047..f8a578a0f 100644
--- a/src/bitmessageqt/tests/__init__.py
+++ b/src/bitmessageqt/tests/__init__.py
@@ -4,8 +4,9 @@
from .main import TestMain, TestUISignaler
from .settings import TestSettings
from .support import TestSupport
+from .test_import import TestImports
__all__ = [
- "TestAddressbook", "TestMain", "TestSettings", "TestSupport",
- "TestUISignaler"
+ "TestAddressbook", "TestImports", "TestMain", "TestSettings",
+ "TestSupport", "TestUISignaler"
]
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..35a193a73
--- /dev/null
+++ b/src/bitmessageqt/tests/test_widgets.py
@@ -0,0 +1,87 @@
+"""
+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_unescape(self):
+ """Undo urlencoding"""
+ from bitmessageqt.safehtmlparser import SafeHTMLParser
+ parser = SafeHTMLParser()
+ parser.reset()
+ parser.reset_safe()
+ parser.feed("hello & world")
+ self.assertIn("hello", parser.sanitised)
+ self.assertNotIn("