Skip to content

views: Support 'j/k', 'J/K' and 'G/end' for navigation in popups. #524

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
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
25 changes: 25 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import pytest

from zulipterminal.config.keys import keys_for_command
from zulipterminal.helper import initial_index as helper_initial_index
from zulipterminal.ui_tools.boxes import MessageBox
from zulipterminal.ui_tools.buttons import (
Expand Down Expand Up @@ -743,3 +744,27 @@ def classified_unread_counts():
99: 1
}
}

# --------------- UI Fixtures -----------------------------------------


@pytest.fixture(params=[
(key, expected_key)
for keys, expected_key in [
(keys_for_command('GO_UP'), 'up'),
(keys_for_command('GO_DOWN'), 'down'),
(keys_for_command('SCROLL_UP'), 'page up'),
(keys_for_command('SCROLL_DOWN'), 'page down'),
(keys_for_command('GO_TO_BOTTOM'), 'end'),
]
for key in keys
],
ids=lambda param: 'key:{}-expected_key:{}'.format(*param)
)
def navigation_key_expected_key_pair(request):
"""
Fixture to generate pairs of navigation keys with their respective
expected key.
The expected key is the one which is passed to the super `keypress` calls.
"""
return request.param
19 changes: 5 additions & 14 deletions tests/ui/test_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,29 +181,20 @@ def test_show_right_panel(self, mocker, view,
else:
view.body.options.assert_not_called()

@pytest.mark.parametrize('key, expected_key', [
(key, expected_key)
for keys, expected_key in [
(keys_for_command('GO_UP'), 'up'),
(keys_for_command('GO_DOWN'), 'down'),
(keys_for_command('SCROLL_UP'), 'page up'),
(keys_for_command('SCROLL_DOWN'), 'page down'),
(keys_for_command('GO_TO_BOTTOM'), 'end'),
]
for key in keys
])
def test_keypress_normal_mode_navigation(self, view, mocker,
key, expected_key):
navigation_key_expected_key_pair):
key, expected_key = navigation_key_expected_key_pair
view.users_view = mocker.Mock()
view.body = mocker.Mock()
view.user_search = mocker.Mock()
size = (20,)

super_view = mocker.patch("zulipterminal.ui.urwid.WidgetWrap.keypress")
super_keypress = mocker.patch("zulipterminal.ui.urwid.WidgetWrap"
+ ".keypress")

view.controller.editor_mode = False
view.keypress(size, key)
super_view.assert_called_once_with(size, expected_key)
super_keypress.assert_called_once_with(size, expected_key)

@pytest.mark.parametrize('key', keys_for_command('ALL_MENTIONS'))
def test_keypress_ALL_MENTIONS(self, view, mocker, key):
Expand Down
99 changes: 82 additions & 17 deletions tests/ui/test_ui_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@
from bs4 import BeautifulSoup
from urwid import AttrWrap, Columns, Padding, Text

from zulipterminal.config.keys import keys_for_command
from zulipterminal.config.keys import is_command_key, keys_for_command
from zulipterminal.helper import powerset
from zulipterminal.ui_tools.boxes import MessageBox
from zulipterminal.ui_tools.buttons import (
StreamButton, TopButton, TopicButton, UserButton,
)
from zulipterminal.ui_tools.views import (
HelpView, LeftColumnView, MessageView, MiddleColumnView, ModListWalker,
MsgInfoView, PopUpConfirmationView, RightColumnView, StreamsView,
TopicsView, UsersView,
MsgInfoView, PopUpConfirmationView, PopUpView, RightColumnView,
StreamInfoView, StreamsView, TopicsView, UsersView,
)


Expand Down Expand Up @@ -1093,6 +1093,56 @@ def test_topics_view(self, mocker, stream_button, width=40):
])


class TestPopUpView:
@pytest.fixture(autouse=True)
def pop_up_view(self, mocker):
self.controller = mocker.Mock()
self.command = 'COMMAND'
self.widget = mocker.Mock()
self.widgets = [self.widget, ]
self.list_walker = mocker.patch(VIEWS + '.urwid.SimpleFocusListWalker',
return_value=[])
self.super_init = mocker.patch(VIEWS + '.urwid.ListBox.__init__')
self.super_keypress = mocker.patch(VIEWS + '.urwid.ListBox.keypress')
self.pop_up_view = PopUpView(self.controller, self.widgets,
self.command)

def test_init(self):
assert self.pop_up_view.controller == self.controller
assert self.pop_up_view.command == self.command
self.list_walker.assert_called_once_with(self.widgets)
self.super_init.assert_called_once_with(self.pop_up_view.log)

@pytest.mark.parametrize('key', keys_for_command('GO_BACK'))
def test_keypress_GO_BACK(self, key):
size = (200, 20)
self.pop_up_view.keypress(size, key)
assert self.controller.exit_popup.called

def test_keypress_command_key(self, mocker):
size = (200, 20)
mocker.patch(VIEWS + '.is_command_key', side_effect=(
lambda command, key: command == self.command
))
self.pop_up_view.keypress(size, 'cmd_key')
assert self.controller.exit_popup.called

def test_keypress_navigation(self, mocker,
navigation_key_expected_key_pair):
key, expected_key = navigation_key_expected_key_pair
size = (200, 20)
# Patch `is_command_key` to not raise an 'Invalid Command' exception
# when its parameters are (self.command, key) as there is no
# self.command='COMMAND' command in keys.py.
mocker.patch(VIEWS + '.is_command_key', side_effect=(
lambda command, key:
False if command == self.command
else is_command_key(command, key)
))
self.pop_up_view.keypress(size, key)
self.super_keypress.assert_called_once_with(size, expected_key)


class TestHelpMenu:
@pytest.fixture(autouse=True)
def mock_external_classes(self, mocker, monkeypatch):
Expand All @@ -1112,22 +1162,13 @@ def test_keypress_GO_BACK(self, key):
self.help_view.keypress(size, key)
assert self.controller.exit_popup.called

@pytest.mark.parametrize('key, expected_key', [
(key, expected_key)
for keys, expected_key in [
(keys_for_command('GO_UP'), 'up'),
(keys_for_command('GO_DOWN'), 'down'),
(keys_for_command('SCROLL_UP'), 'page up'),
(keys_for_command('SCROLL_DOWN'), 'page down'),
(keys_for_command('GO_TO_BOTTOM'), 'end'),
]
for key in keys
])
def test_keypress_navigation(self, mocker, key, expected_key):
def test_keypress_navigation(self, mocker,
navigation_key_expected_key_pair):
key, expected_key = navigation_key_expected_key_pair
size = (200, 20)
super_view = mocker.patch(VIEWS + '.urwid.ListBox.keypress')
super_keypress = mocker.patch(VIEWS + '.urwid.ListBox.keypress')
self.help_view.keypress(size, key)
super_view.assert_called_once_with(size, expected_key)
super_keypress.assert_called_once_with(size, expected_key)


class TestPopUpConfirmationView:
Expand Down Expand Up @@ -1172,6 +1213,22 @@ def test_exit_popup_GO_BACK(self, mocker, popup_view, key):
assert self.controller.exit_popup.called


class TestStreamInfoView:
@pytest.fixture(autouse=True)
def mock_external_classes(self, mocker, monkeypatch):
self.controller = mocker.Mock()
mocker.patch(VIEWS + ".urwid.SimpleFocusListWalker", return_value=[])
self.stream_info_view = StreamInfoView(self.controller, '', '', '')

def test_keypress_navigation(self, mocker,
navigation_key_expected_key_pair):
key, expected_key = navigation_key_expected_key_pair
size = (200, 20)
super_keypress = mocker.patch(VIEWS + '.urwid.ListBox.keypress')
self.stream_info_view.keypress(size, key)
super_keypress.assert_called_once_with(size, expected_key)


class TestMsgInfoView:
@pytest.fixture(autouse=True)
def mock_external_classes(self, mocker, monkeypatch, message_fixture):
Expand Down Expand Up @@ -1241,6 +1298,14 @@ def test_height_reactions(self, message_fixture, to_vary_in_each_message):
expected_height = 8
assert self.msg_info_view.height == expected_height

def test_keypress_navigation(self, mocker,
navigation_key_expected_key_pair):
key, expected_key = navigation_key_expected_key_pair
size = (200, 20)
super_keypress = mocker.patch(VIEWS + '.urwid.ListBox.keypress')
self.msg_info_view.keypress(size, key)
super_keypress.assert_called_once_with(size, expected_key)


class TestMessageBox:
@pytest.fixture(autouse=True)
Expand Down
75 changes: 33 additions & 42 deletions zulipterminal/ui_tools/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -754,10 +754,32 @@ def keypress(self, size: urwid_Size, key: str) -> Optional[str]:
return super().keypress(size, key)


class HelpView(urwid.ListBox):
def __init__(self, controller: Any) -> None:
class PopUpView(urwid.ListBox):
def __init__(self, controller: Any, widgets: List[Any],
command: str) -> None:
self.controller = controller
self.command = command
self.log = urwid.SimpleFocusListWalker(widgets)
super().__init__(self.log)

def keypress(self, size: urwid_Size, key: str) -> str:
if is_command_key('GO_BACK', key) or is_command_key(self.command, key):
self.controller.exit_popup()
elif is_command_key('GO_UP', key):
key = 'up'
elif is_command_key('GO_DOWN', key):
key = 'down'
elif is_command_key('SCROLL_UP', key):
key = 'page up'
elif is_command_key('SCROLL_DOWN', key):
key = 'page down'
elif is_command_key('GO_TO_BOTTOM', key):
key = 'end'
return super().keypress(size, key)


class HelpView(PopUpView):
def __init__(self, controller: Any) -> None:
widths = [(len(binding['help_text'])+4,
len(", ".join(binding['keys'])))
for binding in KEY_BINDINGS.values()]
Expand Down Expand Up @@ -786,26 +808,9 @@ def __init__(self, controller: Any) -> None:
)
)

self.log = urwid.SimpleFocusListWalker(help_menu_content)
self.height = len(help_menu_content)

self.height = len(self.log)

super().__init__(self.log)

def keypress(self, size: urwid_Size, key: str) -> str:
if is_command_key('GO_BACK', key) or is_command_key('HELP', key):
self.controller.exit_popup()
elif is_command_key('GO_UP', key):
key = 'up'
elif is_command_key('GO_DOWN', key):
key = 'down'
elif is_command_key('SCROLL_UP', key):
key = 'page up'
elif is_command_key('SCROLL_DOWN', key):
key = 'page down'
elif is_command_key('GO_TO_BOTTOM', key):
key = 'end'
return super().keypress(size, key)
super().__init__(controller, help_menu_content, 'HELP')


class PopUpConfirmationView(urwid.Overlay):
Expand Down Expand Up @@ -844,26 +849,18 @@ def keypress(self, size: urwid_Size, key: str) -> str:
return super().keypress(size, key)


class StreamInfoView(urwid.ListBox):
class StreamInfoView(PopUpView):
def __init__(self, controller: Any, color: str,
name: str, desc: str) -> None:
self.controller = controller
# TODO: Width & Height handling could be improved
self.width = max(len(desc), len("# {}".format(name)))+2
self.height = 2
log = [urwid.Text(desc, align='center')]
super().__init__(log)
stream_info_content = [urwid.Text(desc, align='center')]
super().__init__(controller, stream_info_content, 'STREAM_DESC')

def keypress(self, size: Tuple[int, int], key: str) -> str:
if (is_command_key('GO_BACK', key) or
is_command_key('STREAM_DESC', key)):
self.controller.exit_popup()
return super().keypress(size, key)


class MsgInfoView(urwid.ListBox):
class MsgInfoView(PopUpView):
def __init__(self, controller: Any, msg: Message) -> None:
self.controller = controller
self.msg = msg

if msg['reactions']:
Expand Down Expand Up @@ -891,18 +888,12 @@ def __init__(self, controller: Any, msg: Message) -> None:
self.width = sum(max_widths)
self.height = len(msg_info['Reactions'])+4 if msg['reactions'] else 5

self.log = urwid.SimpleListWalker(
[urwid.AttrWrap(
msg_info_content = [urwid.AttrWrap(
urwid.Columns([
urwid.Text(field),
(max_widths[1], urwid.Text(data))
], dividechars=2),
None if index % 2 else 'bar')
for index, (field, data) in enumerate(msg_info.items())])
for index, (field, data) in enumerate(msg_info.items())]

super().__init__(self.log)

def keypress(self, size: urwid_Size, key: str) -> str:
if is_command_key('GO_BACK', key) or is_command_key('MSG_INFO', key):
self.controller.exit_popup()
return super().keypress(size, key)
super().__init__(controller, msg_info_content, 'MSG_INFO')