diff --git a/.gitignore b/.gitignore index cfdc18e632fbbde1d211da73aeb9554fcb2faeb7..c067bf1a714e24661d8bc8e3a343e022e1d7663c 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,9 @@ install/ *.log *.pid +# tests +Testing/ + # auto-gen files src/app/resources.qrc src/app/qml.qrc diff --git a/extras/scripts/build-windows.py b/extras/scripts/build-windows.py index 341e6b9f4fee796084d3018cab7784381b804bf9..f12ac20182fbd6035d2401982284e1e1b9b8ec85 100644 --- a/extras/scripts/build-windows.py +++ b/extras/scripts/build-windows.py @@ -376,11 +376,19 @@ def run_tests(config_str, qt_dir): qt_dir, 'bin', 'QtWebEngineProcess.exe') os.environ["QML2_IMPORT_PATH"] = os.path.join(qt_dir, "qml") + cmd = ["ctest", "-V", "-C", config_str] + # On Windows, when running on a jenkins slave, the QML tests don't output + # anything to stdout/stderr. Workaround by outputting to a file and then + # printing the contents of the file. + if os.environ.get("JENKINS_URL"): + cmd += ["--output-log", "test.log", "--quiet"] tests_dir = os.path.join(build_dir, "tests") - if execute_cmd(["ctest", "-V", "-C", config_str], - False, None, tests_dir): - print("Tests failed.") - sys.exit(1) + exit_code = execute_cmd(cmd, False, None, tests_dir) + # Print the contents of the log file. + if os.environ.get("JENKINS_URL"): + with open(os.path.join(tests_dir, "test.log"), "r") as file: + print(file.read()) + sys.exit(exit_code) def generate_msi(version): diff --git a/src/app/commoncomponents/EmojiReactionPopup.qml b/src/app/commoncomponents/EmojiReactionPopup.qml index e7c76600f9a685b4cbdd4973d84953db2ce69d40..cc2dfbb3fbe2a63c6885efb805ec9028ad583ddc 100644 --- a/src/app/commoncomponents/EmojiReactionPopup.qml +++ b/src/app/commoncomponents/EmojiReactionPopup.qml @@ -31,7 +31,7 @@ Popup { background.visible: false parent: Overlay.overlay - property var emojiReaction + property var reactions property string msgId // center in parent @@ -88,9 +88,9 @@ Popup { spacing: 15 Layout.preferredWidth: 400 Layout.preferredHeight: childrenRect.height + 30 < 700 ? childrenRect.height + 30 : 700 - model: Object.entries(emojiReaction) + model: Object.entries(reactions) clip: true - property int modelCount: Object.entries(emojiReaction).length + property int modelCount: Object.entries(reactions).length delegate: RowLayout { width: parent.width diff --git a/src/app/commoncomponents/EmojiReactions.qml b/src/app/commoncomponents/EmojiReactions.qml index 032d08e44d227083b9221d6257375d169a591eb5..4ed741f52cd393e35311e22d7826b612cb72e679 100644 --- a/src/app/commoncomponents/EmojiReactions.qml +++ b/src/app/commoncomponents/EmojiReactions.qml @@ -24,19 +24,20 @@ import net.jami.Constants 1.1 Item { id: root - property var emojiReaction + property var reactions property real contentHeight: bubble.height property real contentWidth: bubble.width - property var emojiTexts: ownEmojiList visible: emojis.length && Body !== "" property string emojis: { + if (reactions === undefined) + return []; var space = ""; var emojiList = []; var emojiNumberList = []; - for (const reactions of Object.entries(emojiReaction)) { - var authorEmojiList = reactions[1]; + for (const reaction of Object.entries(reactions)) { + var authorEmojiList = reaction[1]; for (var emojiIndex in authorEmojiList) { var emoji = authorEmojiList[emojiIndex]; if (emojiList.includes(emoji)) { @@ -60,12 +61,14 @@ Item { return cur; } - property var ownEmojiList: { + property var ownEmojis: { + if (reactions === undefined) + return []; var list = []; var index = 0; - for (const reactions of Object.entries(emojiReaction)) { - var authorUri = reactions[0]; - var authorEmojiList = reactions[1]; + for (const reaction of Object.entries(reactions)) { + var authorUri = reaction[0]; + var authorEmojiList = reaction[1]; if (CurrentAccount.uri === authorUri) { for (var emojiIndex in authorEmojiList) { list[index] = authorEmojiList[emojiIndex]; diff --git a/src/app/commoncomponents/MessageOptionsPopup.qml b/src/app/commoncomponents/MessageOptionsPopup.qml index 8e1b05da50832e58222ed0ef39212aa18bfc9f14..9a32d41a5e19b69a64f2f2a8f4541f15829f7717 100644 --- a/src/app/commoncomponents/MessageOptionsPopup.qml +++ b/src/app/commoncomponents/MessageOptionsPopup.qml @@ -33,9 +33,11 @@ Popup { padding: 0 background.visible: false + required property var emojiReactions + property var emojiReplied: emojiReactions.ownEmojis + required property string msgId required property string msgBody - required property var emojiReplied required property bool isOutgoing required property int type required property string transferName @@ -107,27 +109,11 @@ Popup { onClosed: if (emojiPicker) emojiPicker.closeEmojiPicker() function getModel() { - var model = ["ðŸ‘", "👎", "😂"] - var cur = [] - //Add emoji reacted - var index = 0 - for (let emoji of emojiReplied) { - if (index < model.length) { - cur[index] = emoji - index ++ - } - } - //complete with default model - var modelIndex = cur.length - for (let j = 0; j < model.length; j++) { - if (cur.length < model.length) { - if (!cur.includes(model[j]) ) { - cur[modelIndex] = model[j] - modelIndex ++ - } - } - } - return cur + const defaultModel = ["ðŸ‘", "👎", "😂"] + const reactedEmojis = Array.isArray(emojiReplied) ? emojiReplied.slice(0, defaultModel.length) : [] + const uniqueEmojis = Array.from(new Set(reactedEmojis)) + const missingEmojis = defaultModel.filter(emoji => !uniqueEmojis.includes(emoji)) + return uniqueEmojis.concat(missingEmojis) } Rectangle { @@ -167,7 +153,7 @@ Popup { background: Rectangle { anchors.fill: parent - opacity: emojiReplied.includes(modelData) ? 1 : 0 + opacity: emojiReplied ? (emojiReplied.includes(modelData) ? 1 : 0) : 0 color: JamiTheme.emojiReactPushButtonColor radius: 10 } diff --git a/src/app/commoncomponents/SBSMessageBase.qml b/src/app/commoncomponents/SBSMessageBase.qml index 8fc642c92b51b24ba8b2342b97cfeeb71b275323..88988c086eead355b0db274414416cc1da2a3ce1 100644 --- a/src/app/commoncomponents/SBSMessageBase.qml +++ b/src/app/commoncomponents/SBSMessageBase.qml @@ -15,12 +15,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ - import QtQuick import QtQuick.Controls import QtQuick.Layouts import Qt5Compat.GraphicalEffects - import net.jami.Models 1.1 import net.jami.Adapters 1.1 import net.jami.Constants 1.1 @@ -64,9 +62,7 @@ Control { // If the ListView attached properties are not available, // then the root delegate is likely a Loader. - readonly property ListView listView: ListView.view ? - ListView.view : - parent.ListView.view + readonly property ListView listView: ListView.view ? ListView.view : parent.ListView.view rightPadding: hPadding leftPadding: hPadding @@ -99,7 +95,7 @@ Control { id: username text: UtilsAdapter.getBestNameForUri(CurrentAccount.id, Author) font.bold: true - visible:(seq === MsgSeq.first || seq === MsgSeq.single) && !isOutgoing + visible: (seq === MsgSeq.first || seq === MsgSeq.single) && !isOutgoing font.pixelSize: JamiTheme.usernameBlockFontSize color: JamiTheme.chatviewUsernameColor lineHeight: JamiTheme.usernameBlockLineHeight @@ -108,7 +104,6 @@ Control { } } - Item { id: replyItem property bool isSelf: ReplyToAuthor === CurrentAccount.uri @@ -123,14 +118,15 @@ Control { Layout.leftMargin: isOutgoing ? undefined : JamiTheme.sbsMessageBaseReplyMargin Layout.rightMargin: !isOutgoing ? undefined : JamiTheme.sbsMessageBaseReplyMargin - transform: Translate { y: JamiTheme.sbsMessageBaseReplyBottomMargin } - + transform: Translate { + y: JamiTheme.sbsMessageBaseReplyBottomMargin + } ColumnLayout { width: parent.width spacing: 2 - RowLayout{ + RowLayout { id: replyToLayout Layout.alignment: isOutgoing ? Qt.AlignRight : Qt.AlignLeft @@ -155,8 +151,8 @@ Control { showPresenceIndicator: false imageId: { if (replyItem.isSelf) - return CurrentAccount.id - return ReplyToAuthor + return CurrentAccount.id; + return ReplyToAuthor; } mode: replyItem.isSelf ? Avatar.Mode.Account : Avatar.Mode.Contact } @@ -179,11 +175,10 @@ Control { color: replyItem.isSelf ? CurrentConversation.color : JamiTheme.messageInBgColor radius: msgRadius - Layout.preferredWidth: replyToRow.width + 2*JamiTheme.preferredMarginSize - Layout.preferredHeight: replyToRow.height + 2*JamiTheme.preferredMarginSize + Layout.preferredWidth: replyToRow.width + 2 * JamiTheme.preferredMarginSize + Layout.preferredHeight: replyToRow.height + 2 * JamiTheme.preferredMarginSize Layout.alignment: isOutgoing ? Qt.AlignRight : Qt.AlignLeft - // place actual content here ReplyToRow { id: replyToRow @@ -194,8 +189,8 @@ Control { MouseArea { z: 2 anchors.fill: parent - onClicked: function(mouse) { - CurrentConversation.scrollToMsg(ReplyTo) + onClicked: function (mouse) { + CurrentConversation.scrollToMsg(ReplyTo); } } } @@ -211,7 +206,7 @@ Control { Item { id: avatarBlock - Layout.preferredWidth: isOutgoing ? 0 : avatar.width + hPadding/3 + Layout.preferredWidth: isOutgoing ? 0 : avatar.width + hPadding / 3 Layout.preferredHeight: isOutgoing ? 0 : bubble.height Avatar { id: avatar @@ -238,7 +233,7 @@ Control { hoverEnabled: true onClicked: function (mouse) { if (root.hoveredLink) { - MessagesAdapter.openUrl(root.hoveredLink) + MessagesAdapter.openUrl(root.hoveredLink); } } property bool bubbleHovered: containsMouse || textHovered @@ -276,30 +271,24 @@ Control { anchors.verticalCenter: parent.verticalCenter anchors.right: isOutgoing ? optionButtonItem.right : undefined anchors.left: !isOutgoing ? optionButtonItem.left : undefined - visible: CurrentAccount.type !== Profile.Type.SIP && Body !== "" && - ( - bubbleArea.bubbleHovered - || hovered - || reply.hovered - || bgHandler.hovered - ) + visible: CurrentAccount.type !== Profile.Type.SIP && Body !== "" && (bubbleArea.bubbleHovered || hovered || reply.hovered || bgHandler.hovered) source: JamiResources.more_vert_24dp_svg width: optionButtonItem.width / 2 height: optionButtonItem.height onClicked: { - var component = Qt.createComponent("qrc:/commoncomponents/MessageOptionsPopup.qml") + var component = Qt.createComponent("qrc:/commoncomponents/MessageOptionsPopup.qml"); var obj = component.createObject(bubble, { - "emojiReplied": Qt.binding(() => emojiReaction.emojiTexts), - "isOutgoing": isOutgoing, - "msgId": Id, - "msgBody": Body, - "type": Type, - "transferName": TransferName, - "msgBubble": bubble, - "listView": listView - }) - obj.open() + "emojiReactions": emojiReactions, + "isOutgoing": isOutgoing, + "msgId": Id, + "msgBody": Body, + "type": Type, + "transferName": TransferName, + "msgBubble": bubble, + "listView": listView + }); + obj.open(); } } @@ -315,17 +304,11 @@ Control { anchors.verticalCenter: parent.verticalCenter anchors.right: isOutgoing ? more.left : undefined anchors.left: !isOutgoing ? more.right : undefined - visible: CurrentAccount.type !== Profile.Type.SIP && Body !== "" && - ( - bubbleArea.bubbleHovered - || hovered - || more.hovered - || bgHandler.hovered - ) + visible: CurrentAccount.type !== Profile.Type.SIP && Body !== "" && (bubbleArea.bubbleHovered || hovered || more.hovered || bgHandler.hovered) onClicked: { - MessagesAdapter.editId = "" - MessagesAdapter.replyToId = Id + MessagesAdapter.editId = ""; + MessagesAdapter.replyToId = Id; } } } @@ -335,18 +318,18 @@ Control { property bool isEdited: PreviousBodies.length !== 0 visible: !IsEmojiOnly - z:-1 + z: -1 out: isOutgoing type: seq isReply: root.isReply function getBaseColor() { - var baseColor = isOutgoing ? CurrentConversation.color : JamiTheme.messageInBgColor + var baseColor = isOutgoing ? CurrentConversation.color : JamiTheme.messageInBgColor; if (Id === MessagesAdapter.replyToId || Id === MessagesAdapter.editId) { // If we are replying to or editing the message - return Qt.darker(baseColor, 1.5) + return Qt.darker(baseColor, 1.5); } - return baseColor + return baseColor; } color: getBaseColor() @@ -358,7 +341,6 @@ Control { height: innerContent.childrenRect.height + (visible ? root.extraHeight : 0) } - Rectangle { id: bg @@ -412,8 +394,8 @@ Control { target: CurrentConversation function onScrollTo(id) { if (id !== root.id) - return - selectAnimation.start() + return; + selectAnimation.start(); } } } @@ -444,10 +426,10 @@ Control { width: { if (root.readers.length === 0) - return 0 - var nbAvatars = root.readers.length - var margin = JamiTheme.avatarReadReceiptSize / 3 - return nbAvatars * JamiTheme.avatarReadReceiptSize - (nbAvatars - 1) * margin + return 0; + var nbAvatars = root.readers.length; + var margin = JamiTheme.avatarReadReceiptSize / 3; + return nbAvatars * JamiTheme.avatarReadReceiptSize - (nbAvatars - 1) * margin; } height: JamiTheme.avatarReadReceiptSize @@ -458,20 +440,20 @@ Control { } EmojiReactions { - id: emojiReaction + id: emojiReactions property bool isOutgoing: Author === CurrentAccount.uri Layout.alignment: isOutgoing ? Qt.AlignRight : Qt.AlignLeft Layout.rightMargin: isOutgoing ? status.width : undefined Layout.leftMargin: !isOutgoing ? avatarBlock.width : undefined - Layout.topMargin: - contentHeight/4 + Layout.topMargin: -contentHeight / 4 Layout.preferredHeight: contentHeight + 5 Layout.preferredWidth: contentWidth - emojiReaction: Reactions + reactions: Reactions TapHandler { onTapped: { - reactionPopup.open() + reactionPopup.open(); } } } @@ -483,10 +465,10 @@ Control { orientation: ListView.Horizontal Layout.preferredHeight: { if (showTime || seq === MsgSeq.last) - return contentHeight + timestampItem.contentHeight + return contentHeight + timestampItem.contentHeight; else if (readsMultiple.visible) - return JamiTheme.avatarReadReceiptSize - return 0 + return JamiTheme.avatarReadReceiptSize; + return 0; } ReadStatus { @@ -494,14 +476,14 @@ Control { visible: root.readers.length > 1 && CurrentAccount.sendReadReceipt width: { if (root.readers.length === 0) - return 0 - var nbAvatars = root.readers.length - var margin = JamiTheme.avatarReadReceiptSize / 3 - return nbAvatars * JamiTheme.avatarReadReceiptSize - (nbAvatars - 1) * margin + return 0; + var nbAvatars = root.readers.length; + var margin = JamiTheme.avatarReadReceiptSize / 3; + return nbAvatars * JamiTheme.avatarReadReceiptSize - (nbAvatars - 1) * margin; } anchors.right: parent.right - anchors.top : parent.top + anchors.top: parent.top anchors.topMargin: 1 readers: root.readers } @@ -511,7 +493,7 @@ Control { EmojiReactionPopup { id: reactionPopup - emojiReaction: Reactions + reactions: Reactions msgId: Id } } diff --git a/tests/qml/resources.qrc b/tests/qml/resources.qrc index 97f685ad3c8d07ddc291f4278a85e7dff2be1bf0..969b3a28cca7737057e4e252a5c1db531b9b3124 100644 --- a/tests/qml/resources.qrc +++ b/tests/qml/resources.qrc @@ -3,6 +3,7 @@ <file>src/tst_LocalAccount.qml</file> <file>src/tst_WizardView.qml</file> <file>src/tst_NewSwarmPage.qml</file> + <file>src/tst_MessageOptions.qml</file> <file>src/resources/gif_test.gif</file> <file>src/resources/gz_test.gz</file> <file>src/resources/png_test.png</file> diff --git a/tests/qml/src/tst_MessageOptions.qml b/tests/qml/src/tst_MessageOptions.qml new file mode 100644 index 0000000000000000000000000000000000000000..4fb25db379a3d7c8cd824034efbaefd1a456e1ba --- /dev/null +++ b/tests/qml/src/tst_MessageOptions.qml @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2023 Savoir-faire Linux Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import QtQuick +import QtTest + +import net.jami.Adapters 1.1 +import net.jami.Models 1.1 +import net.jami.Constants 1.1 +import net.jami.Enums 1.1 + +import "../../../src/app/" +import "../../../src/app/commoncomponents" + +Item { + id: uut + + // Mock reactions + EmojiReactions { + id: emojiReactions + } + + // Mock bubble item + Item { + id: bubble + } + + // Mock listview + JamiListView { + id: listView + } + + property int id + function getId() { + id += 1; + return "test" + id; + } + + function getOptionsPopup(isOutgoing, id, body, type, transferName) { + var component = Qt.createComponent("qrc:/commoncomponents/MessageOptionsPopup.qml"); + var obj = component.createObject(bubble, { + "emojiReactions": emojiReactions, + "isOutgoing": isOutgoing, + "msgId": id, + "msgBody": body, + "type": type, + "transferName": transferName, + "msgBubble": bubble, + "listView": listView + }); + return obj; + } + + SignalSpy { + id: accountAdded + + target: AccountAdapter + signalName: "accountAdded" + } + + TestCase { + name: "Test message options popup instantiation" + when: windowShown + + function test_createMessageOptionsPopup() { + // Create an account and set it as current account + AccountAdapter.createSIPAccount({ + "username": "currentAccountUsername" + }); + // Block on account creation + accountAdded.wait(1000); + + // Add some emoji reactions (one from current account uri, one from another uri) + emojiReactions.reactions = { + "currentAccountUsername": ["ðŸŒ"], + "notCurrentAccountUri": ["🌮"] + }; + + var optionsPopup = getOptionsPopup(true, getId(), "test", 0, "test"); + verify(optionsPopup !== null, "Message options popup should be created"); + + // Check if the popup is visible once opened. + optionsPopup.open(); + verify(optionsPopup.visible, "Message options popup should be visible"); + + // Check that emojiReplied has our emoji. + verify(JSON.stringify(optionsPopup.emojiReplied) === JSON.stringify(["ðŸŒ"]), + "Message options popup should have emoji replied"); + } + } +}