Repository: zulip/zulip-terminal
Pull request #1088 (2021-07-18 14:59:03)

Simplify Topbutton attributes
<!-- Please see https://github.com/zulip/zulip-terminal#contributor-guidelines ! -->

**What does this PR do?**  <!-- Overall description goes here -->
This PR creates TopButton attributes `prefix_markup`,
`label_markup` and `suffix_markup` with Type `urwidMarkupTuple`.
All of the other attributes can be written in terms of these
which simplifies the logic in various methods ad reads better.
<!-- If fixing a filed bug or new feature, add 'Fixes #<issue>' or 'Partial fix for #<issue>' -->
This PR formed after splitting of the last commit of #1064 and is a follow-up to it.
<!-- Add a link to a discussion on chat.zulip.org, if relevant -->

**Tested?** <!-- Fine to leave some of these unchecked if this is a draft/work-in-progress -->
- [x] Manually
- [x] Existing tests (adapted, if necessary)
- [x] New tests added (for any new behavior)
- [x] Passed linting & tests (each commit)
<!-- Code must pass CI (GitHub Actions) before merging - look for the green tick! -->

<!-- See https://github.com/zulip/zulip-terminal#commit-style -->
**Commit flow** <!-- if more than one commit; add/delete/fill-in as appropriate -->
<!-- For example:
- first commit doing some thing
- maybe multiple commits doing similar things
-->
**Commit 1-3**: refactors TopButton attributes.
**Commit 4**: button: Add muted style to stream prefix. 

**Notes & Questions** <!-- if any; add/delete/fill-in as appropriate -->
<!-- For example:
- this doesn't include feature X (yet?)
- unsure about Y
- should this do Z?
-->
The old attributes are kept and deleted in the following commits to keep the diffs cleaner and have separate commits.
I haven't handled the count Exception that happens because of `self.count`. I realize I shouldn't have removed it.

**Visual changes** <!-- if any; add/delete/fill-in with screenshot/diagram as appropriate -->
![image](https://user-images.githubusercontent.com/64144419/126071952-3cff2b38-b2c0-476d-bcc3-b61688841786.png)
![image](https://user-images.githubusercontent.com/64144419/126072018-5894981b-0f74-4ec4-b46a-2d7130f976b0.png)


Rohitth007: There is a minor issue I missed. I shouldn't have removed `self.count` I'll add it back soon.
zulipbot: Hello @zulip/server-refactoring members, this pull request was labeled with the "area: refactoring" label, so you may want to check it out!

<!-- areaLabelAddition -->
zulipbot: Heads up @Rohitth007, we just merged some commits that conflict with the changes your made in this pull request! You can review this repository's [recent commits](https://github.com/zulip/zulip-terminal/commits/main) to see where the conflicts occur. Please rebase your feature branch against the `upstream/main` branch and [resolve](https://zulip.readthedocs.io/en/latest/git/troubleshooting.html#recover-from-a-git-rebase-failure) your pull request's merge conflicts accordingly.

<!-- mergeConflictWarning -->

Rohitth007: @neiljp I've split up the commits even further as suggested. Hope all your review points were addressed as well.
neiljp: @Rohitth007 I pushed the first few commits independently yesterday as those ending https://github.com/zulip/zulip-terminal/commit/7eb3ca4e2a17a11a8cd8a7267b38c0de9192679c

After pushing an intermediate commit to migrate some obvious urwid_MarkupTuple candidates, I've now looked at the rest of the PR and will push back here to merge, except for the prefix length restriction - we can discuss this later. The muted stream prefix style is a small enough feature that I'm happy to include it here.

Thanks for working on this - this makes it much clearer and cleaner :+1:
The structure of changing one interface at a time made it much simpler to review.

Patch:
diff --git a/zulipterminal/ui_tools/buttons.py b/zulipterminal/ui_tools/buttons.py
index b50faf3..93db71b 100644
--- a/zulipterminal/ui_tools/buttons.py
+++ b/zulipterminal/ui_tools/buttons.py
@@ -4,7 +4,7 @@ UI buttons for 'narrowing' and showing unread counts, such as Stream, PM, Topic,
 
 import re
 from functools import partial
-from typing import Any, Callable, Dict, List, Optional, Tuple, Union, cast
+from typing import Any, Callable, Dict, List, Optional, Tuple, cast
 from urllib.parse import urljoin, urlparse
 
 import urwid
@@ -24,20 +24,18 @@ class TopButton(urwid.Button):
         self,
         *,
         controller: Any,
-        caption: str,
+        prefix_markup: urwid_MarkupTuple = (None, ""),
+        label_markup: urwid_MarkupTuple,
+        suffix_markup: urwid_MarkupTuple = (None, ""),
         show_function: Callable[[], Any],
-        prefix_character: Union[str, Tuple[Any, str]] = "\N{BULLET}",
-        text_color: Optional[str] = None,
         count: int = 0,
-        count_style: Optional[str] = None,
     ) -> None:
         self.controller = controller
-        self._caption = caption
+        self._prefix_markup = prefix_markup
+        self._label_markup = label_markup
+        self._suffix_markup = suffix_markup
         self.show_function = show_function
-        self.prefix_character = prefix_character
-        self.original_color = text_color
         self.count = count
-        self.count_style = count_style
 
         super().__init__("")
 
@@ -55,36 +53,54 @@ class TopButton(urwid.Button):
         )
         self._w = urwid.AttrMap(cols, None, "selected")
 
-        self.update_count(count, text_color)
+        self.update_count(count)
 
         urwid.connect_signal(self, "click", self.activate)
 
-    def update_count(self, count: int, text_color: Optional[str] = None) -> None:
-        new_color = self.original_color if text_color is None else text_color
+    def _set_prefix_style(self, style: str) -> None:
+        self._prefix_markup = (style, self._prefix_markup[1])
 
-        self.count = count
-        if count == 0:
-            count_text = ""
-        else:
-            count_text = str(count)
+    def _set_label_style(self, style: str) -> None:
+        self._label_markup = (style, self._label_markup[1])
+
+    def _set_suffix_style(self, style: str) -> None:
+        self._suffix_markup = (style, self._suffix_markup[1])
+
+    prefix_style = property(lambda self: self._prefix_markup[0], _set_prefix_style)
+    label_style = property(lambda self: self._label_markup[0], _set_label_style)
+    suffix_style = property(lambda self: self._suffix_markup[0], _set_suffix_style)
+
+    def _set_prefix_text(self, text: str) -> None:
+        self._prefix_markup = (self._prefix_markup[0], text)
 
-        self.update_widget((self.count_style, count_text), new_color)
+    def _set_label_text(self, text: str) -> None:
+        self._label_markup = (self._label_markup[0], text)
+
+    def _set_suffix_text(self, text: str) -> None:
+        self._suffix_markup = (self._suffix_markup[0], text)
+
+    prefix_text = property(lambda self: self._prefix_markup[1], _set_prefix_text)
+    label_text = property(lambda self: self._label_markup[1], _set_label_text)
+    suffix_text = property(lambda self: self._suffix_markup[1], _set_suffix_text)
+
+    def update_count(self, count: int) -> None:
+        self.count = count
+        self.suffix_text = "" if count == 0 else str(count)
+        self.update_widget()
 
-    def update_widget(
-        self, count_text: urwid_MarkupTuple, text_color: Optional[str]
-    ) -> Any:
-        if self.prefix_character:
-            prefix = [" ", self.prefix_character, " "]
+    def update_widget(self) -> Any:
+        if self.prefix_text:
+            prefix = [" ", self._prefix_markup, " "]
         else:
             prefix = [" "]
-        if count_text[1]:
-            suffix = [" ", count_text, " "]
+        if self.suffix_text:
+            suffix = [" ", self._suffix_markup, " "]
         else:
             suffix = ["  "]
         self.button_prefix.set_text(prefix)
-        self.set_label(self._caption)
+        self.set_label(self.label_text)
         self.button_suffix.set_text(suffix)
-        self._w.set_attr_map({None: text_color})
+        self._w.set_attr_map({None: self.label_style})
 
     def activate(self, key: Any) -> None:
         self.controller.view.show_left_panel(visible=False)
@@ -106,11 +122,10 @@ class HomeButton(TopButton):
 
         super().__init__(
             controller=controller,
-            caption=button_text,
+            label_markup=(None, button_text),
+            suffix_markup=("unread_count", ""),
             show_function=controller.narrow_to_all_messages,
-            prefix_character="",
             count=count,
-            count_style="unread_count",
         )
 
 
@@ -120,11 +135,10 @@ class PMButton(TopButton):
 
         super().__init__(
             controller=controller,
-            caption=button_text,
+            label_markup=(None, button_text),
+            suffix_markup=("unread_count", ""),
             show_function=controller.narrow_to_all_pm,
-            prefix_character="",
             count=count,
-            count_style="unread_count",
         )
 
 
@@ -134,11 +148,10 @@ class MentionedButton(TopButton):
 
         super().__init__(
             controller=controller,
-            caption=button_text,
+            label_markup=(None, button_text),
+            suffix_markup=("unread_count", ""),
             show_function=controller.narrow_to_all_mentions,
-            prefix_character="",
             count=count,
-            count_style="unread_count",
         )
 
 
@@ -148,11 +161,10 @@ class StarredButton(TopButton):
 
         super().__init__(
             controller=controller,
-            caption=button_text,
+            label_markup=(None, button_text),
+            suffix_markup=("starred_count", ""),
             show_function=controller.narrow_to_all_starred,
-            prefix_character="",
             count=count,  # Number of starred messages, not unread count
-            count_style="starred_count",
         )
 
 
@@ -197,11 +209,11 @@ class StreamButton(TopButton):
         )
         super().__init__(
             controller=controller,
-            caption=self.stream_name,
+            prefix_markup=(self.color, stream_marker),
+            label_markup=(None, self.stream_name),
+            suffix_markup=("unread_count", ""),
             show_function=narrow_function,
-            prefix_character=(self.color, stream_marker),
             count=count,
-            count_style="unread_count",
         )
 
         # Mark muted streams 'M' during button creation.
@@ -209,10 +221,17 @@ class StreamButton(TopButton):
             self.mark_muted()
 
     def mark_muted(self) -> None:
-        self.update_widget(("muted", MUTE_MARKER), "muted")
+        self.prefix_style = "muted"
+        self.label_style = "muted"
+        self.suffix_style = "muted"
+        self.suffix_text = MUTE_MARKER
+        self.update_widget()
         self.view.home_button.update_count(self.model.unread_counts["all_msg"])
 
     def mark_unmuted(self, unread_count: int) -> None:
+        self.prefix_style = self.color
+        self.label_style = None
+        self.suffix_style = "unread_count"
         self.update_count(unread_count)
         self.view.home_button.update_count(self.model.unread_counts["all_msg"])
 
@@ -252,14 +271,15 @@ class UserButton(TopButton):
 
         super().__init__(
             controller=controller,
-            caption=user["full_name"],
+            prefix_markup=(color, state_marker),
+            label_markup=(color, user["full_name"]),
             show_function=self._narrow_with_compose,
-            prefix_character=(color, state_marker),
-            text_color=color,
             count=count,
         )
         if is_current_user:
-            self.update_widget(("current_user", "(you)"), color)
+            self.suffix_style = "current_user"
+            self.suffix_text = "(you)"
+            self.update_widget()
 
     def _narrow_with_compose(self) -> None:
         # Switches directly to composing with user
@@ -307,18 +327,21 @@ class TopicButton(TopButton):
 
         super().__init__(
             controller=controller,
-            caption=topic_name,
+            prefix_markup=(None, topic_prefix),
+            label_markup=(None, topic_name),
+            suffix_markup=("unread_count", ""),
             show_function=narrow_function,
-            prefix_character=topic_prefix,
             count=count,
-            count_style="unread_count",
         )
 
         if controller.model.is_muted_topic(self.stream_id, self.topic_name):
             self.mark_muted()
 
     def mark_muted(self) -> None:
-        self.update_widget(("muted", MUTE_MARKER), "muted")
+        self.label_style = "muted"
+        self.suffix_style = "muted"
+        self.suffix_text = MUTE_MARKER
+        self.update_widget()
 
     # TODO: Handle event-based approach for topic-muting.
 
@@ -346,27 +369,27 @@ class EmojiButton(TopButton):
         self.reaction_count = reaction_count
         self.toggle_selection = toggle_selection
         self.emoji_name, self.emoji_code, self.aliases = emoji_unit
-        full_button_caption = ", ".join([self.emoji_name, *self.aliases])
+        full_button_label = ", ".join([self.emoji_name, *self.aliases])
 
         super().__init__(
             controller=controller,
-            caption=full_button_caption,
-            prefix_character="",
+            label_markup=(None, full_button_label),
             show_function=self.update_emoji_button,
         )
 
         has_check_mark = self._has_user_reacted_to_msg() or is_selected(self.emoji_name)
-        self.update_widget((None, self.get_update_widget_text(has_check_mark)), None)
+        self.update_check_mark(has_check_mark)
 
     def _has_user_reacted_to_msg(self) -> bool:
         return self.controller.model.has_user_reacted_to_message(
             self.message, emoji_code=self.emoji_code
         )
 
-    def get_update_widget_text(self, user_reacted: bool) -> str:
+    def update_check_mark(self, user_reacted: bool) -> None:
         count_text = str(self.reaction_count) if self.reaction_count > 0 else ""
         reacted_check_mark = CHECK_MARK if user_reacted else ""
-        return f" {reacted_check_mark} {count_text} "
+        self.suffix_text = f" {reacted_check_mark} {count_text} "
+        self.update_widget()
 
     def mouse_event(
         self, size: urwid_Size, event: str, button: int, col: int, row: int, focus: int
@@ -387,7 +410,7 @@ class EmojiButton(TopButton):
             if is_reaction_added
             else (self.reaction_count - 1)
         )
-        self.update_widget((None, self.get_update_widget_text(is_reaction_added)), None)
+        self.update_check_mark(is_reaction_added)
 
 
 class DecodedStream(TypedDict):


Test patch:
diff --git a/tests/ui_tools/test_buttons.py b/tests/ui_tools/test_buttons.py
index cae834b..f842574 100644
--- a/tests/ui_tools/test_buttons.py
+++ b/tests/ui_tools/test_buttons.py
@@ -1,4 +1,4 @@
-from typing import Any, Callable, Dict, List, Optional, Tuple
+from typing import Any, Callable, Dict, List, Optional, Tuple, Union
 
 import pytest
 from pytest import param as case
@@ -19,7 +19,7 @@ from zulipterminal.ui_tools.buttons import (
     TopicButton,
     UserButton,
 )
-from zulipterminal.urwid_types import urwid_Size
+from zulipterminal.urwid_types import urwid_MarkupTuple, urwid_Size
 
 
 MODULE = "zulipterminal.ui_tools.buttons"
@@ -40,9 +40,9 @@ class TestTopButton:
     def top_button(self, mocker: MockerFixture) -> TopButton:
         top_button = TopButton(
             controller=self.controller,
-            caption="caption",
+            prefix_markup=("style", "-"),
+            label_markup=(None, "label"),
             show_function=self.show_function,
-            prefix_character="-",
             count=0,
         )
         return top_button
@@ -50,12 +50,11 @@ class TestTopButton:
     def test_init(self, mocker: MockerFixture, top_button: TopButton) -> None:
 
         assert top_button.controller == self.controller
-        assert top_button._caption == "caption"
+        assert top_button._prefix_markup == ("style", "-")
+        assert top_button._label_markup == (None, "label")
+        assert top_button._suffix_markup == (None, "")
         assert top_button.show_function == self.show_function
-        assert top_button.prefix_character == "-"
-        assert top_button.original_color is None
         assert top_button.count == 0
-        assert top_button.count_style is None
 
         assert top_button._label.wrap == "ellipsis"
         assert top_button._label.get_cursor_coords("size") is None
@@ -74,36 +73,75 @@ class TestTopButton:
             top_button, "click", top_button.activate
         )
 
-    @pytest.mark.parametrize("text_color", ["color", None])
+    def test_style_properties(self, top_button: TopButton) -> None:
+        top_button._prefix_markup = ("prefix-style", "prefix-text")
+        top_button._label_markup = ("label-style", "label-text")
+        top_button._suffix_markup = ("suffix-style", "suffix-text")
+
+        # Test getter
+        prefix_style = top_button.prefix_style
+        assert prefix_style == "prefix-style"
+        label_style = top_button.label_style
+        assert label_style == "label-style"
+        suffix_style = top_button.suffix_style
+        assert suffix_style == "suffix-style"
+
+        # Test setter
+        top_button.prefix_style = "prefix-style2"
+        assert top_button.prefix_style == "prefix-style2"
+        top_button.label_style = "label-style2"
+        assert top_button.label_style == "label-style2"
+        top_button.suffix_style = "suffix-style2"
+        assert top_button.suffix_style == "suffix-style2"
+
+    def test_text_properties(self, top_button: TopButton) -> None:
+        top_button._prefix_markup = ("prefix-style", "prefix-text")
+        top_button._label_markup = ("label-style", "label-text")
+        top_button._suffix_markup = ("suffix-style", "suffix-text")
+
+        # Test getter
+        prefix_text = top_button.prefix_text
+        assert prefix_text == "prefix-text"
+        label_text = top_button.label_text
+        assert label_text == "label-text"
+        suffix_text = top_button.suffix_text
+        assert suffix_text == "suffix-text"
+
+        # Test setter
+        top_button.prefix_text = "prefix-text2"
+        assert top_button.prefix_text == "prefix-text2"
+        top_button.label_text = "label-text2"
+        assert top_button.label_text == "label-text2"
+        top_button.suffix_text = "suffix-text2"
+        assert top_button.suffix_text == "suffix-text2"
+
     @pytest.mark.parametrize(
-        "old_count, new_count, new_count_str",
-        [(10, 11, "11"), (0, 1, "1"), (11, 10, "10"), (1, 0, "")],
+        "new_count, new_count_str", [(11, "11"), (1, "1"), (10, "10"), (0, "")]
     )
     def test_update_count(
         self,
         mocker: MockerFixture,
         top_button: TopButton,
-        old_count: int,
         new_count: int,
         new_count_str: str,
-        text_color: Optional[str],
     ) -> None:
-        top_button.count = old_count
         top_button_update_widget = mocker.patch(MODULE + ".TopButton.update_widget")
 
-        top_button.update_count(new_count, text_color)
+        top_button.update_count(new_count)
 
-        top_button_update_widget.assert_called_once_with(
-            (top_button.count_style, new_count_str),
-            text_color,
-        )
+        top_button_update_widget.assert_called_once_with()
+        assert top_button.suffix_text == new_count_str
 
     @pytest.mark.parametrize(
-        "prefix, expected_prefix", [("-", [" ", "-", " "]), ("", [" "])]
+        "prefix_markup, expected_prefix_markup",
+        [
+            ((None, "-"), [" ", (None, "-"), " "]),
+            ((None, ""), [" "]),
+        ],
     )
-    @pytest.mark.parametrize("text_color", ["color", None])
+    @pytest.mark.parametrize("label_markup", [("color", "caption"), (None, "caption")])
     @pytest.mark.parametrize(
-        "count_text, expected_suffix",
+        "suffix_markup, expected_suffix_markup",
         [
             (("color", "3"), [" ", ("color", "3"), " "]),
             (("color", ""), ["  "]),
@@ -115,24 +153,30 @@ class TestTopButton:
         self,
         mocker: MockerFixture,
         top_button: TopButton,
-        prefix: str,
-        expected_prefix: List[str],
-        text_color: Optional[str],
-        count_text: Tuple[Optional[str], str],
-        expected_suffix: List[Any],
+        prefix_markup: urwid_MarkupTuple,
+        expected_prefix_markup: List[Union[str, urwid_MarkupTuple]],
+        label_markup: urwid_MarkupTuple,
+        suffix_markup: urwid_MarkupTuple,
+        expected_suffix_markup: List[Union[str, urwid_MarkupTuple]],
     ) -> None:
-        top_button.prefix_character = prefix
+        top_button._prefix_markup = prefix_markup
+        top_button._label_markup = label_markup
+        top_button._suffix_markup = suffix_markup
         top_button.button_prefix = mocker.patch(MODULE + ".urwid.Text")
         top_button.set_label = mocker.patch(MODULE + ".urwid.Button.set_label")
         top_button.button_suffix = mocker.patch(MODULE + ".urwid.Text")
         set_attr_map = mocker.patch.object(top_button._w, "set_attr_map")
 
-        top_button.update_widget(count_text, text_color)
+        top_button.update_widget()
 
-        top_button.button_prefix.set_text.assert_called_once_with(expected_prefix)
-        top_button.set_label.assert_called_once_with(top_button._caption)
-        top_button.button_suffix.set_text.assert_called_once_with(expected_suffix)
-        set_attr_map.assert_called_once_with({None: text_color})
+        top_button.button_prefix.set_text.assert_called_once_with(
+            expected_prefix_markup
+        )
+        top_button.set_label.assert_called_once_with(top_button.label_text)
+        top_button.button_suffix.set_text.assert_called_once_with(
+            expected_suffix_markup
+        )
+        set_attr_map.assert_called_once_with({None: top_button.label_style})
 
 
 class TestStarredButton:
@@ -140,7 +184,7 @@ class TestStarredButton:
         self, mocker: MockerFixture, count: int = 10
     ) -> None:
         starred_button = StarredButton(controller=mocker.Mock(), count=count)
-        assert starred_button.count_style == "starred_count"
+        assert starred_button.suffix_style == "starred_count"
 
 
 class TestStreamButton:
@@ -151,7 +195,11 @@ class TestStreamButton:
 
         stream_button.mark_muted()
 
-        update_widget.assert_called_once_with(("muted", MUTE_MARKER), "muted")
+        assert stream_button.prefix_style == "muted"
+        assert stream_button.label_style == "muted"
+        assert stream_button.suffix_style == "muted"
+        assert stream_button.suffix_text == MUTE_MARKER
+        update_widget.assert_called_once_with()
 
     def test_mark_unmuted(
         self, mocker: MockerFixture, stream_button: StreamButton
@@ -162,6 +210,9 @@ class TestStreamButton:
 
         stream_button.mark_unmuted(unread_count)
 
+        assert stream_button.prefix_style == stream_button.color
+        assert stream_button.label_style is None
+        assert stream_button.suffix_style == "unread_count"
         update_count.assert_called_once_with(unread_count)
 
     @pytest.mark.parametrize("key", keys_for_command("TOGGLE_TOPIC"))
@@ -206,14 +257,14 @@ class TestUserButton:
         mocker: MockerFixture,
         enter_key: str,
         widget_size: Callable[[Widget], urwid_Size],
-        caption: str = "some user",
+        full_name: str = "some user",
         email: str = "some_email",
         user_id: int = 5,
     ) -> None:
         user: Dict[str, Any] = {
             "email": email,
             "user_id": user_id,
-            "full_name": caption,
+            "full_name": full_name,
         }
         activate = mocker.patch(MODULE + ".UserButton.activate")
         user_button = UserButton(
@@ -274,9 +325,9 @@ class TestEmojiButton:
     ) -> None:
         controller = mocker.Mock()
         controller.model.has_user_reacted_to_message = mocker.Mock(return_value=False)
-        update_widget = mocker.patch(MODULE + ".EmojiButton.update_widget")
+        update_widget = mocker.patch(MODULE + ".EmojiButton.update_check_mark")
         top_button = mocker.patch(MODULE + ".TopButton.__init__")
-        caption = ", ".join([emoji_unit[0], *emoji_unit[2]])
+        label = ", ".join([emoji_unit[0], *emoji_unit[2]])
         message_fixture["reactions"] = to_vary_in_message["reactions"]
 
         emoji_button = EmojiButton(
@@ -290,8 +341,7 @@ class TestEmojiButton:
 
         top_button.assert_called_once_with(
             controller=controller,
-            caption=caption,
-            prefix_character="",
+            label_markup=(None, label),
             show_function=emoji_button.update_emoji_button,
         )
         assert emoji_button.emoji_name == emoji_unit[0]
@@ -388,10 +438,10 @@ class TestTopicButton:
         )
 
         top_button.assert_called_once_with(
-            caption=title if not is_resolved else title[2:],
-            prefix_character=" " if not is_resolved else title[:1],
+            prefix_markup=(None, " ") if not is_resolved else (None, title[:1]),
+            label_markup=(None, title) if not is_resolved else (None, title[2:]),
+            suffix_markup=("unread_count", ""),
             show_function=mocker.ANY,  # partial
-            count_style="unread_count",
             **params,
         )
         assert topic_button.stream_name == stream_name
@@ -444,7 +494,10 @@ class TestTopicButton:
 
         topic_button.mark_muted()
 
-        update_widget.assert_called_once_with(("muted", MUTE_MARKER), "muted")
+        assert topic_button.label_style == "muted"
+        assert topic_button.suffix_style == "muted"
+        assert topic_button.suffix_text == MUTE_MARKER
+        update_widget.assert_called_once_with()
 
     @pytest.mark.parametrize("key", keys_for_command("TOGGLE_TOPIC"))
     def test_keypress_EXIT_TOGGLE_TOPIC(