From 4b983251cb464d104aafd95ea56e19b5c404247a Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Sun, 25 Mar 2018 16:24:30 -0400 Subject: [PATCH] Add advanced search term parser * Support quoted strings & per-field searching * Support regex and exact matching * Simplify search sequence * Make search widget larger * Add regex converter to Tools namespace --- src/browser/BrowserService.cpp | 2 +- src/core/EntrySearcher.cpp | 144 ++++++++++++++++++++++++++++----- src/core/EntrySearcher.h | 39 ++++++++- src/core/Tools.cpp | 29 +++++++ src/core/Tools.h | 3 + src/gui/DatabaseWidget.cpp | 11 ++- src/gui/DatabaseWidget.h | 3 +- src/gui/SearchWidget.cpp | 2 +- src/gui/SearchWidget.ui | 15 ++++ src/gui/entry/EntryView.cpp | 4 +- src/gui/entry/EntryView.h | 6 +- tests/TestEntrySearcher.cpp | 107 +++++++++++++++++------- tests/TestEntrySearcher.h | 3 +- tests/gui/TestGui.cpp | 3 +- 14 files changed, 303 insertions(+), 68 deletions(-) diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp index a69508026..a1315ad49 100644 --- a/src/browser/BrowserService.cpp +++ b/src/browser/BrowserService.cpp @@ -390,7 +390,7 @@ QList BrowserService::searchEntries(Database* db, const QString& hostnam return entries; } - for (Entry* entry : EntrySearcher().search(baseDomain(hostname), rootGroup, Qt::CaseInsensitive)) { + for (Entry* entry : EntrySearcher().search(baseDomain(hostname), rootGroup)) { QString entryUrl = entry->url(); QUrl entryQUrl(entryUrl); QString entryScheme = entryQUrl.scheme(); diff --git a/src/core/EntrySearcher.cpp b/src/core/EntrySearcher.cpp index b181ad389..6614ab463 100644 --- a/src/core/EntrySearcher.cpp +++ b/src/core/EntrySearcher.cpp @@ -19,42 +19,91 @@ #include "EntrySearcher.h" #include "core/Group.h" +#include "core/Tools.h" -QList EntrySearcher::search(const QString& searchTerm, const Group* group, Qt::CaseSensitivity caseSensitivity) +EntrySearcher::EntrySearcher(bool caseSensitive) + : m_caseSensitive(caseSensitive) + , m_termParser(R"re(([-*+]+)?(?:(\w*):)?(?:(?=")"((?:[^"\\]|\\.)*)"|([^ ]*))( |$))re") + // Group 1 = modifiers, Group 2 = field, Group 3 = quoted string, Group 4 = unquoted string { +} + +QList EntrySearcher::search(const QString& searchString, const Group* baseGroup, bool forceSearch) +{ + Q_ASSERT(baseGroup); + QList results; - - if (group->resolveSearchingEnabled()) { - results.append(searchEntries(searchTerm, group->entries(), caseSensitivity)); - } - - for (Group* childGroup : group->children()) { - if (childGroup->resolveSearchingEnabled()) { - results.append(searchEntries(searchTerm, childGroup->entries(), caseSensitivity)); + for (const auto group : baseGroup->groupsRecursive(true)) { + if (forceSearch || group->resolveSearchingEnabled()) { + results.append(searchEntries(searchString, group->entries())); } } return results; } -QList EntrySearcher::searchEntries(const QString& searchTerm, const QList& entries, - Qt::CaseSensitivity caseSensitivity) +QList EntrySearcher::searchEntries(const QString& searchString, const QList& entries) { QList results; for (Entry* entry : entries) { - if (matchEntry(searchTerm, entry, caseSensitivity)) { + if (searchEntryImpl(searchString, entry)) { results.append(entry); } } return results; } -bool EntrySearcher::matchEntry(const QString& searchTerm, Entry* entry, - Qt::CaseSensitivity caseSensitivity) +void EntrySearcher::setCaseSensitive(bool state) { - const QStringList wordList = searchTerm.split(QRegExp("\\s"), QString::SkipEmptyParts); - for (const QString& word : wordList) { - if (!wordMatch(word, entry, caseSensitivity)) { + m_caseSensitive = state; +} + +bool EntrySearcher::isCaseSensitive() +{ + return m_caseSensitive; +} + +bool EntrySearcher::searchEntryImpl(const QString& searchString, Entry* entry) +{ + // Pre-load in case they are needed + auto attributes = QStringList(entry->attributes()->keys()); + auto attachments = QStringList(entry->attachments()->keys()); + + bool found; + auto searchTerms = parseSearchTerms(searchString); + + for (const auto& term : searchTerms) { + switch (term->field) { + case Field::Title: + found = term->regex.match(entry->resolvePlaceholder(entry->title())).hasMatch(); + break; + case Field::Username: + found = term->regex.match(entry->resolvePlaceholder(entry->username())).hasMatch(); + break; + case Field::Password: + found = term->regex.match(entry->resolvePlaceholder(entry->password())).hasMatch(); + break; + case Field::Url: + found = term->regex.match(entry->resolvePlaceholder(entry->url())).hasMatch(); + break; + case Field::Notes: + found = term->regex.match(entry->notes()).hasMatch(); + break; + case Field::Attribute: + found = !attributes.filter(term->regex).empty(); + break; + case Field::Attachment: + found = !attachments.filter(term->regex).empty(); + break; + default: + found = term->regex.match(entry->resolvePlaceholder(entry->title())).hasMatch() || + term->regex.match(entry->resolvePlaceholder(entry->username())).hasMatch() || + term->regex.match(entry->resolvePlaceholder(entry->url())).hasMatch() || + term->regex.match(entry->notes()).hasMatch(); + } + + // Short circuit if we failed to match or we matched and are excluding this term + if (!found || term->exclude) { return false; } } @@ -62,10 +111,61 @@ bool EntrySearcher::matchEntry(const QString& searchTerm, Entry* entry, return true; } -bool EntrySearcher::wordMatch(const QString& word, Entry* entry, Qt::CaseSensitivity caseSensitivity) +QList > EntrySearcher::parseSearchTerms(const QString& searchString) { - return entry->resolvePlaceholder(entry->title()).contains(word, caseSensitivity) - || entry->resolvePlaceholder(entry->username()).contains(word, caseSensitivity) - || entry->resolvePlaceholder(entry->url()).contains(word, caseSensitivity) - || entry->resolvePlaceholder(entry->notes()).contains(word, caseSensitivity); + auto terms = QList >(); + + auto results = m_termParser.globalMatch(searchString); + while (results.hasNext()) { + auto result = results.next(); + auto term = QSharedPointer::create(); + + // Quoted string group + term->word = result.captured(3); + + // If empty, use the unquoted string group + if (term->word.isEmpty()) { + term->word = result.captured(4); + } + + // If still empty, ignore this match + if (term->word.isEmpty()) { + continue; + } + + auto mods = result.captured(1); + + // Convert term to regex + term->regex = Tools::convertToRegex(term->word, !mods.contains("*"), mods.contains("+"), m_caseSensitive); + + // Exclude modifier + term->exclude = mods.contains("-"); + + // Determine the field to search + QString field = result.captured(2); + if (!field.isEmpty()) { + auto cs = Qt::CaseInsensitive; + if (field.compare("title", cs) == 0) { + term->field = Field::Title; + } else if (field.startsWith("user", cs)) { + term->field = Field::Username; + } else if (field.startsWith("pass", cs)) { + term->field = Field::Password; + } else if (field.compare("url", cs) == 0) { + term->field = Field::Url; + } else if (field.compare("notes", cs) == 0) { + term->field = Field::Notes; + } else if (field.startsWith("attr", cs)) { + term->field = Field::Attribute; + } else if (field.startsWith("attach", cs)) { + term->field = Field::Attachment; + } else { + term->field = Field::Undefined; + } + } + + terms.append(term); + } + + return terms; } diff --git a/src/core/EntrySearcher.h b/src/core/EntrySearcher.h index def5eb8f6..ec71a7ce1 100644 --- a/src/core/EntrySearcher.h +++ b/src/core/EntrySearcher.h @@ -20,6 +20,7 @@ #define KEEPASSX_ENTRYSEARCHER_H #include +#include class Group; class Entry; @@ -27,12 +28,42 @@ class Entry; class EntrySearcher { public: - QList search(const QString& searchTerm, const Group* group, Qt::CaseSensitivity caseSensitivity); + explicit EntrySearcher(bool caseSensitive = false); + + QList search(const QString& searchString, const Group* baseGroup, bool forceSearch = false); + QList searchEntries(const QString& searchString, const QList& entries); + + void setCaseSensitive(bool state); + bool isCaseSensitive(); private: - QList searchEntries(const QString& searchTerm, const QList& entries, Qt::CaseSensitivity caseSensitivity); - bool matchEntry(const QString& searchTerm, Entry* entry, Qt::CaseSensitivity caseSensitivity); - bool wordMatch(const QString& word, Entry* entry, Qt::CaseSensitivity caseSensitivity); + bool searchEntryImpl(const QString& searchString, Entry* entry); + + enum class Field { + Undefined, + Title, + Username, + Password, + Url, + Notes, + Attribute, + Attachment + }; + + struct SearchTerm + { + Field field; + QString word; + QRegularExpression regex; + bool exclude; + }; + + QList > parseSearchTerms(const QString& searchString); + + bool m_caseSensitive; + QRegularExpression m_termParser; + + friend class TestEntrySearcher; }; #endif // KEEPASSX_ENTRYSEARCHER_H diff --git a/src/core/Tools.cpp b/src/core/Tools.cpp index ded3a1651..362cfa937 100644 --- a/src/core/Tools.cpp +++ b/src/core/Tools.cpp @@ -26,6 +26,8 @@ #include #include #include +#include + #include #include @@ -199,4 +201,31 @@ void wait(int ms) } } +// Escape common regex symbols except for *, ?, and | +auto regexEscape = QRegularExpression(R"re(([-[\]{}()+.,\\\/^$#]))re"); + +QRegularExpression convertToRegex(const QString& string, bool useWildcards, bool exactMatch, bool caseSensitive) +{ + QString pattern = string; + + // Wildcard support (*, ?, |) + if (useWildcards) { + pattern.replace(regexEscape, "\\\\1"); + pattern.replace("*", ".*"); + pattern.replace("?", "."); + } + + // Exact modifier + if (exactMatch) { + pattern = "^" + pattern + "$"; + } + + auto regex = QRegularExpression(pattern); + if (!caseSensitive) { + regex.setPatternOptions(QRegularExpression::CaseInsensitiveOption); + } + + return regex; +} + } // namespace Tools diff --git a/src/core/Tools.h b/src/core/Tools.h index 13d9869f7..37214f069 100644 --- a/src/core/Tools.h +++ b/src/core/Tools.h @@ -27,6 +27,7 @@ #include class QIODevice; +class QRegularExpression; namespace Tools { @@ -38,6 +39,8 @@ bool isHex(const QByteArray& ba); bool isBase64(const QByteArray& ba); void sleep(int ms); void wait(int ms); +QRegularExpression convertToRegex(const QString& string, bool useWildcards = false, bool exactMatch = false, + bool caseSensitive = false); template RandomAccessIterator binaryFind(RandomAccessIterator begin, RandomAccessIterator end, const T& value) diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index ebec29b46..701ba588c 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -209,7 +209,7 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent) m_fileWatchUnblockTimer.setSingleShot(true); m_ignoreAutoReload = false; - m_searchCaseSensitive = false; + m_EntrySearcher = new EntrySearcher(false); m_searchLimitGroup = config()->get("SearchLimitGroup", false).toBool(); #ifdef WITH_XC_SSHAGENT @@ -227,6 +227,7 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent) DatabaseWidget::~DatabaseWidget() { + delete m_EntrySearcher; } DatabaseWidget::Mode DatabaseWidget::currentMode() const @@ -1012,17 +1013,15 @@ void DatabaseWidget::search(const QString& searchtext) emit searchModeAboutToActivate(); - Qt::CaseSensitivity caseSensitive = m_searchCaseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive; - Group* searchGroup = m_searchLimitGroup ? currentGroup() : m_db->rootGroup(); - QList searchResult = EntrySearcher().search(searchtext, searchGroup, caseSensitive); + QList searchResult = m_EntrySearcher->search(searchtext, searchGroup); m_entryView->displaySearch(searchResult); m_lastSearchText = searchtext; // Display a label detailing our search results - if (searchResult.size() > 0) { + if (!searchResult.isEmpty()) { m_searchingLabel->setText(tr("Search Results (%1)").arg(searchResult.size())); } else { m_searchingLabel->setText(tr("No Results")); @@ -1035,7 +1034,7 @@ void DatabaseWidget::search(const QString& searchtext) void DatabaseWidget::setSearchCaseSensitive(bool state) { - m_searchCaseSensitive = state; + m_EntrySearcher->setCaseSensitive(state); refreshSearch(); } diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h index a5d881ff9..d0c4e2042 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -37,6 +37,7 @@ class EditEntryWidget; class EditGroupWidget; class Entry; class EntryView; +class EntrySearcher; class Group; class GroupView; class KeePass1OpenWidget; @@ -246,8 +247,8 @@ private: QString m_databaseFileName; // Search state + EntrySearcher* m_EntrySearcher; QString m_lastSearchText; - bool m_searchCaseSensitive; bool m_searchLimitGroup; // CSV import state diff --git a/src/gui/SearchWidget.cpp b/src/gui/SearchWidget.cpp index 96bd05a5b..ba8b616d8 100644 --- a/src/gui/SearchWidget.cpp +++ b/src/gui/SearchWidget.cpp @@ -137,7 +137,7 @@ void SearchWidget::startSearchTimer() if (!m_searchTimer->isActive()) { m_searchTimer->stop(); } - m_searchTimer->start(100); + m_searchTimer->start(300); } void SearchWidget::startSearch() diff --git a/src/gui/SearchWidget.ui b/src/gui/SearchWidget.ui index 438a242c7..19b274a88 100644 --- a/src/gui/SearchWidget.ui +++ b/src/gui/SearchWidget.ui @@ -34,6 +34,9 @@ Qt::Horizontal + + QSizePolicy::Minimum + 30 @@ -44,6 +47,18 @@ + + + 0 + 0 + + + + + 0 + 0 + + padding:3px diff --git a/src/gui/entry/EntryView.cpp b/src/gui/entry/EntryView.cpp index 28d5ec2bb..64eca5ee3 100644 --- a/src/gui/entry/EntryView.cpp +++ b/src/gui/entry/EntryView.cpp @@ -58,9 +58,9 @@ EntryView::EntryView(QWidget* parent) m_headerMenu->setTitle(tr("Customize View")); m_headerMenu->addSection(tr("Customize View")); - m_hideUsernamesAction = m_headerMenu->addAction(tr("Hide Usernames"), m_model, SLOT(setUsernamesHidden(bool))); + m_hideUsernamesAction = m_headerMenu->addAction(tr("Hide Usernames"), this, SLOT(setUsernamesHidden(bool))); m_hideUsernamesAction->setCheckable(true); - m_hidePasswordsAction = m_headerMenu->addAction(tr("Hide Passwords"), m_model, SLOT(setPasswordsHidden(bool))); + m_hidePasswordsAction = m_headerMenu->addAction(tr("Hide Passwords"), this, SLOT(setPasswordsHidden(bool))); m_hidePasswordsAction->setCheckable(true); m_headerMenu->addSeparator(); diff --git a/src/gui/entry/EntryView.h b/src/gui/entry/EntryView.h index fa002c717..766699599 100644 --- a/src/gui/entry/EntryView.h +++ b/src/gui/entry/EntryView.h @@ -43,9 +43,7 @@ public: int numberOfSelectedEntries(); void setFirstEntryActive(); bool isUsernamesHidden() const; - void setUsernamesHidden(bool hide); bool isPasswordsHidden() const; - void setPasswordsHidden(bool hide); QByteArray viewState() const; bool setViewState(const QByteArray& state); @@ -57,6 +55,10 @@ signals: void entrySelectionChanged(); void viewStateChanged(); +public slots: + void setUsernamesHidden(bool hide); + void setPasswordsHidden(bool hide); + protected: void keyPressEvent(QKeyEvent* event) override; void focusInEvent(QFocusEvent* event) override; diff --git a/tests/TestEntrySearcher.cpp b/tests/TestEntrySearcher.cpp index 0c0a2c3e4..51be468b5 100644 --- a/tests/TestEntrySearcher.cpp +++ b/tests/TestEntrySearcher.cpp @@ -22,23 +22,32 @@ QTEST_GUILESS_MAIN(TestEntrySearcher) void TestEntrySearcher::initTestCase() { - m_groupRoot = new Group(); + m_rootGroup = new Group(); } void TestEntrySearcher::cleanupTestCase() { - delete m_groupRoot; + delete m_rootGroup; } void TestEntrySearcher::testSearch() { + /** + * Root + * - group1 (search disabled) + * - group11 + * - group2 + * - group21 + * - group211 + * - group2111 + */ Group* group1 = new Group(); Group* group2 = new Group(); Group* group3 = new Group(); - group1->setParent(m_groupRoot); - group2->setParent(m_groupRoot); - group3->setParent(m_groupRoot); + group1->setParent(m_rootGroup); + group2->setParent(m_rootGroup); + group3->setParent(m_rootGroup); Group* group11 = new Group(); @@ -55,15 +64,15 @@ void TestEntrySearcher::testSearch() group1->setSearchingEnabled(Group::Disable); Entry* eRoot = new Entry(); - eRoot->setNotes("test search term test"); - eRoot->setGroup(m_groupRoot); + eRoot->setTitle("test search term test"); + eRoot->setGroup(m_rootGroup); Entry* eRoot2 = new Entry(); eRoot2->setNotes("test term test"); - eRoot2->setGroup(m_groupRoot); + eRoot2->setGroup(m_rootGroup); Entry* e1 = new Entry(); - e1->setNotes("test search term test"); + e1->setUsername("test search term test"); e1->setGroup(group1); Entry* e11 = new Entry(); @@ -71,29 +80,37 @@ void TestEntrySearcher::testSearch() e11->setGroup(group11); Entry* e2111 = new Entry(); - e2111->setNotes("test search term test"); + e2111->setTitle("test search term test"); e2111->setGroup(group2111); Entry* e2111b = new Entry(); e2111b->setNotes("test search test"); + e2111b->setPassword("testpass"); e2111b->setGroup(group2111); Entry* e3 = new Entry(); - e3->setNotes("test search term test"); + e3->setUrl("test search term test"); e3->setGroup(group3); Entry* e3b = new Entry(); - e3b->setNotes("test search test"); + e3b->setTitle("test search test"); + e3b->setPassword("realpass"); e3b->setGroup(group3); - m_searchResult = m_entrySearcher.search("search term", m_groupRoot, Qt::CaseInsensitive); - QCOMPARE(m_searchResult.count(), 2); + m_searchResult = m_entrySearcher.search("search", m_rootGroup); + QCOMPARE(m_searchResult.count(), 5); - m_searchResult = m_entrySearcher.search("search term", group211, Qt::CaseInsensitive); + m_searchResult = m_entrySearcher.search("search term", m_rootGroup); + QCOMPARE(m_searchResult.count(), 3); + + m_searchResult = m_entrySearcher.search("search term", group211); QCOMPARE(m_searchResult.count(), 1); - // Parent group disabled search - m_searchResult = m_entrySearcher.search("search term", group11, Qt::CaseInsensitive); + m_searchResult = m_entrySearcher.search("password:testpass", m_rootGroup); + QCOMPARE(m_searchResult.count(), 1); + + // Parent group has search disabled + m_searchResult = m_entrySearcher.search("search term", group11); QCOMPARE(m_searchResult.count(), 0); } @@ -102,38 +119,74 @@ void TestEntrySearcher::testAndConcatenationInSearch() Entry* entry = new Entry(); entry->setNotes("abc def ghi"); entry->setTitle("jkl"); - entry->setGroup(m_groupRoot); + entry->setGroup(m_rootGroup); - m_searchResult = m_entrySearcher.search("", m_groupRoot, Qt::CaseInsensitive); + m_searchResult = m_entrySearcher.search("", m_rootGroup); QCOMPARE(m_searchResult.count(), 1); - m_searchResult = m_entrySearcher.search("def", m_groupRoot, Qt::CaseInsensitive); + m_searchResult = m_entrySearcher.search("def", m_rootGroup); QCOMPARE(m_searchResult.count(), 1); - m_searchResult = m_entrySearcher.search(" abc ghi ", m_groupRoot, Qt::CaseInsensitive); + m_searchResult = m_entrySearcher.search(" abc ghi ", m_rootGroup); QCOMPARE(m_searchResult.count(), 1); - m_searchResult = m_entrySearcher.search("ghi ef", m_groupRoot, Qt::CaseInsensitive); + m_searchResult = m_entrySearcher.search("ghi ef", m_rootGroup); QCOMPARE(m_searchResult.count(), 1); - m_searchResult = m_entrySearcher.search("abc ef xyz", m_groupRoot, Qt::CaseInsensitive); + m_searchResult = m_entrySearcher.search("abc ef xyz", m_rootGroup); QCOMPARE(m_searchResult.count(), 0); - m_searchResult = m_entrySearcher.search("abc kl", m_groupRoot, Qt::CaseInsensitive); + m_searchResult = m_entrySearcher.search("abc kl", m_rootGroup); QCOMPARE(m_searchResult.count(), 1); } void TestEntrySearcher::testAllAttributesAreSearched() { Entry* entry = new Entry(); - entry->setGroup(m_groupRoot); + entry->setGroup(m_rootGroup); entry->setTitle("testTitle"); entry->setUsername("testUsername"); entry->setUrl("testUrl"); entry->setNotes("testNote"); - m_searchResult = - m_entrySearcher.search("testTitle testUsername testUrl testNote", m_groupRoot, Qt::CaseInsensitive); + // Default is to AND all terms together + m_searchResult = m_entrySearcher.search("testTitle testUsername testUrl testNote", m_rootGroup); QCOMPARE(m_searchResult.count(), 1); } + +void TestEntrySearcher::testSearchTermParser() +{ + // Test standard search terms + auto terms = m_entrySearcher.parseSearchTerms("-test \"quoted \\\"string\\\"\" user:user pass:\"test me\" noquote "); + + QCOMPARE(terms.length(), 5); + + QCOMPARE(terms[0]->field, EntrySearcher::Field::Undefined); + QCOMPARE(terms[0]->word, QString("test")); + QCOMPARE(terms[0]->exclude, true); + + QCOMPARE(terms[1]->field, EntrySearcher::Field::Undefined); + QCOMPARE(terms[1]->word, QString("quoted \\\"string\\\"")); + QCOMPARE(terms[1]->exclude, false); + + QCOMPARE(terms[2]->field, EntrySearcher::Field::Username); + QCOMPARE(terms[2]->word, QString("user")); + + QCOMPARE(terms[3]->field, EntrySearcher::Field::Password); + QCOMPARE(terms[3]->word, QString("test me")); + + QCOMPARE(terms[4]->field, EntrySearcher::Field::Undefined); + QCOMPARE(terms[4]->word, QString("noquote")); + + // Test wildcard and regex search terms + terms = m_entrySearcher.parseSearchTerms("+url:*.google.com *user:\\d+\\w{2}"); + + QCOMPARE(terms.length(), 2); + + QCOMPARE(terms[0]->field, EntrySearcher::Field::Url); + QCOMPARE(terms[0]->regex.pattern(), QString("^.*\\.google\\.com$")); + + QCOMPARE(terms[1]->field, EntrySearcher::Field::Username); + QCOMPARE(terms[1]->regex.pattern(), QString("\\d+\\w{2}")); +} diff --git a/tests/TestEntrySearcher.h b/tests/TestEntrySearcher.h index 17d486c22..f385d618e 100644 --- a/tests/TestEntrySearcher.h +++ b/tests/TestEntrySearcher.h @@ -34,9 +34,10 @@ private slots: void testAndConcatenationInSearch(); void testSearch(); void testAllAttributesAreSearched(); + void testSearchTermParser(); private: - Group* m_groupRoot; + Group* m_rootGroup; EntrySearcher m_entrySearcher; QList m_searchResult; }; diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index 9b04dd18e..f48d0777c 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -841,9 +841,10 @@ void TestGui::testSearch() // Ensure Down focuses on entry view when search text is selected QTest::keyClick(searchTextEdit, Qt::Key_Down); QTRY_VERIFY(entryView->hasFocus()); + QCOMPARE(entryView->selectionModel()->currentIndex().row(), 0); // Test that password copies (entry has focus) QClipboard* clipboard = QApplication::clipboard(); - QTest::keyClick(searchTextEdit, Qt::Key_C, Qt::ControlModifier); + QTest::keyClick(entryView, Qt::Key_C, Qt::ControlModifier); QModelIndex searchedItem = entryView->model()->index(0, 1); Entry* searchedEntry = entryView->entryFromIndex(searchedItem); QTRY_COMPARE(searchedEntry->password(), clipboard->text());