diff --git a/.gitignore b/.gitignore index c8878d858bb..0c23d28b310 100644 --- a/.gitignore +++ b/.gitignore @@ -42,9 +42,7 @@ nim_status_client.log test/ui-test/testSuites/suite_status/config.xml test/ui-test/testSuites/suite_status/envvars -test/ui-test/testSuites/suite_status/shared/scripts/__pycache__/* -test/ui-test/testSuites/suite_status/shared/scripts/sections/__pycache__/* - +test/ui-test/**/__pycache__/* # CPP app ===================================================================== diff --git a/test/ui-test/src/drivers/SquishDriver.py b/test/ui-test/src/drivers/SquishDriver.py index 69124f6731b..5cb45de966a 100755 --- a/test/ui-test/src/drivers/SquishDriver.py +++ b/test/ui-test/src/drivers/SquishDriver.py @@ -8,6 +8,7 @@ # * \brief It contains generic Status view components definitions and Squish driver API. # *****************************************************************************/ from enum import Enum +import sys # IMPORTANT: It is necessary to import manually the Squish drivers module by module. # More info in: https://kb.froglogic.com/display/KB/Article+-+Using+Squish+functions+in+your+own+Python+modules+or+packages @@ -44,13 +45,23 @@ def is_loaded(objName: str): except LookupError: return False, obj -def is_Visible(objName: str): +# It tries to find if the object with given objectName is currently displayed (visible and enabled): +# It returns True in case it is found. Otherwise, false. +def is_found(objName: str): try: squish.findObject(getattr(names, objName)) return True except LookupError: return False - + +# It waits for the object with given objectName to appear in the UI (visible and enabled): +# It returns True in case it appears without exceeding a specific timeout. Otherwise, false. +def is_displayed(objName: str): + try: + squish.waitForObject(getattr(names, objName)) + return True + except LookupError: + return False # It checks if the given object is visible and enabled. def is_visible_and_enabled(obj): @@ -149,3 +160,49 @@ def wait_for_object_and_type(objName: str, text: str): return True except LookupError: return False + +# Clicking link in label / textedit +def click_link(objName: str, link: str): + point = _find_link(getattr(names, objName), link) + if point[0] != -1 and point[1] != -1: + squish.mouseClick(getattr(names, objName), point[0], point[1], 0, squish.Qt.LeftButton) + +# Global properties for getting link / hovered handler management: +_expected_link = None +_link_found = False + +def _handle_link_hovered(obj, link): + global _link_found + if link == _expected_link: + _link_found = True + +# It registers to hovered handler and moves mouse around a specific object. +# Return: If handler is executed, link has been found and the position of the link is returned. Otherwise, it returns position [-1, -1] +def _find_link(objName: str, link: str): + global _expected_link + global _link_found + _expected_link = link + _link_found = False + obj = squish.waitForObject(objName) + + # Inject desired function into main module: + sys.modules['__main__']._handle_link_hovered = _handle_link_hovered + squish.installSignalHandler(obj, "linkHovered(QString)", "_handle_link_hovered") + + # Start moving the cursor: + squish.mouseMove(obj, int(obj.x), int(obj.y)) + end_x = obj.x + obj.width + end_y = obj.y + obj.height + y = int(obj.y) + while y < end_y: + x = int(obj.x) + while x < end_x: + squish.mouseMove(obj, x, y) + if _link_found: + squish.uninstallSignalHandler(obj, "linkHovered(QString)", "_handle_link_hovered") + return [x - obj.x, y - obj.y] + x += 10 + y += 10 + + squish.uninstallSignalHandler(obj, "linkHovered(QString)", "_handle_link_hovered") + return [-1, -1] \ No newline at end of file diff --git a/test/ui-test/src/drivers/SquishDriverVerification.py b/test/ui-test/src/drivers/SquishDriverVerification.py index 18af12b01e4..6f548b366e7 100644 --- a/test/ui-test/src/drivers/SquishDriverVerification.py +++ b/test/ui-test/src/drivers/SquishDriverVerification.py @@ -41,3 +41,9 @@ def verify_text_does_not_contain(text: str, substring: str): def verify_text(text1: str, text2: str): test.compare(text1, text2, "Text 1: " + text1 + "\nText 2: " + text2) + +def verify_failure(errorMsg: str): + test.fail(errorMsg) + +def log(text: str): + test.log(text) diff --git a/test/ui-test/src/screens/SettingsScreen.py b/test/ui-test/src/screens/SettingsScreen.py index 930e0c92926..cf9efa69e8a 100644 --- a/test/ui-test/src/screens/SettingsScreen.py +++ b/test/ui-test/src/screens/SettingsScreen.py @@ -38,7 +38,7 @@ def __init__(self): verify_screen(SidebarComponents.ADVANCED_OPTION.value) def activate_open_wallet_settings(self): - if not (is_Visible(SidebarComponents.WALLET_ITEM.value)) : + if not (is_found(SidebarComponents.WALLET_ITEM.value)) : click_obj_by_name(SidebarComponents.ADVANCED_OPTION.value) click_obj_by_name(AdvancedOptionScreen.ACTIVATE_OR_DEACTIVATE_WALLET.value) click_obj_by_name(AdvancedOptionScreen.I_UNDERSTAND_POP_UP.value) @@ -47,7 +47,7 @@ def activate_open_wallet_settings(self): click_obj_by_name(SidebarComponents.WALLET_ITEM.value) def activate_open_wallet_section(self): - if not (is_Visible(SidebarComponents.WALLET_ITEM.value)): + if not (is_found(SidebarComponents.WALLET_ITEM.value)): click_obj_by_name(SidebarComponents.ADVANCED_OPTION.value) click_obj_by_name(AdvancedOptionScreen.ACTIVATE_OR_DEACTIVATE_WALLET.value) click_obj_by_name(AdvancedOptionScreen.I_UNDERSTAND_POP_UP.value) diff --git a/test/ui-test/src/screens/StatusChatScreen.py b/test/ui-test/src/screens/StatusChatScreen.py index af2ecc83baa..493aa33db80 100644 --- a/test/ui-test/src/screens/StatusChatScreen.py +++ b/test/ui-test/src/screens/StatusChatScreen.py @@ -8,6 +8,7 @@ # * \brief Chat Screen. # *****************************************************************************/ +import re from enum import Enum from drivers.SquishDriver import * @@ -15,6 +16,8 @@ from drivers.SDKeyboardCommands import * from common.Common import * +_MENTION_SYMBOL = "@" +_LINK_HREF_REGEX = '' class ChatComponents(Enum): MESSAGE_INPUT = "chatView_messageInput" @@ -25,6 +28,10 @@ class ChatComponents(Enum): REPLY_TO_MESSAGE_BUTTON = "chatView_replyToMessageButton" DELETE_MESSAGE_BUTTON = "chatView_DeleteMessageButton" CONFIRM_DELETE_MESSAGE_BUTTON = "chatButtonsPanelConfirmDeleteMessageButton_StatusButton" + SUGGESTIONS_BOX = "chatView_SuggestionBoxPanel" + SUGGESTIONS_LIST = "chatView_suggestion_ListView" + MENTION_PROFILE_VIEW = "chatView_userMentioned_ProfileView" + class ChatMessagesHistory(Enum): CHAT_CREATED_TEXT = 1 @@ -35,6 +42,9 @@ class StatusChatScreen: def __init__(self): verify_screen(ChatComponents.MESSAGE_INPUT.value) verify_screen(ChatComponents.TOOLBAR_INFO_BUTTON.value) + + def chat_loaded(self): + verify(is_displayed(ChatComponents.LAST_MESSAGE_TEXT.value), "Checking chat is loaded by looking if last message is displayed.") # Screen actions region: def send_message(self, message: str): @@ -53,6 +63,30 @@ def verify_last_message_sent_is_not(self, message: str): test.passes("Success: No message was found") return verify_text_does_not_contain(str(last_message_obj.text), str(message)) + + # This method expects to have just one mention / link in the last chat message + def verify_last_message_sent_contains_mention(self, displayName: str, message: str): + [loaded, last_message_obj] = is_loaded_visible_and_enabled(ChatComponents.LAST_MESSAGE_TEXT.value) + + if loaded: + # Verifying mention + verify_text_contains(str(last_message_obj.text), displayName) + + # Verifying message + verify_text_contains(str(last_message_obj.text), message) + + # Get link value from chat text: + try: + href_info = re.search(_LINK_HREF_REGEX, str(last_message_obj.text)).group(1) + except AttributeError: + # not found in the original string + verify_failure("Mention link not found in last chat message.") + + click_link(ChatComponents.LAST_MESSAGE_TEXT.value, href_info) + verify(is_found(ChatComponents.MENTION_PROFILE_VIEW.value), "Checking user mentioned profile popup is open.") + + else: + verify_failure("No messages found in chat.") def verify_chat_title(self, title: str): info_btn = get_obj(ChatComponents.TOOLBAR_INFO_BUTTON.value) @@ -113,7 +147,36 @@ def delete_message_at_index(self, index: int): def cannot_delete_last_message(self): [loaded, last_message_obj] = is_loaded_visible_and_enabled(ChatComponents.LAST_MESSAGE_TEXT.value) if not loaded: - test.fail("No message found") + verify_failure("No message found") return hover_obj(last_message_obj) object_not_enabled(ChatComponents.DELETE_MESSAGE_BUTTON.value) + + + def send_message_with_mention(self, displayName: str, message: str): + self.do_mention(displayName) + self.send_message(message) + + def cannot_do_mention(self, displayName: str): + self.chat_loaded() + type(ChatComponents.MESSAGE_INPUT.value, _MENTION_SYMBOL + displayName) + displayed = is_displayed(ChatComponents.SUGGESTIONS_BOX.value) + verify(displayed == False , "Checking suggestion box is not displayed when trying to mention a non existing user.") + + def do_mention(self, displayName: str): + self.chat_loaded() + type(ChatComponents.MESSAGE_INPUT.value, _MENTION_SYMBOL + displayName) + displayed = is_displayed(ChatComponents.SUGGESTIONS_BOX.value) + verify(displayed, "Checking suggestion box displayed when trying to do a mention") + [loaded, suggestions_list] = is_loaded_visible_and_enabled(ChatComponents.SUGGESTIONS_LIST.value) + verify(suggestions_list.count > 0, "Checking if suggestion list is greater than 0") + found = False + if loaded: + for index in range(suggestions_list.count): + user_mention = suggestions_list.itemAtIndex(index) + if user_mention.objectName == displayName: + found = True + click_obj(user_mention) + break + verify(found, "Checking if the following display name is in the mention's list: " + displayName) + diff --git a/test/ui-test/testSuites/suite_status/shared/scripts/sections/chat_names.py b/test/ui-test/testSuites/suite_status/shared/scripts/sections/chat_names.py index 547bedc3dc2..2b07d5c394a 100644 --- a/test/ui-test/testSuites/suite_status/shared/scripts/sections/chat_names.py +++ b/test/ui-test/testSuites/suite_status/shared/scripts/sections/chat_names.py @@ -18,6 +18,9 @@ chatView_DeleteMessageButton = {"container": chatView_log, "objectName": "chatDeleteMessageButton", "type": "StatusFlatRoundButton"} chatButtonsPanelConfirmDeleteMessageButton_StatusButton = {"container": statusDesktop_mainWindow_overlay, "objectName": "chatButtonsPanelConfirmDeleteMessageButton", "type": "StatusButton"} mark_as_Read_StatusMenuItemDelegate = {"container": statusDesktop_mainWindow_overlay, "objectName": "chatMarkAsReadMenuItem", "type": "StatusMenuItemDelegate", "visible": True} +chatView_SuggestionBoxPanel ={"container": statusDesktop_mainWindow, "objectName": "suggestionsBox", "type": "SuggestionBoxPanel"} +chatView_suggestion_ListView ={"container": chatView_SuggestionBoxPanel, "objectName": "suggestionBoxList", "type": "StatusListView"} +chatView_userMentioned_ProfileView ={"container": statusDesktop_mainWindow_overlay, "objectName": "profileView", "type": "ProfileView"} # Join chat popup: startChat_Btn = {"container": statusDesktop_mainWindow_overlay, "objectName": "startChatButton", "type": "StatusButton"} diff --git a/test/ui-test/testSuites/suite_status/shared/steps/chatSteps.py b/test/ui-test/testSuites/suite_status/shared/steps/chatSteps.py index 8c34ab5ff96..8a9b2c1f2a0 100644 --- a/test/ui-test/testSuites/suite_status/shared/steps/chatSteps.py +++ b/test/ui-test/testSuites/suite_status/shared/steps/chatSteps.py @@ -13,6 +13,7 @@ @When("user joins chat room |any|") def step(context, room): _statusMain.join_chat_room(room) + _statusChat.chat_loaded() @When("the user creates a group chat adding users") def step(context): @@ -22,6 +23,10 @@ def step(context): @When("the user clicks on |any| chat") def step(context, chatName): _statusMain.open_chat(chatName) + +@When("the user inputs a mention to |any| with message |any|") +def step(context,displayName,message): + _statusChat.send_message_with_mention(displayName, message) @Then("user is able to send chat message") def step(context): @@ -85,3 +90,10 @@ def step(context, message): def step(context): _statusChat.verify_last_message_sent_is_not(context.userData["randomMessage"]) +@Then("the user cannot input a mention to a not existing user |any|") +def step(context, displayName): + _statusChat.cannot_do_mention(displayName) + +@Then("the |any| mention with message |any| have been sent") +def step(context,displayName,message): + _statusChat.verify_last_message_sent_contains_mention(displayName, message) diff --git a/test/ui-test/testSuites/suite_status/tst_ChatFlow/test.feature b/test/ui-test/testSuites/suite_status/tst_ChatFlow/test.feature index 177d3ddd1c5..ad553f3f5a1 100644 --- a/test/ui-test/testSuites/suite_status/tst_ChatFlow/test.feature +++ b/test/ui-test/testSuites/suite_status/tst_ChatFlow/test.feature @@ -4,7 +4,8 @@ # https://cucumber.io/docs/gherkin/reference/ Feature: Status Desktop Chat - As a user I want to join a room and chat. + # TODO The complete feature / all scenarios have a chance to fail since they rely on the mailserver (at least, to verify a chat is loaded, something in the history needs to be displayed). + As a user I want to join a room and chat and do basic interactions. The following scenarios cover basic chat flows. @@ -13,7 +14,7 @@ Feature: Status Desktop Chat When user signs up with username tester123 and password TesTEr16843/!@00 Then the user lands on the signed in app - Scenario: User joins a room and chats + Scenario: User joins a public room and chats When user joins chat room test Then user is able to send chat message | message | @@ -54,3 +55,19 @@ Feature: Status Desktop Chat # Scenario: User cannot delete another user's message # When user joins chat room test # Then the user cannot delete the last message + + Scenario Outline: The user can do a mention + When user joins chat room test + And the user inputs a mention to with message + Then the mention with message have been sent + Examples: + | displayName | message | + | tester123 | testing mention | + + Scenario Outline: The user can not do a mention to not existing users + When user joins chat room test + Then the user cannot input a mention to a not existing user + Examples: + | displayName | + | notExistingAccount | + | asdfgNoNo | \ No newline at end of file diff --git a/ui/app/AppLayouts/Chat/panels/SuggestionBoxPanel.qml b/ui/app/AppLayouts/Chat/panels/SuggestionBoxPanel.qml index 44574825b15..43f302c58e1 100644 --- a/ui/app/AppLayouts/Chat/panels/SuggestionBoxPanel.qml +++ b/ui/app/AppLayouts/Chat/panels/SuggestionBoxPanel.qml @@ -119,6 +119,7 @@ Rectangle { StatusListView { id: listView + objectName: "suggestionBoxList" model: mentionsListDelegate keyNavigationEnabled: true anchors.fill: parent @@ -194,6 +195,7 @@ Rectangle { delegate: Rectangle { id: itemDelegate + objectName: model.name color: ListView.isCurrentItem ? Style.current.backgroundHover : Style.current.transparent border.width: 0 width: parent.width diff --git a/ui/imports/shared/status/StatusChatInput.qml b/ui/imports/shared/status/StatusChatInput.qml index 8cf7178bb9e..9368d0ae194 100644 --- a/ui/imports/shared/status/StatusChatInput.qml +++ b/ui/imports/shared/status/StatusChatInput.qml @@ -689,6 +689,7 @@ Rectangle { SuggestionBoxPanel { id: suggestionsBox + objectName: "suggestionsBox" model: control.usersStore ? control.usersStore.usersModel : [] x : messageInput.x y: -height - Style.current.smallPadding diff --git a/ui/imports/shared/views/ProfileView.qml b/ui/imports/shared/views/ProfileView.qml index 8cae43775c1..12c9f4a9488 100644 --- a/ui/imports/shared/views/ProfileView.qml +++ b/ui/imports/shared/views/ProfileView.qml @@ -82,6 +82,7 @@ Rectangle { signal contactRemoved(publicKey: string) signal nicknameEdited(publicKey: string) + objectName: "profileView" implicitWidth: modalContent.implicitWidth implicitHeight: modalContent.implicitHeight