diff --git a/src/Makefile.qttest.include b/src/Makefile.qttest.include index 62071d40e5a5..30cd380e6411 100644 --- a/src/Makefile.qttest.include +++ b/src/Makefile.qttest.include @@ -16,6 +16,7 @@ TEST_QT_MOC_CPP = \ if ENABLE_WALLET TEST_QT_MOC_CPP += \ qt/test/moc_addressbooktests.cpp \ + qt/test/moc_transactionviewtests.cpp \ qt/test/moc_wallettests.cpp endif # ENABLE_WALLET @@ -24,6 +25,7 @@ TEST_QT_H = \ qt/test/apptests.h \ qt/test/optiontests.h \ qt/test/rpcnestedtests.h \ + qt/test/transactionviewtests.h \ qt/test/uritests.h \ qt/test/util.h \ qt/test/trafficgraphdatatests.h \ @@ -45,6 +47,7 @@ qt_test_test_dash_qt_SOURCES = \ if ENABLE_WALLET qt_test_test_dash_qt_SOURCES += \ qt/test/addressbooktests.cpp \ + qt/test/transactionviewtests.cpp \ qt/test/wallettests.cpp \ wallet/test/wallet_test_fixture.cpp endif # ENABLE_WALLET diff --git a/src/qt/test/test_main.cpp b/src/qt/test/test_main.cpp index 466c347c19fb..8f313380ab6a 100644 --- a/src/qt/test/test_main.cpp +++ b/src/qt/test/test_main.cpp @@ -19,6 +19,7 @@ #ifdef ENABLE_WALLET #include +#include #include #endif // ENABLE_WALLET @@ -105,6 +106,9 @@ int main(int argc, char* argv[]) AddressBookTests test6(app.node()); num_test_failures += QTest::qExec(&test6); + + TransactionViewTests transaction_view_tests(app.node()); + num_test_failures += QTest::qExec(&transaction_view_tests); #endif TrafficGraphDataTests test7; num_test_failures += QTest::qExec(&test7); diff --git a/src/qt/test/transactionviewtests.cpp b/src/qt/test/transactionviewtests.cpp new file mode 100644 index 000000000000..efe23cad2c73 --- /dev/null +++ b/src/qt/test/transactionviewtests.cpp @@ -0,0 +1,187 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +using wallet::AddWallet; +using wallet::CreateMockWalletDatabase; +using wallet::CWallet; +using wallet::RemoveWallet; +using wallet::WALLET_FLAG_DESCRIPTORS; +using wallet::WalletContext; + +namespace { + +QComboBox* FindTransactionTypeWidget(TransactionView& view) +{ + for (QComboBox* combo : view.findChildren()) { + if (combo->findText("All") >= 0 && combo->findText("Data Transaction") >= 0) return combo; + } + return nullptr; +} + +class WalletCleanup +{ +public: + WalletCleanup(WalletContext& context, std::shared_ptr wallet) : + m_context(context), + m_wallet(std::move(wallet)) + { + } + ~WalletCleanup() { RemoveWallet(m_context, m_wallet, /*load_on_start=*/std::nullopt); } + +private: + WalletContext& m_context; + std::shared_ptr m_wallet; +}; + +class CoinJoinOptionsRestorer +{ +public: + explicit CoinJoinOptionsRestorer(interfaces::CoinJoin::Options& options) : + m_options(options), + m_enabled(options.isEnabled()) + { + } + ~CoinJoinOptionsRestorer() { m_options.setEnabled(m_enabled); } + +private: + interfaces::CoinJoin::Options& m_options; + const bool m_enabled; +}; + +class TransactionTypeSettingRestorer +{ +public: + TransactionTypeSettingRestorer() : + m_had_value(m_settings.contains("transactionType")), + m_value(m_settings.value("transactionType")) + { + } + ~TransactionTypeSettingRestorer() + { + if (m_had_value) { + m_settings.setValue("transactionType", m_value); + } else { + m_settings.remove("transactionType"); + } + } + +private: + QSettings m_settings; + const bool m_had_value; + const QVariant m_value; +}; + +} // namespace + +void TransactionViewTests::transactionTypeSettingPersistence() +{ +#if defined(Q_OS_MACOS) + if (QApplication::platformName() == "minimal") { + QSKIP("Skipping TransactionView checks on macOS with the minimal platform due to QTBUG-49686"); + } +#endif + + TestChain100Setup test; + m_node.setContext(&test.m_node); + + WalletContext& context{*m_node.walletLoader().context()}; + std::shared_ptr wallet{std::make_shared(test.m_node.chain.get(), test.m_node.coinjoin_loader.get(), + "", test.m_args, CreateMockWalletDatabase())}; + wallet->LoadWallet(); + wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS); + wallet->SetupDescriptorScriptPubKeyMans("", ""); + AddWallet(context, wallet); + WalletCleanup wallet_cleanup{context, wallet}; + + OptionsModel options_model{m_node}; + bilingual_str error; + QVERIFY(options_model.Init(error)); + ClientModel client_model{m_node, &options_model}; + WalletModel wallet_model{interfaces::MakeWallet(context, wallet), client_model}; + + CoinJoinOptionsRestorer coinjoin_restorer{m_node.coinJoinOptions()}; + m_node.coinJoinOptions().setEnabled(false); + + TransactionView type_template; + QComboBox* const template_widget{FindTransactionTypeWidget(type_template)}; + QVERIFY(template_widget != nullptr); + const int coinjoin_row{ + template_widget->findData(TransactionFilterProxy::TYPE(TransactionRecord::CoinJoinSend))}; + QVERIFY(coinjoin_row >= 0); + + struct SettingCase { + int saved_index; + int expected_index; + QString expected_text; + quint32 expected_filter; + }; + const std::vector setting_cases{ + {12, 12, QString{"Data Transaction"}, TransactionFilterProxy::TYPE(TransactionRecord::DataTransaction)}, + {13, 13, QString{"Dust Receive"}, TransactionFilterProxy::TYPE(TransactionRecord::DustReceive)}, + {14, 14, QString{"Other"}, TransactionFilterProxy::TYPE(TransactionRecord::Other)}, + {coinjoin_row, 1, QString{"Most Common"}, TransactionFilterProxy::COMMON_TYPES}, + {template_widget->count(), 1, QString{"Most Common"}, TransactionFilterProxy::COMMON_TYPES}, + }; + + TransactionTypeSettingRestorer setting_restorer; + for (const SettingCase& setting_case : setting_cases) { + QSettings{}.setValue("transactionType", setting_case.saved_index); + TransactionView restored_view; + restored_view.setModel(&wallet_model); + QComboBox* const restored_widget{FindTransactionTypeWidget(restored_view)}; + QVERIFY(restored_widget != nullptr); + QCOMPARE(restored_widget->currentIndex(), setting_case.expected_index); + QCOMPARE(restored_widget->currentText(), setting_case.expected_text); + QCOMPARE(restored_widget->currentData().toUInt(), setting_case.expected_filter); + } + + { + QSettings settings; + settings.remove("transactionType"); + + TransactionView default_view; + default_view.setModel(&wallet_model); + QComboBox* const default_widget{FindTransactionTypeWidget(default_view)}; + QVERIFY(default_widget != nullptr); + QCOMPARE(default_widget->currentIndex(), 1); + QCOMPARE(default_widget->currentText(), QString{"Most Common"}); + QCOMPARE(default_widget->currentData().toUInt(), TransactionFilterProxy::COMMON_TYPES); + + QListView* const type_list{qobject_cast(default_widget->view())}; + QVERIFY(type_list != nullptr); + for (const quint32 coinjoin_filter : + {TransactionFilterProxy::TYPE(TransactionRecord::CoinJoinSend), + TransactionFilterProxy::TYPE(TransactionRecord::CoinJoinMakeCollaterals), + TransactionFilterProxy::TYPE(TransactionRecord::CoinJoinCreateDenominations), + TransactionFilterProxy::TYPE(TransactionRecord::CoinJoinMixing), + TransactionFilterProxy::TYPE(TransactionRecord::CoinJoinCollateralPayment)}) { + const int row{default_widget->findData(coinjoin_filter)}; + QVERIFY(row >= 0); + QVERIFY(type_list->isRowHidden(row)); + } + } +} diff --git a/src/qt/test/transactionviewtests.h b/src/qt/test/transactionviewtests.h new file mode 100644 index 000000000000..ed49145f2e54 --- /dev/null +++ b/src/qt/test/transactionviewtests.h @@ -0,0 +1,31 @@ +// Copyright (c) 2026 The Dash Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_QT_TEST_TRANSACTIONVIEWTESTS_H +#define BITCOIN_QT_TEST_TRANSACTIONVIEWTESTS_H + +#include + +namespace interfaces { +class Node; +} // namespace interfaces + +class TransactionViewTests : public QObject +{ + Q_OBJECT + +public: + explicit TransactionViewTests(interfaces::Node& node) : + m_node(node) + { + } + +private Q_SLOTS: + void transactionTypeSettingPersistence(); + +private: + interfaces::Node& m_node; +}; + +#endif // BITCOIN_QT_TEST_TRANSACTIONVIEWTESTS_H diff --git a/src/qt/transactionview.cpp b/src/qt/transactionview.cpp index 19b62418b4f9..1ecf31b2cb7e 100644 --- a/src/qt/transactionview.cpp +++ b/src/qt/transactionview.cpp @@ -99,7 +99,7 @@ TransactionView::TransactionView(QWidget* parent) : typeWidget->addItem(tr("Data Transaction"), TransactionFilterProxy::TYPE(TransactionRecord::DataTransaction)); typeWidget->addItem(tr("Dust Receive"), TransactionFilterProxy::TYPE(TransactionRecord::DustReceive)); typeWidget->addItem(tr("Other"), TransactionFilterProxy::TYPE(TransactionRecord::Other)); - typeWidget->setCurrentIndex(settings.value("transactionType").toInt()); + typeWidget->setCurrentIndex(settings.value("transactionType", -1).toInt()); hlayout->addWidget(typeWidget); @@ -259,7 +259,6 @@ void TransactionView::setModel(WalletModel *_model) connect(_model, &WalletModel::notifyWatchonlyChanged, this, &TransactionView::updateWatchOnlyColumn); // Update transaction list with persisted settings - chooseType(settings.value("transactionType").toInt()); chooseDate(settings.value("transactionDate").toInt()); updateCoinJoinVisibility(); @@ -785,15 +784,23 @@ void TransactionView::updateCoinJoinVisibility() if (model == nullptr) { return; } - bool fEnabled = model->node().coinJoinOptions().isEnabled(); - // If CoinJoin gets enabled use "All" else "Most common" - int idx = fEnabled ? 0 : 1; - chooseType(idx); - typeWidget->setCurrentIndex(idx); - // Hide all CoinJoin related filters + const bool fEnabled = model->node().coinJoinOptions().isEnabled(); + // Hide all CoinJoin related filters by value so this stays correct when entries are reordered. QListView* typeList = qobject_cast(typeWidget->view()); - std::vector vecRows{4, 5, 6, 7, 8}; - for (auto nRow : vecRows) { - typeList->setRowHidden(nRow, !fEnabled); + for (const quint32 type_filter : {TransactionFilterProxy::TYPE(TransactionRecord::CoinJoinSend), + TransactionFilterProxy::TYPE(TransactionRecord::CoinJoinMakeCollaterals), + TransactionFilterProxy::TYPE(TransactionRecord::CoinJoinCreateDenominations), + TransactionFilterProxy::TYPE(TransactionRecord::CoinJoinMixing), + TransactionFilterProxy::TYPE(TransactionRecord::CoinJoinCollateralPayment)}) { + const int row = typeWidget->findData(type_filter); + if (row >= 0) typeList->setRowHidden(row, !fEnabled); + } + + int idx = typeWidget->currentIndex(); + if (idx < 0 || typeList->isRowHidden(idx)) { + // Invalid and hidden CoinJoin selections fall back to "All" or "Most Common". + idx = fEnabled ? 0 : 1; + typeWidget->setCurrentIndex(idx); } + chooseType(idx); }