diff --git a/tests/ui_tools/test_buttons.py b/tests/ui_tools/test_buttons.py index 14d857139b..e80865192a 100644 --- a/tests/ui_tools/test_buttons.py +++ b/tests/ui_tools/test_buttons.py @@ -101,16 +101,22 @@ def test__decode_stream_data(self, stream_data, expected_response): {'narrow': 'stream', 'stream_id': 1}), (SERVER_URL + '/#narrow/stream/Stream.201', {'narrow': 'stream', 'stream_name': 'Stream 1'}), + (SERVER_URL + '/#narrow/stream/1-Stream-1/topic/foo.20bar', + {'narrow': 'stream:topic', 'stream_id': 1, 'topic_name': 'foo bar'}), (SERVER_URL + '/#narrow/foo', {}), (SERVER_URL + '/#narrow/stream/', {}), + (SERVER_URL + '/#narrow/stream/1-Stream-1/topic/', + {}), ], ids=[ 'modern_stream_narrow_link', 'deprecated_stream_narrow_link', + 'topic_narrow_link', 'invalid_narrow_link_1', 'invalid_narrow_link_2', + 'invalid_narrow_link_3', ] ) def test__parse_narrow_link(self, link, expected_parsed_link): @@ -123,26 +129,43 @@ def test__parse_narrow_link(self, link, expected_parsed_link): 'parsed_link', 'is_user_subscribed_to_stream', 'is_valid_stream', + 'topics_in_stream', 'expected_error' ], [ ({'narrow': 'stream', 'stream_id': 1}, True, None, + None, ''), ({'narrow': 'stream', 'stream_id': 462}, False, None, + None, 'The stream seems to be either unknown or unsubscribed'), ({'narrow': 'stream', 'stream_name': 'Stream 1'}, None, True, + None, ''), ({'narrow': 'stream', 'stream_name': 'foo'}, None, False, + None, 'Invalid stream name'), + ({'narrow': 'stream:topic', 'stream_id': 1, 'topic_name': 'Valid'}, + True, + None, + ['Valid'], + ''), + ({'narrow': 'stream:topic', 'stream_id': 1, + 'topic_name': 'Invalid'}, + True, + None, + [], + 'Invalid topic name'), ({}, + None, None, None, 'The narrow link seems to be either broken or unsupported'), @@ -152,17 +175,21 @@ def test__parse_narrow_link(self, link, expected_parsed_link): 'invalid_modern_stream_narrow_parsed_link', 'valid_deprecated_stream_narrow_parsed_link', 'invalid_deprecated_stream_narrow_parsed_link', + 'valid_topic_narrow_parsed_link', + 'invalid_topic_narrow_parsed_link', 'invalid_narrow_link', ] ) def test__validate_narrow_link(self, parsed_link, is_user_subscribed_to_stream, is_valid_stream, + topics_in_stream, expected_error): self.controller.model.is_user_subscribed_to_stream.return_value = ( is_user_subscribed_to_stream ) self.controller.model.is_valid_stream.return_value = is_valid_stream + self.controller.model.topics_in_stream.return_value = topics_in_stream mocked_button = self.message_link_button() return_value = mocked_button._validate_narrow_link(parsed_link) @@ -193,17 +220,24 @@ def test__get_stream_data(self, stream_dict, stream_id, stream_name, @pytest.mark.parametrize([ 'parsed_link', 'narrow_to_stream_called', + 'narrow_to_topic_called', ], [ ({'narrow': 'stream', 'stream_id': 1}, + True, + False), + ({'narrow': 'stream:topic', 'stream_id': 1, 'topic_name': 'Foo'}, + False, True), ], ids=[ 'stream_narrow', + 'topic_narrow', ] ) def test__switch_narrow_to(self, mocker, parsed_link, - narrow_to_stream_called): + narrow_to_stream_called, + narrow_to_topic_called): mocker.patch(BUTTONS + '.MessageLinkButton._get_stream_data', return_value=(1, 'Stream 1')) mocked_button = self.message_link_button() @@ -212,6 +246,8 @@ def test__switch_narrow_to(self, mocker, parsed_link, assert (mocked_button.controller.narrow_to_stream.called == narrow_to_stream_called) + assert (mocked_button.controller.narrow_to_topic.called + == narrow_to_topic_called) @pytest.mark.parametrize(['error', 'set_footer_text_called', '_switch_narrow_to_called', diff --git a/zulipterminal/ui_tools/buttons.py b/zulipterminal/ui_tools/buttons.py index 338c745026..e02f717812 100644 --- a/zulipterminal/ui_tools/buttons.py +++ b/zulipterminal/ui_tools/buttons.py @@ -333,6 +333,20 @@ def _parse_narrow_link(cls, link: str) -> ParsedNarrowLink: parsed_link['stream_id'] = stream_id return parsed_link + if (len_fragments == 5 and fragments[1] == 'stream' + and fragments[3] == 'topic'): + stream_id, stream_name = cls._decode_stream_data(fragments[2]) + topic_name = hash_util_decode(fragments[4]) + parsed_link = { + 'narrow': 'stream:topic', + 'topic_name': topic_name, + } + if stream_name: + parsed_link['stream_name'] = stream_name + if stream_id: + parsed_link['stream_id'] = stream_id + return parsed_link + return dict() def _validate_narrow_link(self, parsed_link: ParsedNarrowLink) -> str: @@ -362,6 +376,15 @@ def _validate_narrow_link(self, parsed_link: ParsedNarrowLink) -> str: if stream_name and not self.model.is_valid_stream(stream_name): return 'Invalid stream name' + if 'topic' in narrow: + topic_name = parsed_link['topic_name'] + + # Validate topic name. + if not stream_id: + stream_id = self.model.stream_id_from_name(stream_name) + if topic_name not in self.model.topics_in_stream(stream_id): + return 'Invalid topic name' + return '' def _get_stream_data(self, stream_id: Optional[int], @@ -395,6 +418,13 @@ def _switch_narrow_to(self, parsed_link: ParsedNarrowLink) -> None: cast(Optional[str], parsed_link.get('stream_name')), ) self.controller.narrow_to_stream(self) + elif 'stream:topic' == narrow: + self.stream_id, self.stream_name = self._get_stream_data( + parsed_link.get('stream_id'), + parsed_link.get('stream_name'), + ) + self.topic_name = parsed_link['topic_name'] + self.controller.narrow_to_topic(self) def handle_narrow_link(self) -> None: """