Lin_Simulator/python/tests/test_main_window.py
Mohamed Salem b808770573 Step 1: GUI skeleton for LIN Simulator (Python + C++)
- 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>
2026-04-02 16:40:52 +02:00

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()