""" test_rx_realtime.py — Tests for Step 4: Rx panel real-time display. Tests frame reception, timestamp updates, signal unpacking, change highlighting, auto-scroll, and clear functionality. """ import sys import pytest from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent / "src")) from PyQt6.QtWidgets import QApplication from PyQt6.QtCore import Qt from PyQt6.QtGui import QBrush, QColor SAMPLE_LDF = str(Path(__file__).parent.parent.parent / "resources" / "sample.ldf") @pytest.fixture(scope="session") def app(): application = QApplication.instance() or QApplication(sys.argv) yield application @pytest.fixture def window(app): from main_window import MainWindow w = MainWindow() w._load_ldf_file(SAMPLE_LDF) return w class TestRxFrameReception: """Test that receive_rx_frame updates the Rx panel correctly.""" def test_timestamp_updates(self, window): """Receiving a frame should set the timestamp.""" window.receive_rx_frame(0x20, [0x03, 0xC8]) frame_item = window.rx_table.topLevelItem(0) ts = frame_item.text(0) assert ts != "—" assert ":" in ts # Should be HH:mm:ss.zzz format def test_frame_bytes_stored(self, window): """Received bytes should be stored in the frame row.""" window.receive_rx_frame(0x20, [0x03, 0xC8]) frame_item = window.rx_table.topLevelItem(0) stored = frame_item.data(4, Qt.ItemDataRole.UserRole) assert stored == [0x03, 0xC8] def test_frame_value_displayed(self, window): """Frame value column should show received bytes.""" window.receive_rx_frame(0x20, [0x03, 0xC8]) frame_item = window.rx_table.topLevelItem(0) val = frame_item.text(4) # Default hex mode assert "03" in val assert "C8" in val def test_signal_values_unpacked(self, window): """Signal children should have unpacked values from received bytes.""" # Motor_Status: MotorStatus (bit 0, w2), MotorTemp (bit 8, w8) window.receive_rx_frame(0x20, [0x03, 0xC8]) frame_item = window.rx_table.topLevelItem(0) # MotorStatus (bit 0, width 2): byte0 & 0x03 = 3 status_val = frame_item.child(0).data(4, Qt.ItemDataRole.UserRole) assert status_val == 3 # MotorTemp (bit 8, width 8): byte1 = 0xC8 = 200 temp_val = frame_item.child(1).data(4, Qt.ItemDataRole.UserRole) assert temp_val == 200 def test_unknown_frame_id_ignored(self, window): """Receiving an unknown frame ID should not crash.""" window.receive_rx_frame(0xFF, [0x00]) # No assertion — just verify no crash def test_update_same_frame_twice(self, window): """Second reception of same frame should update in-place.""" window.receive_rx_frame(0x20, [0x01, 0x10]) window.receive_rx_frame(0x20, [0x02, 0x20]) frame_item = window.rx_table.topLevelItem(0) stored = frame_item.data(4, Qt.ItemDataRole.UserRole) assert stored == [0x02, 0x20] # Updated, not appended class TestChangeHighlighting: """Test that changed signal values get highlighted.""" def test_changed_signal_highlighted(self, window): """When a signal value changes, it should be highlighted yellow.""" # First reception window.receive_rx_frame(0x20, [0x01, 0x10]) # Second reception with different MotorTemp window.receive_rx_frame(0x20, [0x01, 0x20]) frame_item = window.rx_table.topLevelItem(0) temp_sig = frame_item.child(1) # MotorTemp changed: 0x10 → 0x20 bg = temp_sig.background(4) # Should be highlighted (amber/yellow with alpha) assert bg.color().red() > 200 assert bg.color().green() > 150 def test_unchanged_signal_not_highlighted(self, window): """Signal that didn't change should not be highlighted.""" window.receive_rx_frame(0x20, [0x01, 0x10]) window.receive_rx_frame(0x20, [0x01, 0x20]) frame_item = window.rx_table.topLevelItem(0) status_sig = frame_item.child(0) # MotorStatus unchanged: still 1 bg = status_sig.background(4) # Should NOT be yellow — default/empty brush assert bg.style() == Qt.BrushStyle.NoBrush def test_first_reception_no_highlight(self, window): """First reception should not highlight anything (no previous value).""" window.receive_rx_frame(0x20, [0x01, 0x10]) frame_item = window.rx_table.topLevelItem(0) for j in range(frame_item.childCount()): bg = frame_item.child(j).background(4) assert bg.style() == Qt.BrushStyle.NoBrush class TestAutoScroll: """Test auto-scroll behavior.""" def test_auto_scroll_default_on(self, window): assert window.chk_auto_scroll.isChecked() def test_auto_scroll_can_be_disabled(self, window): window.chk_auto_scroll.setChecked(False) assert not window.chk_auto_scroll.isChecked() class TestClearRx: """Test the Clear button.""" def test_clear_resets_timestamps(self, window): window.receive_rx_frame(0x20, [0x01, 0x10]) window._on_clear_rx() for i in range(window.rx_table.topLevelItemCount()): assert window.rx_table.topLevelItem(i).text(0) == "—" def test_clear_resets_values(self, window): window.receive_rx_frame(0x20, [0x01, 0x10]) window._on_clear_rx() for i in range(window.rx_table.topLevelItemCount()): assert window.rx_table.topLevelItem(i).text(4) == "—" def test_clear_resets_signal_highlights(self, window): window.receive_rx_frame(0x20, [0x01, 0x10]) window.receive_rx_frame(0x20, [0x02, 0x20]) window._on_clear_rx() frame_item = window.rx_table.topLevelItem(0) for j in range(frame_item.childCount()): bg = frame_item.child(j).background(4) assert bg.style() == Qt.BrushStyle.NoBrush def test_clear_resets_last_values_tracking(self, window): window.receive_rx_frame(0x20, [0x01, 0x10]) window._on_clear_rx() assert len(window._rx_last_values) == 0 class TestRxHexDec: """Test hex/dec mode applies to received Rx data.""" def test_rx_hex_mode(self, window): window.receive_rx_frame(0x20, [0x03, 0xC8]) frame_item = window.rx_table.topLevelItem(0) # Hex mode (default) assert "C8" in frame_item.text(4) assert frame_item.child(1).text(4).startswith("0x") def test_rx_dec_mode(self, window): window.chk_hex_mode.setChecked(False) window.receive_rx_frame(0x20, [0x03, 0xC8]) frame_item = window.rx_table.topLevelItem(0) # Dec mode assert "200" in frame_item.text(4) assert not frame_item.child(1).text(4).startswith("0x") window.chk_hex_mode.setChecked(True) # Restore