mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-08-10 12:10:37 +02:00
feat(logpanel): redesign toolbar, filtering, detail pane, and tailing affordances
This commit is contained in:
@@ -85,7 +85,9 @@ def test_log_panel_filters(qtbot, log_panel: LogPanel):
|
||||
# Service filter
|
||||
log_panel._update_service_filter({"DeviceServer"})
|
||||
qtbot.waitUntil(lambda: log_panel._proxy.rowCount() == 1, timeout=200)
|
||||
log_panel._update_service_filter(set())
|
||||
log_panel._update_service_filter(set()) # empty include-list shows nothing
|
||||
qtbot.waitUntil(lambda: log_panel._proxy.rowCount() == 0, timeout=200)
|
||||
log_panel._update_service_filter(None) # no service filter shows everything
|
||||
qtbot.waitUntil(lambda: log_panel._proxy.rowCount() == 3, timeout=200)
|
||||
# Text filter
|
||||
log_panel._proxy.update_filter_text("efgh")
|
||||
@@ -289,6 +291,177 @@ def test_direct_queue_construction_registers_singleton(qtbot, mocked_client, mon
|
||||
queue.cleanup()
|
||||
|
||||
|
||||
def test_log_panel_copy_selection(qtbot, log_panel: LogPanel):
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
log_panel._table.selectAll()
|
||||
log_panel._copy_selection()
|
||||
copied = QApplication.clipboard().text().splitlines()
|
||||
assert len(copied) == 3
|
||||
assert "test debug message abcd" in copied[0]
|
||||
assert "[DEBUG]" in copied[0] and "ScanServer" in copied[0]
|
||||
|
||||
|
||||
def test_log_panel_detail_pane(qtbot, log_panel: LogPanel):
|
||||
assert log_panel._detail.isHidden()
|
||||
log_panel._table.setCurrentIndex(log_panel._proxy.index(1, 0))
|
||||
log_panel._show_details()
|
||||
assert not log_panel._detail.isHidden()
|
||||
assert log_panel._detail_text.toPlainText() == "test info message efgh"
|
||||
assert "INFO" in log_panel._detail_header.text()
|
||||
# selection changes update the open pane (sync is deferred by one event-loop turn)
|
||||
log_panel._table.setCurrentIndex(log_panel._proxy.index(2, 0))
|
||||
qtbot.waitUntil(
|
||||
lambda: log_panel._detail_text.toPlainText() == "test success message ijkl", timeout=500
|
||||
)
|
||||
log_panel._hide_details()
|
||||
assert log_panel._detail.isHidden()
|
||||
|
||||
|
||||
def test_log_panel_clear_view_keeps_history(qtbot, log_panel: LogPanel):
|
||||
queue = log_panel._model.log_queue
|
||||
log_panel._clear_view()
|
||||
assert log_panel._model.rowCount() == 0
|
||||
assert len(queue) == 3 # shared history untouched
|
||||
_feed(log_panel, [make_log_msg(0)])
|
||||
assert log_panel._model.rowCount() == 1 # new logs keep arriving after a clear
|
||||
|
||||
|
||||
def test_log_panel_toolbar_service_selection(qtbot, log_panel: LogPanel):
|
||||
toolbar = log_panel._toolbar
|
||||
toolbar._known_services = {"ScanServer", "DeviceServer"}
|
||||
toolbar.set_service_selection({"DeviceServer"})
|
||||
qtbot.waitUntil(lambda: log_panel._proxy.rowCount() == 1, timeout=200)
|
||||
assert toolbar.service_button.text() == "DeviceServer"
|
||||
toolbar.hide_service("DeviceServer")
|
||||
qtbot.waitUntil(lambda: log_panel._proxy.rowCount() == 0, timeout=200)
|
||||
assert toolbar.service_button.text() == "No services"
|
||||
# re-checking every known service through the menu collapses to the unfiltered state
|
||||
toolbar.set_service_selection({"ScanServer"})
|
||||
toolbar._on_service_toggled("DeviceServer", True)
|
||||
qtbot.waitUntil(lambda: log_panel._proxy.rowCount() == 3, timeout=200)
|
||||
assert toolbar.service_button.text() == "All services"
|
||||
assert toolbar._checked_services is None
|
||||
|
||||
|
||||
def test_log_panel_toolbar_level_default_and_preset(qtbot, log_panel: LogPanel):
|
||||
box = log_panel._toolbar.filter_level_dropdown
|
||||
assert box.currentIndex() == 0
|
||||
assert box.currentText() == "All levels"
|
||||
assert box.itemData(0) is None
|
||||
assert "CONSOLE_LOG" not in [box.itemText(i) for i in range(box.count())]
|
||||
log_panel._toolbar.set_level(LogLevel.SUCCESS)
|
||||
qtbot.waitUntil(lambda: log_panel._proxy.rowCount() == 1, timeout=200)
|
||||
log_panel._toolbar.set_level(None)
|
||||
qtbot.waitUntil(lambda: log_panel._proxy.rowCount() == 3, timeout=200)
|
||||
|
||||
|
||||
def test_log_panel_search_debounce_and_match_count(qtbot, log_panel: LogPanel):
|
||||
toolbar = log_panel._toolbar
|
||||
toolbar.search_textbox.setText("efgh")
|
||||
assert log_panel._proxy.rowCount() == 3 # not yet applied - debounced
|
||||
qtbot.waitUntil(lambda: log_panel._proxy.rowCount() == 1, timeout=1000)
|
||||
assert toolbar.match_label.text() == "1 / 3"
|
||||
toolbar.search_textbox.clear() # clearing applies instantly
|
||||
assert log_panel._proxy.rowCount() == 3
|
||||
assert toolbar.match_label.text() == "3"
|
||||
|
||||
|
||||
def test_log_panel_jump_button_counts_new_rows(qtbot, log_panel: LogPanel, monkeypatch):
|
||||
monkeypatch.setattr(log_panel._model, "_max_length", 50)
|
||||
log_panel.resize(600, 300)
|
||||
log_panel.show()
|
||||
qtbot.waitExposed(log_panel)
|
||||
_feed(log_panel, [make_log_msg(i) for i in range(30)])
|
||||
table = log_panel._table
|
||||
qtbot.waitUntil(lambda: table.verticalScrollBar().maximum() > 0, timeout=500)
|
||||
table.verticalScrollBar().setValue(0) # scroll up to read history
|
||||
_feed(log_panel, [make_log_msg(i) for i in range(30, 35)])
|
||||
assert table._jump_button.isVisible()
|
||||
assert table._jump_button.text() == "5 new"
|
||||
table._jump_button.click()
|
||||
qtbot.waitUntil(lambda: not table._jump_button.isVisible(), timeout=500)
|
||||
scrollbar = table.verticalScrollBar()
|
||||
assert scrollbar.value() == scrollbar.maximum()
|
||||
|
||||
|
||||
def test_log_panel_constructor_service_filter_stays_applied(qtbot, mocked_client, monkeypatch):
|
||||
monkeypatch.setattr(mocked_client.connector, "xread", lambda *_, **__: TEST_LOG_MESSAGES)
|
||||
widget = LogPanel(service_filter={"DeviceServer"})
|
||||
qtbot.addWidget(widget)
|
||||
try:
|
||||
assert widget._proxy.rowCount() == 1 # only the DeviceServer row
|
||||
assert widget._toolbar.service_button.text() == "DeviceServer"
|
||||
finally:
|
||||
widget._model.log_queue.cleanup()
|
||||
|
||||
|
||||
def test_log_panel_context_service_filters_on_fresh_panel(qtbot, log_panel: LogPanel):
|
||||
# neither action may depend on the service menu having been opened before
|
||||
log_panel._filter_service_only("DeviceServer")
|
||||
qtbot.waitUntil(lambda: log_panel._proxy.rowCount() == 1, timeout=200)
|
||||
log_panel._toolbar.set_service_selection(None)
|
||||
qtbot.waitUntil(lambda: log_panel._proxy.rowCount() == 3, timeout=200)
|
||||
log_panel._filter_service_hide("ScanServer")
|
||||
qtbot.waitUntil(lambda: log_panel._proxy.rowCount() == 1, timeout=200)
|
||||
assert log_panel._proxy.index(0, 2).data() == "DeviceServer"
|
||||
|
||||
|
||||
def test_log_panel_filter_change_does_not_show_jump_pill(qtbot, log_panel: LogPanel, monkeypatch):
|
||||
monkeypatch.setattr(log_panel._model, "_max_length", 100)
|
||||
log_panel.resize(600, 300)
|
||||
log_panel.show()
|
||||
qtbot.waitExposed(log_panel)
|
||||
_feed(log_panel, [make_log_msg(i) for i in range(40)])
|
||||
table = log_panel._table
|
||||
qtbot.waitUntil(lambda: table.verticalScrollBar().maximum() > 0, timeout=500)
|
||||
table.verticalScrollBar().setValue(0)
|
||||
log_panel._proxy.update_filter_text("m1") # tighten, then relax: no new logs arrived
|
||||
log_panel._proxy.update_filter_text("")
|
||||
assert not table._jump_button.isVisible()
|
||||
assert table._new_below == 0
|
||||
|
||||
|
||||
def test_log_panel_filter_tighten_does_not_leak_scroll_latch(qtbot, log_panel, monkeypatch):
|
||||
monkeypatch.setattr(log_panel._model, "_max_length", 100)
|
||||
log_panel.resize(600, 300)
|
||||
log_panel.show()
|
||||
qtbot.waitExposed(log_panel)
|
||||
_feed(log_panel, [make_log_msg(i) for i in range(40)])
|
||||
table = log_panel._table
|
||||
qtbot.waitUntil(lambda: table.verticalScrollBar().maximum() > 0, timeout=500)
|
||||
table.scrollToBottom()
|
||||
log_panel._proxy.update_filter_text("m") # removal-only cycle while pinned to bottom
|
||||
table.verticalScrollBar().setValue(0) # then scroll up to read
|
||||
_feed(log_panel, [make_log_msg(99)]) # a new matching log must not yank the view
|
||||
assert table.verticalScrollBar().value() == 0
|
||||
|
||||
|
||||
def test_log_panel_detail_pane_freezes_when_record_trimmed(qtbot, log_panel, monkeypatch):
|
||||
_patched_const(monkeypatch, TRIM_CHUNK=3)
|
||||
monkeypatch.setattr(log_panel._model, "_max_length", 8)
|
||||
log_panel._table.setCurrentIndex(log_panel._proxy.index(0, 0))
|
||||
log_panel._show_details()
|
||||
assert log_panel._detail_text.toPlainText() == "test debug message abcd"
|
||||
_feed(log_panel, [make_log_msg(i) for i in range(4)]) # append-only, 7 rows
|
||||
_feed(log_panel, [make_log_msg(i) for i in range(4, 8)]) # trims the shown record away
|
||||
qtbot.wait(100) # let the deferred selection sync run
|
||||
assert log_panel._detail_text.toPlainText() == "test debug message abcd"
|
||||
assert "(no longer in buffer)" in log_panel._detail_header.text()
|
||||
# an explicit click follows the selection again, out of the freeze
|
||||
log_panel._table.setCurrentIndex(log_panel._proxy.index(0, 0))
|
||||
log_panel._on_row_clicked()
|
||||
assert log_panel._detail_frozen is False
|
||||
assert "(no longer in buffer)" not in log_panel._detail_header.text()
|
||||
|
||||
|
||||
def test_log_panel_set_level_with_unlisted_level_keeps_filter(qtbot, log_panel: LogPanel):
|
||||
log_panel._proxy.update_level_filter(LogLevel.CONSOLE_LOG)
|
||||
log_panel._toolbar.set_level(LogLevel.CONSOLE_LOG) # not in the dropdown
|
||||
assert log_panel._proxy._level_num == LogLevel.CONSOLE_LOG.value # filter not wiped
|
||||
assert log_panel._toolbar.filter_level_dropdown.currentIndex() == 0
|
||||
|
||||
|
||||
def test_log_panel_colors_follow_theme(qtbot, log_panel: LogPanel):
|
||||
info_index = log_panel._model.index(1, 0)
|
||||
success_index = log_panel._model.index(2, 0)
|
||||
|
||||
Reference in New Issue
Block a user