Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions tests/ui/test_ui_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -1221,6 +1221,77 @@ def test_topics_view(self, mocker, stream_button):
]
)

def test_update_stream_view_restores_focus_by_stream_id(self, mocker):
mocker.patch(VIEWS + ".LeftColumnView.streams_view")
mocker.patch(VIEWS + ".LeftColumnView.menu_view")
mocker.patch(VIEWS + ".LeftColumnView.show_stream_view")
left_col_view = LeftColumnView(self.view)
left_col_view.is_in_topic_view = False

# Simulate focus on a stream widget with stream_id=42 at index 3.
focused_widget = mocker.Mock(stream_id=42)
self.view.stream_w = mocker.Mock()
self.view.stream_w.log.get_focus.return_value = (focused_widget, 3)
self.view.stream_w.stream_search_box.edit_text = ""

# After rebuild, stream_id=42 moved to index 1 (e.g. another stream
# was pinned and pushed to the top).
new_widgets = [
mocker.Mock(stream_id=99),
mocker.Mock(stream_id=42),
mocker.Mock(stream_id=7),
]
self.view.stream_w.log.__iter__ = mocker.Mock(return_value=iter(new_widgets))

left_col_view.update_stream_view()

# Focus should follow stream_id=42 to its new position (index 1),
# not stay at the old index (3).
self.view.stream_w.log.set_focus.assert_called_once_with(1)
left_col_view.show_stream_view.assert_called_once()

def test_update_stream_view_restores_search_text(self, mocker):
mocker.patch(VIEWS + ".LeftColumnView.streams_view")
mocker.patch(VIEWS + ".LeftColumnView.menu_view")
mocker.patch(VIEWS + ".LeftColumnView.show_stream_view")
left_col_view = LeftColumnView(self.view)
left_col_view.is_in_topic_view = False

focused_widget = mocker.Mock(stream_id=10)
self.view.stream_w = mocker.Mock()
self.view.stream_w.log.get_focus.return_value = (focused_widget, 0)
self.view.stream_w.stream_search_box.edit_text = "foo"

new_widgets = [mocker.Mock(stream_id=10)]
self.view.stream_w.log.__iter__ = mocker.Mock(return_value=iter(new_widgets))

left_col_view.update_stream_view()

self.view.stream_w.stream_search_box.set_edit_text.assert_called_once_with(
"foo"
)

def test_update_stream_view_no_set_focus_when_stream_not_found(self, mocker):
mocker.patch(VIEWS + ".LeftColumnView.streams_view")
mocker.patch(VIEWS + ".LeftColumnView.menu_view")
mocker.patch(VIEWS + ".LeftColumnView.show_stream_view")
left_col_view = LeftColumnView(self.view)
left_col_view.is_in_topic_view = False

# Focus was on stream_id=42 which no longer exists after rebuild.
focused_widget = mocker.Mock(stream_id=42)
self.view.stream_w = mocker.Mock()
self.view.stream_w.log.get_focus.return_value = (focused_widget, 2)
self.view.stream_w.stream_search_box.edit_text = ""

new_widgets = [mocker.Mock(stream_id=99), mocker.Mock(stream_id=7)]
self.view.stream_w.log.__iter__ = mocker.Mock(return_value=iter(new_widgets))

left_col_view.update_stream_view()

# Should not call set_focus since stream_id=42 is gone.
self.view.stream_w.log.set_focus.assert_not_called()


class TestTabView:
@pytest.fixture
Expand Down
27 changes: 25 additions & 2 deletions zulipterminal/ui_tools/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,12 @@ def __init__(self, streams_btn_list: List[Any], view: Any) -> None:
self.log = urwid.SimpleFocusListWalker(streams_btn_list)
self.streams_btn_list = streams_btn_list
self.focus_index_before_search = 0
# Initialize search_lock before super().__init__() to prevent an
# AttributeError if an external pinning event triggers update_streams
# (via @asynch) before construction completes (see issue #1487).
self.search_lock = threading.Lock()
self.empty_search = False

list_box = urwid.ListBox(self.log)
self.stream_search_box = PanelSearchBox(
self, "SEARCH_STREAMS", self.update_streams
Expand All @@ -333,8 +339,6 @@ def __init__(self, streams_btn_list: List[Any], view: Any) -> None:
[self.stream_search_box, urwid.Divider(SECTION_DIVIDER_LINE)]
),
)
self.search_lock = threading.Lock()
self.empty_search = False

@asynch
def update_streams(self, search_box: Any, new_text: str) -> None:
Expand Down Expand Up @@ -898,7 +902,26 @@ def is_in_topic_view_with_stream_id(self, stream_id: int) -> bool:
)

def update_stream_view(self) -> None:
old_stream_id = None
old_search_text = ""
if hasattr(self.view, "stream_w"):
widget, _ = self.view.stream_w.log.get_focus()
if widget is not None and hasattr(widget, "stream_id"):
old_stream_id = widget.stream_id
old_search_text = self.view.stream_w.stream_search_box.edit_text

self.stream_v = self.streams_view()

# Restore focus by finding the same stream in the rebuilt list.
if old_stream_id is not None:
for i, widget in enumerate(self.view.stream_w.log):
if hasattr(widget, "stream_id") and widget.stream_id == old_stream_id:
self.view.stream_w.log.set_focus(i)
break

if old_search_text:
self.view.stream_w.stream_search_box.set_edit_text(old_search_text)

if not self.is_in_topic_view:
self.show_stream_view()

Expand Down
Loading