diff --git a/storybook/CMakeLists.txt b/storybook/CMakeLists.txt index 096e557400..0dff70631d 100644 --- a/storybook/CMakeLists.txt +++ b/storybook/CMakeLists.txt @@ -72,6 +72,7 @@ add_test(NAME FigmaModelTest COMMAND FigmaModelTest) add_executable(QmlTests qmlTests/main.cpp + qmlTests/src/TextUtils.cpp qmlTests/src/TextUtils.h ${TEST_QML_FILES}) target_compile_definitions(QmlTests PRIVATE QML_IMPORT_ROOT="${CMAKE_CURRENT_LIST_DIR}" diff --git a/storybook/qmlTests/TestComponents/tst_StatusChatInput.qml b/storybook/qmlTests/TestComponents/tst_StatusChatInput.qml index 3725870604..b241c68716 100644 --- a/storybook/qmlTests/TestComponents/tst_StatusChatInput.qml +++ b/storybook/qmlTests/TestComponents/tst_StatusChatInput.qml @@ -1,4 +1,5 @@ import QtQuick 2.14 +import QtQuick.Controls 2.14 import QtQml 2.14 import QtTest 1.0 @@ -6,16 +7,524 @@ import utils 1.0 import shared.status 1.0 import shared.stores 1.0 +import TextUtils 1.0 + Item { id: root width: 600 height: 400 + Component { + id: componentUnderTest + StatusChatInput { + property var globalUtils: globalUtilsMock + + width: parent.width + height: implicitHeight + anchors.bottom: parent.bottom + usersStore: QtObject { + property var usersModel: ListModel {} + } + } + } + + QtObject { + id: testData + readonly property var multiLineText: ["Typing on first row", "Typing on the second row", "Typing on the third row"] + } + + TestCase { + name: "StatusChatInputInitialization" + when: windowShown + + property StatusChatInput controlUnderTest: null + + function init() { + controlUnderTest = createTemporaryObject(componentUnderTest, root) + } + + //Scenario: StatusChatInput initialisation + //Given a new instance of StatusChatInput + //When there is no keyboard input + //Then the StatucChatInput raw text is empty + //But the StatusChatInput is configured to support Rich Text + function test_empty_chat_input() { + verify(controlUnderTest.textInput.length == 0, `Expected 0 text length, received: ${controlUnderTest.textInput.length}`) + verify(controlUnderTest.getPlainText() == "", `Expected empty string, received: ${controlUnderTest.getPlainText()}`) + verify(controlUnderTest.textInput.textFormat == TextEdit.RichText, "Expected text input format to be Rich") + verify(controlUnderTest.textInput.text.startsWith(" + //Then the text is displayed as it is typed + //And the input text is not modified by mentions processor + //And the input text is not modified by emoji processor + // + //Example: + //text + //(!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~) + function test_keyboard_any_ascii_input() { + var expectations_after_typed_text = (typedText) => { + verify(controlUnderTest.getPlainText() === typedText, + `Expected text: ${typedText}, received: ${controlUnderTest.getPlainText()}`) + verify(controlUnderTest.textInput.text === controlUnderTest.getTextWithPublicKeys(), + `Expected text: ${controlUnderTest.textInput.text}, received: ${controlUnderTest.getTextWithPublicKeys()}`) + } + + testHelper.when_text_is_typed(statusChatInputKeyboardInputExpectedAsText, + testHelper.get_all_ascii_characters(), + expectations_after_typed_text); + } + +// Scenario outline: User can navigate in chat input using keyboard +// Given the user has typed text on multiple lines in StatusChatInput +// """ +// Typing on first row +// Typing on the second row +// Typing on the third row +// +// """ +// When the user hits +// The cursor will move inside the text area to the +// +// Examples: +// |direction| +// |left| +// |right| +// |up| +// |down| + +// When the user hits "left" +// The cursor will move inside the text area to the "left" +// When the user hits "right" +// The cursor will move inside the text area to the "right" +// When the user hits "up" +// The cursor will move "up" inside the text area, on the previous line if any +// When the user hits "down" +// The cursor will move "down" inside the text area, on the next line if any + function test_user_can_navigate_with_keys() { + + var expect_row_to_be_added = (rowNb) => { + const expectedText = testData.multiLineText.slice(0, rowNb).join("\n") + "\n" + + compare(controlUnderTest.getPlainText(), expectedText) + compare(controlUnderTest.textInput.lineCount, rowNb + 1) + } + + testHelper.when_multiline_text_is_typed(statusChatInputKeyboardInputExpectedAsText, + testData.multiLineText, + expect_row_to_be_added, + (typedRowText) => {}) + + compare(controlUnderTest.textInput.cursorPosition, + controlUnderTest.textInput.length, + "Expected cursor position at the end of text") + + keyClick(Qt.Key_Right) + compare(controlUnderTest.textInput.cursorPosition, + controlUnderTest.textInput.length, + "Expected cursor not to move as we already are at the end of text") + + keyClick(Qt.Key_Left) + compare(controlUnderTest.textInput.cursorPosition, + controlUnderTest.textInput.length - 1, + "Expected cursor to move left") + + keyClick(Qt.Key_Up) + compare(controlUnderTest.textInput.cursorPosition, 42, + "Expected cursor to be on the on the second row, two chars from the end") + + keyClick(Qt.Key_Up) + compare(controlUnderTest.textInput.cursorPosition, 19, + "Expected cursor to be on the on end of the first line") + + keyClick(Qt.Key_Down) + compare(controlUnderTest.textInput.cursorPosition, + 42, + "Expected cursor to be back on the second row, two chars from the end") + + keyClick(Qt.Key_Down) + compare(controlUnderTest.textInput.cursorPosition, + controlUnderTest.textInput.length - 1, + "Expected cursor to be back at the end of second line") + + keyClick(Qt.Key_Down) + compare(controlUnderTest.textInput.cursorPosition, + controlUnderTest.textInput.length, + "Expected cursor to be back at the end of text") + } + +// Scenario: User can select text using keyboard +// Given the user has typed text on multiple lines in StatusChatInput +// """ +// Typing on first row +// Typing on the second row +// Typing on the third row + +// """ +// When the user holds shift key +// When the user hits +// The cursor will move inside the text area to the +// And the selected text will be + +// Examples: +// |direction| selectedText | +// |right| "" | +// |left| "\u2028" | +// |up| "ow\u2028Typing on the third row\u2028" | +// |down| "\u2028" | + function test_user_can_select_text() { + + testHelper.given_multiline_text_is_typed(statusChatInputKeyboardInputExpectedAsText, + testData.multiLineText) + + keyClick(Qt.Key_Right, Qt.ShiftModifier) + compare(controlUnderTest.textInput.selectedText, "", "No selection expected") + + keyClick(Qt.Key_Left, Qt.ShiftModifier) + compare(controlUnderTest.textInput.selectedText, "\u2028", "Expected line separator.") + + keyClick(Qt.Key_Up, Qt.ShiftModifier) + compare(controlUnderTest.textInput.selectedText, "ow\u2028Typing on the third row\u2028") + + keyClick(Qt.Key_Down, Qt.ShiftModifier) + compare(controlUnderTest.textInput.selectedText, "\u2028", "Expected line separator.") + } + +// Scenario: The user can select all text in StatusChatInput +// Given the user has typed text in StatusChatInput +// """ +// Typing on first row +// Typing on the second row +// Typing on the third row + +// """ +// When the user hits select all shortcut +// Then all the text is selected + function test_user_can_select_all_text() { + testHelper.given_multiline_text_is_typed(statusChatInputKeyboardInputExpectedAsText, + testData.multiLineText) + + keySequence(StandardKey.SelectAll) + compare(controlUnderTest.textInput.selectedText, testData.multiLineText.join("\u2028") + "\u2028") + } + +// Scenario: The user can cut text in StatusChatInput +// Given the user has selected all text in StatusChatInput +// """ +// Typing on first row +// Typing on the second row +// Typing on the third row + +// """ +// When the user hits cut shortcut +// Then all selected text is removed +// And his clipboard contains the initially selected text + function test_user_can_cut_text() { + testHelper.given_multiline_text_is_typed(statusChatInputKeyboardInputExpectedAsText, + testData.multiLineText) + + keySequence(StandardKey.SelectAll) + keySequence(StandardKey.Cut) + compare(controlUnderTest.getPlainText(), "") + + keySequence(StandardKey.Paste) + compare(controlUnderTest.getPlainText(), testData.multiLineText.join("\n") + "\n") + } + + +// Given the user has contact JohnDoe +// And the user types a message "Hello @JohnDoe!" +// Then the user can mention his contact as @JohnDoe +// And mentions suggestions openes when "@" is typed +// And the mentions suggestions will contain @JohnDoe as the mention is typed +// And the mentions suggestions will close once "!" is typed +// And the mention is sepparated by the next input once the mention is added to TextArea +// And the contact name from the mention can be replaced with "'0x0JohnDoe'" public key + function test_keyboard_mentions_input() { + testHelper.when_the_user_has_contact(controlUnderTest, "JohnDoe", (contact) => { + verify(controlUnderTest.usersStore.usersModel.count == 1, `Expected user to have 1 contact`) + }) + + let expect_visible_suggestions_on_mention_typing = (typedText) => { + if(typedText.startsWith("Hello @") && !typedText.endsWith("!")) { + verify(controlUnderTest.suggestions.visible == true, + `Expected the mention suggestions to be visible`) + + verify(controlUnderTest.suggestions.listView.currentItem.objectName === "JohnDoe", + `Expected the mention suggestions current item to be JohnDoe, received ${controlUnderTest.suggestions.listView.currentItem.objectName}`) + } + } + + testHelper.when_text_is_typed(statusChatInputKeyboardInputExpectedAsText, + "Hello @JohnDoe!", expect_visible_suggestions_on_mention_typing) + + verify(controlUnderTest.suggestions.visible == false, + `Expected the mention suggestions to be closed`) + + compare(controlUnderTest.getPlainText(), + "Hello \[\[mention\]\]@JohnDoe\[\[mention\]\] !", + "Expected the mention to be inserted") + + var textWithPubKey = controlUnderTest.getTextWithPublicKeys() + verify(textWithPubKey.includes("@0x0JohnDoe"), + "Expected @pubKey to replace @contactName") + } + } + + TestCase { + id: statusChatInputStandardKeySequence + name: "StatusChatInputStandardKeySequence" + when: windowShown + + property var controlUnderTest: null + property TextArea standardInput: null + + Component { + id: standardTextAreaComponent + TextArea { + width: 400 + height: 100 + textFormat: Qt.RichText + selectByMouse: true + } + } + + function initTestCase() { + Utils.globalUtilsInst = globalUtilsMock + } + +// Scenario: Default TextArea keyboard shortcuts are not altered by StatusChatInput +// Given a new StatusChatInput instance +// And a new standard Qt TextArea instance +// When the user is typing multiline text +// """ +// Typing on first row +// Typing on the second row +// Typing on the third row + +// """ +// And is using standard key shortcutss +// Then the StatusChatInput will behave as standard TextArea + function init() { + controlUnderTest = createTemporaryObject(componentUnderTest, root) + standardInput = createTemporaryObject(standardTextAreaComponent, root) + + standardInput.forceActiveFocus() + testHelper.given_multiline_text_is_typed(statusChatInputStandardKeySequence, testData.multiLineText) + + controlUnderTest.textInput.forceActiveFocus() + testHelper.given_multiline_text_is_typed(statusChatInputStandardKeySequence, testData.multiLineText) + + } + + function test_standard_key_shortcuts_data() { + return [ + { tag: "Delete", key: StandardKey.Delete, initialCursorPosition: 0, initialSelectionStart: 0, initialSelectionEnd: 0, expectIdenticalPlainText: true }, + { tag: "Undo", key: StandardKey.Undo, initialCursorPosition: 0, initialSelectionStart: 0, initialSelectionEnd: 0, expectIdenticalPlainText: true }, + { tag: "Redo", key: StandardKey.Redo, initialCursorPosition: 0, initialSelectionStart: 0, initialSelectionEnd: 0, expectIdenticalPlainText: true }, + { tag: "SelectAll", key: StandardKey.SelectAll, initialCursorPosition: 0, initialSelectionStart: 0, initialSelectionEnd: 0 , expectIdenticalPlainText: true }, + { tag: "Bold", key: StandardKey.Bold, initialCursorPosition: 0, initialSelectionStart: 0, initialSelectionEnd: 3, expectIdenticalPlainText: false, expectIdenticalSelection: false }, + { tag: "Italic", key: StandardKey.Italic, initialCursorPosition: 0, initialSelectionStart: 8, initialSelectionEnd: 10, expectIdenticalPlainText: false, expectIdenticalSelection: false }, + { tag: "MoveToNextChar", key: StandardKey.MoveToNextChar, initialCursorPosition: 0, initialSelectionStart: 0, initialSelectionEnd: 0, expectIdenticalPlainText: true }, + { tag: "MoveToPreviousChar", key: StandardKey.MoveToPreviousChar, initialCursorPosition: 5, initialSelectionStart: 0, initialSelectionEnd: 0, expectIdenticalPlainText: true }, + { tag: "MoveToNextWord", key: StandardKey.MoveToNextWord, initialCursorPosition: 2, initialSelectionStart: 0, initialSelectionEnd: 0, expectIdenticalPlainText: true}, + { tag: "MoveToPreviousWord", key: StandardKey.MoveToPreviousWord, initialCursorPosition: 15, initialSelectionStart: 0, initialSelectionEnd: 0, expectIdenticalPlainText: true}, + { tag: "MoveToNextLine", key: StandardKey.MoveToNextLine, initialCursorPosition: 0, initialSelectionStart: 0, initialSelectionEnd: 0, expectIdenticalPlainText: true}, + { tag: "MoveToPreviousLine", key: StandardKey.MoveToPreviousLine, initialCursorPosition: 30, initialSelectionStart: 0, initialSelectionEnd: 0, expectIdenticalPlainText: true}, + { tag: "MoveToStartOfLine", key: StandardKey.MoveToStartOfLine, initialCursorPosition: 10, initialSelectionStart: 0, initialSelectionEnd: 0, expectIdenticalPlainText: true }, + { tag: "MoveToEndOfLine", key: StandardKey.MoveToEndOfLine, initialCursorPosition: 0, initialSelectionStart: 0, initialSelectionEnd: 0, expectIdenticalPlainText: true }, + { tag: "MoveToStartOfBlock", key: StandardKey.MoveToStartOfBlock, initialCursorPosition: 40, initialSelectionStart: 0, initialSelectionEnd: 0, expectIdenticalPlainText: true }, + { tag: "MoveToEndOfBlock", key: StandardKey.MoveToEndOfBlock, initialCursorPosition: 0, initialSelectionStart: 0, initialSelectionEnd: 0, expectIdenticalPlainText: true }, + { tag: "MoveToStartOfDocument", key: StandardKey.MoveToStartOfDocument, initialCursorPosition: 40, initialSelectionStart: 0, initialSelectionEnd: 0, expectIdenticalPlainText: true }, + { tag: "MoveToEndOfDocument", key: StandardKey.MoveToEndOfDocument, initialCursorPosition: 0, initialSelectionStart: 0, initialSelectionEnd: 0, expectIdenticalPlainText: true }, + { tag: "SelectNextChar", key: StandardKey.SelectNextChar, initialCursorPosition: 5, initialSelectionStart: 0, initialSelectionEnd: 0, expectIdenticalPlainText: true }, + { tag: "SelectPreviousChar", key: StandardKey.SelectPreviousChar, initialCursorPosition: 5, initialSelectionStart: 0, initialSelectionEnd: 0, expectIdenticalPlainText: true }, + { tag: "SelectNextWord", key: StandardKey.SelectNextWord, initialCursorPosition: 2, initialSelectionStart: 0, initialSelectionEnd: 0, expectIdenticalPlainText: true }, + { tag: "SelectPreviousWord", key: StandardKey.SelectPreviousWord, initialCursorPosition: 15, initialSelectionStart: 0, initialSelectionEnd: 0, expectIdenticalPlainText: true }, + { tag: "SelectNextLine", key: StandardKey.SelectNextLine, initialCursorPosition: 0, initialSelectionStart: 0, initialSelectionEnd: 0, expectIdenticalPlainText: true }, + { tag: "SelectPreviousLine", key: StandardKey.SelectPreviousLine, initialCursorPosition: 30, initialSelectionStart: 0, initialSelectionEnd: 0, expectIdenticalPlainText: true }, + { tag: "SelectStartOfLine", key: StandardKey.SelectStartOfLine, initialCursorPosition: 0, initialSelectionStart: 0, initialSelectionEnd: 0, expectIdenticalPlainText: true }, + { tag: "SelectEndOfLine", key: StandardKey.SelectEndOfLine, initialCursorPosition: 0, initialSelectionStart: 0, initialSelectionEnd: 0, expectIdenticalPlainText: true }, + { tag: "SelectStartOfBlock", key: StandardKey.SelectStartOfBlock, initialCursorPosition: 40, initialSelectionStart: 0, initialSelectionEnd: 0, expectIdenticalPlainText: true }, + { tag: "SelectEndOfBlock", key: StandardKey.SelectEndOfBlock, initialCursorPosition: 0, initialSelectionStart: 0, initialSelectionEnd: 0, expectIdenticalPlainText: true }, + { tag: "SelectStartOfDocument", key: StandardKey.SelectStartOfDocument, initialCursorPosition: 40, initialSelectionStart: 0, initialSelectionEnd: 0, expectIdenticalPlainText: true }, + { tag: "SelectEndOfDocument", key: StandardKey.SelectEndOfDocument, initialCursorPosition: 0, initialSelectionStart: 0, initialSelectionEnd: 0, expectIdenticalPlainText: true }, + { tag: "DeleteStartOfWord", key: StandardKey.DeleteStartOfWord, initialCursorPosition: 4, initialSelectionStart: 0, initialSelectionEnd: 0, expectIdenticalPlainText: true }, + { tag: "DeleteEndOfWord", key: StandardKey.DeleteEndOfWord, initialCursorPosition: 4, initialSelectionStart: 0, initialSelectionEnd: 0, expectIdenticalPlainText: true }, + { tag: "DeleteEndOfLine", key: StandardKey.DeleteEndOfLine, initialCursorPosition: 0, initialSelectionStart: 0, initialSelectionEnd: 0, expectIdenticalPlainText: true }, + { tag: "InsertLineSeparator", key: StandardKey.InsertLineSeparator, initialCursorPosition: 0, initialSelectionStart: 0, initialSelectionEnd: 0, expectIdenticalPlainText: true }, + { tag: "Deselect", key: StandardKey.Deselect, initialCursorPosition: 0, initialSelectionStart: 0, initialSelectionEnd: 10, expectIdenticalPlainText: true }, + { tag: "DeleteCompleteLine", key: StandardKey.DeleteCompleteLine, initialCursorPosition: 0, initialSelectionStart: 0, initialSelectionEnd: 0, expectIdenticalPlainText: true }, + { tag: "Backspace", key: StandardKey.Backspace, initialCursorPosition: 5, initialSelectionStart: 0, initialSelectionEnd: 0, expectIdenticalPlainText: true }, + { tag: "AddTab", key: StandardKey.AddTab, initialCursorPosition: 0, initialSelectionStart: 0, initialSelectionEnd: 0, expectIdenticalPlainText: true } + ] + } + + function test_standard_key_shortcuts(data) { + + standardInput.forceActiveFocus() + standardInput.cursorPosition = data.initialCursorPosition + standardInput.select(data.initialSelectionStart, data.initialSelectionEnd) + + keySequence(data.key) + let standardControlSelection = standardInput.selectedText + let standardControlCursorPosition = standardInput.cursorPosition + + controlUnderTest.textInput.forceActiveFocus() + controlUnderTest.textInput.cursorPosition = data.initialCursorPosition + controlUnderTest.textInput.select(data.initialSelectionStart, data.initialSelectionEnd) + + keySequence(data.key) + let controlUnderTestSelection = controlUnderTest.textInput.selectedText + let controlUnderTestCursorPosition = controlUnderTest.textInput.cursorPosition + + if(data.expectIdenticalPlainText) { + compare(controlUnderTest.textInput.length, standardInput.length, "Expected identical text length") + compare(controlUnderTest.textInput.getText(0, controlUnderTest.textInput.length), + standardInput.getText(0, standardInput.length), + "Expected identical text") + compare(controlUnderTestCursorPosition, standardControlCursorPosition, "Expected identical cursor position") + compare(controlUnderTestSelection, standardControlSelection, "Expected identical selected text") + } else { + //the text is expected to be different due to custom processor + //Ex: bold or italic where text is wrapped in specific symbols + verify(controlUnderTest.textInput.getText(0, controlUnderTest.textInput.length) !== + standardInput.getText(0, standardInput.length), + "Expected different text") + } + } + } + + TestCase { + id: statusChatInputEmojiAndMentions + name: "StatusChatInputEmojiAndMentions" + when: windowShown + + property StatusChatInput controlUnderTest: null + + function init() { + Utils.globalUtilsInst = globalUtilsMock + controlUnderTest = createTemporaryObject(componentUnderTest, root) + } + +// Scenario: The user can type text, mention and emoji +// Given a new instance of StatusChatInput +// And the user has as contact +// When the user is typing +// Then the text is displayed in the input field as +// And the is inserted in the input field +// And the is inserted in the input field +// And the can be replaced with contact public key + +// Examples: +// text | mention | expectedPlainText +// “Hello John:D” | | “Hello John\uD83D\uDE04“ +// “Hello @JohnDoe” | @JohnDoe | “Hello @JohnDoe ” +// “Hello:D@JohnDoe“ | @JohnDoe | “Hello\uD83D\uDE04 @JohnDoe “ +// “:DHello@JohnDoe” | @JohnDoe | “\uD83D\uDE04 Hello @JohnDoe ” +// “:D:D:D:D:D:D@JohnDoe:D:D:D” | @JohnDoe | “\uD83D\uDE04 \uD83D\uDE04 \uD83D\uDE04 \uD83D\uDE04 \uD83D\uDE04 \uD83D\uDE04 @JohnDoe \uD83D\uDE04 \uD83D\uDE04 \uD83D\uDE04 ” +// “Hello:@JohnDoe1:D@JohnDoe2:D | @JohnDoe1 @JohnDoe2 | “Hello:@JohnDoe1 \uD83D\uDE04 @JohnDoe2 \uD83D\uDE04 ” +// “Hello:@JohnDoe1:D@JohnDoe2:D | | “Hello:@JohnDoe1\uD83D\uDE04 @JohnDoe2\uD83D\uDE04 ” + function test_text_mention_emoji_input_data() { + return [ + { tag: "Hello John:D", text: "Hello John:D", mention: [], expectedText: "Hello John\uD83D\uDE04 " }, + { tag: "Hello @JohnDoe", text: "Hello @JohnDoe", mention: ["JohnDoe"], expectedText: "Hello @JohnDoe " }, + { tag: "Hello:D@JohnDoe", text: "Hello:D@JohnDoe", mention: ["JohnDoe"], expectedText: "Hello\uD83D\uDE04 @JohnDoe " }, + { tag: ":DHello@JohnDoe", text: ":DHello@JohnDoe", mention: ["JohnDoe"], expectedText: "\uD83D\uDE04 Hello@JohnDoe " }, + { tag: ":D:D:D:D:D:D@JohnDoe:D:D:D", text: ":D:D:D:D:D:D@JohnDoe:D:D:D", mention: ["JohnDoe"], expectedText: "\uD83D\uDE04 \uD83D\uDE04 \uD83D\uDE04 \uD83D\uDE04 \uD83D\uDE04 \uD83D\uDE04 @JohnDoe \uD83D\uDE04 \uD83D\uDE04 \uD83D\uDE04 " }, + { tag: "Hello:@JohnDoe1:D@JohnDoe2:D with contact", text: "Hello @JohnDoe1:D@JohnDoe2:D", mention: ["JohnDoe1", "JohnDoe2"], expectedText: "Hello @JohnDoe1 \uD83D\uDE04 @JohnDoe2 \uD83D\uDE04 " }, + { tag: "Hello:@JohnDoe1:D@JohnDoe2:D without contact", text: "Hello @JohnDoe1:D@JohnDoe2:D", mention: [], expectedText: "Hello @JohnDoe1\uD83D\uDE04 @JohnDoe2\uD83D\uDE04 " }, + ] + } + + function test_text_mention_emoji_input(data) { + data.mention.forEach(contact => testHelper.when_the_user_has_contact(controlUnderTest, contact, (addedContact) => {})) + testHelper.when_text_is_typed(statusChatInputEmojiAndMentions, + data.text, (typedText) => { keyClick(Qt.Key_Shift) /*mentions will be inserted only after a key input*/}) + var plainText = controlUnderTest.removeMentions(controlUnderTest.getPlainText()) + + compare(plainText, + data.expectedText) + } + } + + QtObject { + id: testHelper + + function get_all_ascii_characters() { + let result = ''; + for( let i = 32; i <= 126; i++ ) + { + result += String.fromCharCode( i ); + } + return result + } + + function when_the_user_has_contact(controlUnderTest: StatusChatInput, contact: string, expectationAfterContactAdded) { + controlUnderTest.usersStore.usersModel.append({ + pubKey: `0x0${contact}`, + onlineStatus: 1, + isContact: true, + isVerified: true, + isAdmin: false, + isUntrustworthy: false, + displayName: contact, + alias: `${contact}-alias`, + localNickname: `${contact}-local-nickname`, + ensName: `${contact}.stateofus.eth`, + icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAAlklEQVR4nOzW0QmDQBAG4SSkl7SUQlJGCrElq9F3QdjjVhh/5nv3cFhY9vUIYQiNITSG0BhCExPynn1gWf9bx498P7/ + nzPcxEzGExhBdJGYihtAYQlO+tUZvqrPbqeudo5iJGEJjCE15a3VtodH3q2ImYgiNITTlTdG1nUZ5a92VITQxITFiJmIIjSE0htAYQrMHAAD//+wwFVpz+yqXAAAAAElFTkSuQmCC", + colorId: 7 + }) + expectationAfterContactAdded(contact) + } + + function when_text_is_typed(testCase: TestCase, textInput: string, expectationAfterTextInput) { + for (let i = 0; i < textInput.length; i++) { + const typedText = textInput.slice(0,i+1) + const key = textInput[i]; + testCase.keyClick(key) + expectationAfterTextInput(typedText) + } + } + + function when_multiline_text_is_typed(testCase: TestCase, textLines: list, expectationAfterNewLine, expectationAfterTextInput) { + for(let i = 0; i < textLines.length; i++) { + when_text_is_typed(testCase, textLines[i], expectationAfterTextInput) + testCase.keyClick(Qt.Key_Enter, Qt.ShiftModifier) + expectationAfterNewLine(i+1) + } + } + + function given_multiline_text_is_typed(testCase: TestCase, textLines: list) { + when_multiline_text_is_typed(testCase, textLines, (lineNb) => {}, (typedText) => {}) + } + } + QtObject { id: globalUtilsMock - property var plainText - property var isCompressedPubKey: function (publicKey) { + function plainText(htmlText) { + return TextUtils.htmlToPlainText(htmlText) + } + + function isCompressedPubKey(publicKey) { return false } } @@ -26,7 +535,7 @@ Item { property ListModel gifColumnA: ListModel {} readonly property var formationChars: (["*", "`", "~"]) - + property bool isTenorWarningAccepted: true function getSelectedTextWithFormationChars(messageInputField) { let i = 1 let text = "" @@ -49,31 +558,11 @@ Item { Component.onCompleted: { RootStore.isGifWidgetEnabled = true RootStore.isWalletEnabled = true - RootStore.isTenorWarningAccepted = true + RootStore.isTenorWarningAccepted = rootStoreMock.isTenorWarningAccepted RootStore.getSelectedTextWithFormationChars = rootStoreMock.getSelectedTextWithFormationChars RootStore.gifColumnA = rootStoreMock.gifColumnA - } - } - StatusChatInput { - id: controlUnderTest - width: parent.width - property var globalUtils: globalUtilsMock - Component.onCompleted: { Global.dragArea = root } } - - TestCase { - name: "TestChatInputInitialization" - when: windowShown - - function test_empty_chat_input() { - globalUtilsMock.plainText = (htmlText) => { - return "" - } - verify(controlUnderTest.textInput.length == 0, `Expected 0 text length, received: ${controlUnderTest.textInput.length}`) - verify(controlUnderTest.getPlainText() == "", `Expected empty string, received: ${controlUnderTest.getPlainText()}`) - } - } } diff --git a/storybook/qmlTests/main.cpp b/storybook/qmlTests/main.cpp index 2aca712478..cb833891d9 100644 --- a/storybook/qmlTests/main.cpp +++ b/storybook/qmlTests/main.cpp @@ -1,5 +1,6 @@ #include #include +#include "src/TextUtils.h" class Setup : public QObject { @@ -18,6 +19,8 @@ public slots: for (const auto& path : additionalImportPaths) engine->addImportPath(path); + + qmlRegisterSingletonType("TextUtils", 1, 0, "TextUtils", &TextUtils::qmlInstance); } }; diff --git a/storybook/qmlTests/src/TextUtils.cpp b/storybook/qmlTests/src/TextUtils.cpp new file mode 100644 index 0000000000..f318c8f784 --- /dev/null +++ b/storybook/qmlTests/src/TextUtils.cpp @@ -0,0 +1,25 @@ +#include "TextUtils.h" + +#include +#include + +TextUtils::TextUtils(QObject *parent) : + QObject(parent) +{ + +} + +QObject *TextUtils::qmlInstance(QQmlEngine *engine, QJSEngine *scriptEngine) +{ + Q_UNUSED(engine) + Q_UNUSED(scriptEngine) + + static TextUtils instance; + QQmlEngine::setObjectOwnership(&instance, QQmlEngine::CppOwnership); + + return &instance; +} + +QString TextUtils::htmlToPlainText(const QString &html) { + return QTextDocumentFragment::fromHtml( html ).toPlainText(); +} diff --git a/storybook/qmlTests/src/TextUtils.h b/storybook/qmlTests/src/TextUtils.h new file mode 100644 index 0000000000..db82915ac8 --- /dev/null +++ b/storybook/qmlTests/src/TextUtils.h @@ -0,0 +1,18 @@ +#pragma once +#include + +class QQmlEngine; +class QJSEngine; + +class TextUtils : public QObject +{ + Q_OBJECT + Q_DISABLE_COPY_MOVE(TextUtils) +public: + static QObject *qmlInstance(QQmlEngine *engine, QJSEngine *scriptEngine); + + Q_INVOKABLE QString htmlToPlainText(const QString& html); + +private: + TextUtils(QObject *parent = nullptr); +}; diff --git a/storybook/stubs/shared/stores/RootStore.qml b/storybook/stubs/shared/stores/RootStore.qml index 8e42dcf54c..6a958fae3f 100644 --- a/storybook/stubs/shared/stores/RootStore.qml +++ b/storybook/stubs/shared/stores/RootStore.qml @@ -4,9 +4,9 @@ import QtQuick 2.14 QtObject { property var userProfileInst - property bool isWalletEnabled property bool isTenorWarningAccepted + property bool isGifWidgetEnabled + property bool isWalletEnabled property var getSelectedTextWithFormationChars - property var isGifWidgetEnabled property var gifColumnA }