fix(StatusSeedPhraseInput): accept a common prefix suggestion

- fix for a corner case prob when there was a valid seedphrase word
(e.g. cat) that is at the same type a prefix for other valid suggestions
(e.g. category, catalog, ...); it was imposible to select it using Enter
key or mouse click from the suggestions popup at the last field
- add a QML regression test for this issue
- use standard subcomponents (StatusDropdown, StatusListView) reducing
code duplication and unifying UI/UX

Fixes #16291
This commit is contained in:
Lukáš Tinkl 2024-09-09 23:13:57 +02:00 committed by Lukáš Tinkl
parent ce525890eb
commit 0a1ebb69bd
4 changed files with 61 additions and 46 deletions

View File

@ -49,6 +49,52 @@ Item {
waitForRendering(itemUnderTest)
}
// regression test for https://github.com/status-im/status-desktop/issues/16291
function test_threeLetterPrefixSuggestionInput() {
const commonPrefixToTest = "cat"
//generate a seed phrase
const expectedSeedPhrase = ["abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract", "absurd", "abuse", "access", commonPrefixToTest]
const baseDictionary = [...expectedSeedPhrase, "cow", "catalog", "catch", "category", "cattle"]
let isSeedPhraseValidCalled = false
itemUnderTest.isSeedPhraseValid = (seedPhrase) => {
verify(seedPhrase === expectedSeedPhrase.join(" "), "Seed phrase is not valid")
isSeedPhraseValidCalled = true
return true
}
itemUnderTest.dictionary.append(baseDictionary.map((word) => ({seedWord: word})))
//Type the seed phrase except the last word
const str = expectedSeedPhrase.join(" ")
for (let i = 0; i < str.length - commonPrefixToTest.length; i++) {
keyPress(str.charAt(i))
}
const lastInputField = findChild(itemUnderTest, "enterSeedPhraseInputField12")
verify(!!lastInputField)
mouseClick(lastInputField)
tryCompare(lastInputField, "activeFocus", true)
// type the common prefix -> "cat..."
keyClick(Qt.Key_C)
keyClick(Qt.Key_A)
keyClick(Qt.Key_T)
tryCompare(lastInputField, "text", "cat")
// hit Enter to accept "cat"
keyClick(Qt.Key_Enter)
verify(isSeedPhraseValidCalled, "isSeedPhraseValid was not called")
// hit Enter to submit the form
keyClick(Qt.Key_Enter)
verify(itemUnderTest.submitSpy.count === 1, "submitSeedPhrase signal was not emitted")
// This signal is emitted multiple times due to the way the seed phrase is updated and validated
// The minimum is the length if the seed phrase
verify(itemUnderTest.seedPhraseUpdatedSpy.count >= expectedSeedPhrase.length, "seedPhraseUpdate signal was not emitted")
}
function test_componentCreation() {
verify(itemUnderTest !== null, "Component creation failed")
}
@ -310,7 +356,6 @@ Item {
}
keyClick(Qt.Key_Enter)
print (itemUnderTest.seedPhraseUpdatedSpy.count)
verify(itemUnderTest.submitSpy.count === 0, "submitSeedPhrase signal was emitted")
verify(itemUnderTest.seedPhraseUpdatedSpy.count >= expectedSeedPhrase.length, "seedPhraseUpdate signal was not emitted")
}
@ -354,10 +399,8 @@ Item {
keyClick(Qt.Key_Enter)
verify(isSeedPhraseValidCalled, "isSeedPhraseValid was not called")
print (lastVerifiedSeedPhrase)
verify(lastVerifiedSeedPhrase === expectedSeedPhrase.join(" ").slice(0, -1) + "a", "Seed phrase is not updated")
verify(itemUnderTest.seedPhraseUpdatedSpy.count >= expectedSeedPhrase.length, "seedPhraseUpdate signal was not emitted")
}
// Test suggestions are active after the seed phrase word is fixed

View File

@ -5,7 +5,7 @@ ListModel {
Component.onCompleted: {
var englishWords = [
"apple", "banana", "cat", "dog", "elephant", "fish", "grape", "horse", "ice cream", "jellyfish",
"apple", "banana", "cat", "cow", "catalog", "catch", "category", "cattle", "dog", "elephant", "fish", "grape", "horse", "ice cream", "jellyfish",
"kiwi", "lemon", "mango", "nut", "orange", "pear", "quail", "rabbit", "strawberry", "turtle",
"umbrella", "violet", "watermelon", "xylophone", "yogurt", "zebra"
// Add more English words here...

View File

@ -66,16 +66,13 @@ Item {
property string leftComponentText: ""
/*!
\qmlproperty ListModel StatusSeedPhraseInput::inputList
This property holds the filtered words list based on the user's
input text.
This property holds the seed words dictionary
*/
property ListModel inputList: ListModel { }
/*!
\qmlproperty ListModel StatusSeedPhraseInput::filteredList
This signal is emitted when the user selects a word from
the suggestions list, either by clicking on it or by completing
typing 4 charactersnd passes as a parameter the selected word.
The corresponding handler is \c onDoneInsertingWord
This property holds the filtered words list based on the user's
input text.
*/
property ListModel filteredList: ListModel { }
/*!
@ -107,6 +104,7 @@ Item {
let seedWordTrimmed = seedWord.trim()
seedWordInput.input.edit.text = seedWordTrimmed
seedWordInput.input.edit.cursorPosition = seedWordInput.text.length
filteredList.clear()
root.doneInsertingWord(seedWordTrimmed)
}
@ -116,20 +114,14 @@ Item {
}
}
QtObject {
id: d
property bool isInputValidWord: false
}
Component {
id: seedInputLeftComponent
StatusBaseText {
leftPadding: 4
rightPadding: 6
text: root.leftComponentText
color: seedWordInput.input.edit.activeFocus ?
Theme.palette.primaryColor1 : Theme.palette.baseColor1
font.pixelSize: 15
}
}
@ -140,7 +132,6 @@ Item {
input.leftComponent: seedInputLeftComponent
input.acceptReturn: true
onTextChanged: {
d.isInputValidWord = false
filteredList.clear();
let textToCheck = text.trim().toLowerCase()
@ -149,10 +140,9 @@ Item {
}
for (var i = 0; i < inputList.count; i++) {
if (inputList.get(i).seedWord.startsWith(textToCheck)) {
filteredList.insert(filteredList.count, {"seedWord": inputList.get(i).seedWord});
if(inputList.get(i).seedWord === textToCheck)
d.isInputValidWord = true
const word = inputList.get(i).seedWord
if (word.startsWith(textToCheck)) {
filteredList.insert(filteredList.count, {"seedWord": word})
}
}
@ -203,7 +193,7 @@ Item {
}
}
Popup {
StatusDropdown {
id: suggListContainer
contentWidth: seedSuggestionsList.width
contentHeight: ((seedSuggestionsList.count <= 5) ? seedSuggestionsList.count : 5) *34
@ -215,26 +205,11 @@ Item {
rightPadding: 0
visible: ((filteredList.count > 0) && seedWordInput.input.edit.activeFocus)
background: Rectangle {
id: statusMenuBackgroundContent
color: Theme.palette.statusMenu.backgroundColor
radius: 8
layer.enabled: true
layer.effect: DropShadow {
anchors.fill: parent
source: statusMenuBackgroundContent
horizontalOffset: 0
verticalOffset: 4
radius: 12
samples: 25
spread: 0.2
color: Theme.palette.dropShadow
}
}
ListView {
StatusListView {
id: seedSuggestionsList
width: ((seedSuggestionsList.contentItem.childrenRect.width + 24) > root.width) ? root.width
: (seedSuggestionsList.contentItem.childrenRect.width + 24)
width: (((seedSuggestionsList.contentItem.childrenRect.width + 24) > root.width) ? root.width
: (seedSuggestionsList.contentItem.childrenRect.width + 24)) + 8
anchors.top: parent.top
anchors.bottom: parent.bottom
@ -242,8 +217,6 @@ Item {
seedSuggestionsList.currentIndex = 0
}
clip: true
ScrollBar.vertical: ScrollBar { }
model: root.filteredList
delegate: Item {
@ -259,7 +232,7 @@ Item {
StatusBaseText {
id: suggWord
anchors.left: parent.left
anchors.leftMargin: 14
anchors.leftMargin: 8
anchors.verticalCenter: parent.verticalCenter
text: seedWord
color: mouseArea.containsMouse || index === seedSuggestionsList.currentIndex ? Theme.palette.indirectColor1 : Theme.palette.directColor1

View File

@ -55,7 +55,6 @@ ColumnLayout {
let mnemonicString = ""
if (d.allEntriesValid) {
mnemonicString = buildMnemonicString()
if (!Utils.isMnemonic(mnemonicString) || !root.isSeedPhraseValid(mnemonicString)) {
root.setWrongSeedPhraseMessage(qsTr("Invalid seed phrase"))