diff --git a/.gitignore b/.gitignore
index c8878d858b..0c23d28b31 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 c5276a82df..e272dc39c9 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):
@@ -151,3 +162,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 53c61beef6..bf684fcc38 100644
--- a/test/ui-test/src/drivers/SquishDriverVerification.py
+++ b/test/ui-test/src/drivers/SquishDriverVerification.py
@@ -88,3 +88,9 @@ def verify_the_app_is_closed(pid: int):
def verify_equals(val1, val2):
test.compare(val1, val2, "1st value [" + str(val1) + ("] equal to " if val1 == val2 else "] NOT equal to ") + "2nd value [" + str(val2) + "]")
+
+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 833f9cadf6..6fe83181cd 100644
--- a/test/ui-test/src/screens/SettingsScreen.py
+++ b/test/ui-test/src/screens/SettingsScreen.py
@@ -53,7 +53,7 @@ class SettingsScreen:
click_obj_by_name(SidebarComponents.WALLET_ITEM.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)
@@ -62,7 +62,7 @@ class SettingsScreen:
self.open_wallet_settings()
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 af2ecc83ba..493aa33db8 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.SquishDriverVerification import *
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 @@ class StatusChatScreen:
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 @@ class StatusChatScreen:
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 547bedc3dc..2b07d5c394 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_replyToMessageButton = {"container": chatView_log, "objectName": "reply
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 8c34ab5ff9..8a9b2c1f2a 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 @@ _statusCreateChatView = StatusCreateChatScreen()
@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 177d3ddd1c..3b2ec5ebd5 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 44574825b1..43f302c58e 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 8cf7178bb9..9368d0ae19 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 8cae43775c..12c9f4a948 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