- PyQt6 main window with Tx/Rx tables, connection dock, LDF toolbar, control bar with global send rate, and status bar - C++ Qt6 equivalent with identical layout and feature parity - About dialog: TeqanyLogix LTD / Developer: Mohamed Salem - Application logo (SVG + PNG) with LIN bus waveform design - Full test suites: Python (32 tests), C++ QTest (34 tests) - Project plan and Step 1 documentation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
208 lines
7.1 KiB
Python
208 lines
7.1 KiB
Python
"""
|
|
test_main_window.py — Tests for the GUI skeleton (Step 1).
|
|
|
|
TESTING PyQt6 APPLICATIONS:
|
|
============================
|
|
GUI testing has a challenge: Qt widgets need a running QApplication.
|
|
We use a pytest fixture to create ONE QApplication for the entire test session.
|
|
|
|
Key things we verify:
|
|
1. Window creates without errors
|
|
2. All panels exist and are the correct widget types
|
|
3. Tables have the right columns
|
|
4. Buttons and controls are in the expected initial state
|
|
5. Window has a reasonable minimum size
|
|
|
|
We do NOT test visual appearance (that's manual testing).
|
|
We DO test that the widget tree is correctly assembled.
|
|
"""
|
|
|
|
import sys
|
|
import pytest
|
|
|
|
# Add src to path so we can import our modules
|
|
sys.path.insert(0, str(__import__("pathlib").Path(__file__).parent.parent / "src"))
|
|
|
|
from PyQt6.QtWidgets import QApplication, QDockWidget, QTableWidget
|
|
from PyQt6.QtCore import Qt
|
|
|
|
|
|
# ─── Fixtures ──────────────────────────────────────────────────────────
|
|
# Fixtures are pytest's way of providing shared setup. The "session" scope
|
|
# means this QApplication is created once and reused across ALL tests.
|
|
|
|
@pytest.fixture(scope="session")
|
|
def app():
|
|
"""Create a QApplication instance for the test session."""
|
|
application = QApplication.instance() or QApplication(sys.argv)
|
|
yield application
|
|
|
|
|
|
@pytest.fixture
|
|
def window(app):
|
|
"""Create a fresh MainWindow for each test."""
|
|
from main_window import MainWindow
|
|
w = MainWindow()
|
|
yield w
|
|
w.close()
|
|
|
|
|
|
# ─── Tests ─────────────────────────────────────────────────────────────
|
|
|
|
class TestWindowBasics:
|
|
"""Test that the window initializes correctly."""
|
|
|
|
def test_window_title(self, window):
|
|
assert window.windowTitle() == "LIN Simulator"
|
|
|
|
def test_minimum_size(self, window):
|
|
assert window.minimumWidth() >= 1024
|
|
assert window.minimumHeight() >= 768
|
|
|
|
def test_central_widget_exists(self, window):
|
|
assert window.centralWidget() is not None
|
|
|
|
|
|
class TestMenuBar:
|
|
"""Test menu bar structure."""
|
|
|
|
def test_menu_bar_exists(self, window):
|
|
assert window.menuBar() is not None
|
|
|
|
def test_load_ldf_action_exists(self, window):
|
|
assert window.action_load_ldf is not None
|
|
assert window.action_load_ldf.text() == "&Load LDF..."
|
|
|
|
def test_load_ldf_shortcut(self, window):
|
|
assert window.action_load_ldf.shortcut().toString() == "Ctrl+O"
|
|
|
|
|
|
class TestLdfToolbar:
|
|
"""Test the LDF file toolbar."""
|
|
|
|
def test_ldf_path_field_exists(self, window):
|
|
assert window.ldf_path_edit is not None
|
|
assert window.ldf_path_edit.isReadOnly()
|
|
|
|
def test_ldf_path_placeholder(self, window):
|
|
assert window.ldf_path_edit.placeholderText() == "No LDF file loaded"
|
|
|
|
def test_browse_button_exists(self, window):
|
|
assert window.btn_browse_ldf is not None
|
|
|
|
def test_auto_reload_default_checked(self, window):
|
|
assert window.chk_auto_reload.isChecked()
|
|
|
|
|
|
class TestTxTable:
|
|
"""Test the Tx (transmit) table structure."""
|
|
|
|
def test_tx_table_exists(self, window):
|
|
assert isinstance(window.tx_table, QTableWidget)
|
|
|
|
def test_tx_table_columns(self, window):
|
|
assert window.tx_table.columnCount() == 7
|
|
headers = []
|
|
for i in range(window.tx_table.columnCount()):
|
|
headers.append(window.tx_table.horizontalHeaderItem(i).text())
|
|
assert headers == [
|
|
"Frame Name", "Frame ID", "Length", "Interval (ms)",
|
|
"Data", "Signals", "Action"
|
|
]
|
|
|
|
def test_tx_table_alternating_colors(self, window):
|
|
assert window.tx_table.alternatingRowColors()
|
|
|
|
|
|
class TestRxTable:
|
|
"""Test the Rx (receive) table structure."""
|
|
|
|
def test_rx_table_exists(self, window):
|
|
assert isinstance(window.rx_table, QTableWidget)
|
|
|
|
def test_rx_table_columns(self, window):
|
|
assert window.rx_table.columnCount() == 5
|
|
headers = []
|
|
for i in range(window.rx_table.columnCount()):
|
|
headers.append(window.rx_table.horizontalHeaderItem(i).text())
|
|
assert headers == ["Timestamp", "Frame Name", "Frame ID", "Data", "Signals"]
|
|
|
|
def test_rx_table_not_editable(self, window):
|
|
"""Rx table should be read-only — users can't edit received data."""
|
|
assert (
|
|
window.rx_table.editTriggers()
|
|
== QTableWidget.EditTrigger.NoEditTriggers
|
|
)
|
|
|
|
|
|
class TestConnectionDock:
|
|
"""Test the connection panel dock widget."""
|
|
|
|
def test_dock_exists(self, window):
|
|
docks = window.findChildren(QDockWidget)
|
|
assert len(docks) == 1
|
|
assert docks[0].windowTitle() == "Connection"
|
|
|
|
def test_device_combo_exists(self, window):
|
|
assert window.combo_device is not None
|
|
|
|
def test_connect_button_exists(self, window):
|
|
assert window.btn_connect is not None
|
|
assert window.btn_connect.isEnabled()
|
|
|
|
def test_disconnect_button_disabled_initially(self, window):
|
|
"""Disconnect should be disabled when no device is connected."""
|
|
assert not window.btn_disconnect.isEnabled()
|
|
|
|
def test_status_label_shows_disconnected(self, window):
|
|
assert "Disconnected" in window.lbl_conn_status.text()
|
|
|
|
def test_baud_rate_label_exists(self, window):
|
|
assert window.lbl_baud_rate is not None
|
|
|
|
def test_baud_rate_shows_placeholder_before_ldf(self, window):
|
|
"""Before loading an LDF, baud rate should show a placeholder."""
|
|
assert "load LDF" in window.lbl_baud_rate.text()
|
|
|
|
|
|
class TestControlBar:
|
|
"""Test the bottom control toolbar."""
|
|
|
|
def test_schedule_combo_exists(self, window):
|
|
assert window.combo_schedule is not None
|
|
|
|
def test_scheduler_buttons_disabled_initially(self, window):
|
|
"""Start/Stop/Pause should be disabled until LDF loaded + connected."""
|
|
assert not window.btn_start.isEnabled()
|
|
assert not window.btn_stop.isEnabled()
|
|
assert not window.btn_pause.isEnabled()
|
|
|
|
def test_manual_send_disabled_initially(self, window):
|
|
assert not window.btn_manual_send.isEnabled()
|
|
|
|
def test_global_rate_spinbox_exists(self, window):
|
|
assert window.spin_global_rate is not None
|
|
|
|
def test_global_rate_default_50ms(self, window):
|
|
"""50ms default = ~20 frames/sec, typical LIN schedule speed."""
|
|
assert window.spin_global_rate.value() == 50
|
|
|
|
def test_global_rate_range(self, window):
|
|
"""Range should be 1ms (fast stress test) to 10000ms (very slow)."""
|
|
assert window.spin_global_rate.minimum() == 1
|
|
assert window.spin_global_rate.maximum() == 10000
|
|
|
|
def test_global_rate_suffix(self, window):
|
|
"""Should display the unit 'ms' in the spinbox."""
|
|
assert window.spin_global_rate.suffix() == " ms"
|
|
|
|
|
|
class TestStatusBar:
|
|
"""Test the status bar."""
|
|
|
|
def test_status_bar_exists(self, window):
|
|
assert window.statusBar() is not None
|
|
|
|
def test_connection_status_label(self, window):
|
|
assert "Disconnected" in window.lbl_status_connection.text()
|