Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/Makefile.qttest.include
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 \
Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions src/qt/test/test_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

#ifdef ENABLE_WALLET
#include <qt/test/addressbooktests.h>
#include <qt/test/transactionviewtests.h>
#include <qt/test/wallettests.h>
#endif // ENABLE_WALLET

Expand Down Expand Up @@ -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);
Expand Down
187 changes: 187 additions & 0 deletions src/qt/test/transactionviewtests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
// Copyright (c) 2026 The Dash Core developers

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: Register the new Dash-specific Qt test files

transactionviewtests.cpp and transactionviewtests.h are newly authored Dash-specific files, but no matching entry was added to test/util/data/non-backported.txt. That registry supplies the file list to test/lint/lint-cppcheck-dash.py, so these files currently bypass the additional Dash-specific cppcheck coverage. Add src/qt/test/transactionviewtests.* to the registry.

source: ['codex']

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 5c14ee0a3f7.

Added src/qt/test/transactionviewtests.* to test/util/data/non-backported.txt after the existing src/qt/* entries (before src/rpc/), matching the registry's established ordering/style.

// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#include <qt/test/transactionviewtests.h>

#include <interfaces/chain.h>
#include <interfaces/node.h>
#include <qt/clientmodel.h>
#include <qt/optionsmodel.h>
#include <qt/transactionfilterproxy.h>
#include <qt/transactionrecord.h>
#include <qt/transactionview.h>
#include <qt/walletmodel.h>
#include <test/util/setup_common.h>
#include <validation.h>
#include <wallet/wallet.h>

#include <memory>
#include <vector>

#include <QApplication>
#include <QComboBox>
#include <QListView>
#include <QSettings>
#include <QTest>

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<QComboBox*>()) {
if (combo->findText("All") >= 0 && combo->findText("Data Transaction") >= 0) return combo;
}
return nullptr;
}

class WalletCleanup
{
public:
WalletCleanup(WalletContext& context, std::shared_ptr<CWallet> 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<CWallet> 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<CWallet> wallet{std::make_shared<CWallet>(test.m_node.chain.get(), test.m_node.coinjoin_loader.get(),
"", test.m_args, CreateMockWalletDatabase())};
wallet->LoadWallet();
wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
wallet->SetupDescriptorScriptPubKeyMans("", "");
Comment on lines +115 to +116

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocking: Hold cs_wallet while setting up descriptor managers

CWallet::SetupDescriptorScriptPubKeyMans() is declared EXCLUSIVE_LOCKS_REQUIRED(cs_wallet), and both overloads begin with AssertLockHeld(cs_wallet). This call is made without holding the lock, so Clang emits a -Wthread-safety diagnostic; Dash CI enables both that warning and --enable-werror, making the Qt test fail to compile. A debug lock-order build would also abort when the test reaches this call. Existing Qt wallet tests acquire wallet->cs_wallet around the same setup operation.

Suggested change
wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
wallet->SetupDescriptorScriptPubKeyMans("", "");
wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
{
LOCK(wallet->cs_wallet);
wallet->SetupDescriptorScriptPubKeyMans("", "");
}

source: ['codex']

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 5c14ee0a3f7.

Wrapped SetupDescriptorScriptPubKeyMans("", "") in a narrow LOCK(wallet->cs_wallet) scope (same pattern as addressbooktests.cpp / wallettests.cpp). Verified with a one-shot clang++ compile using -Wthread-safety + -Werror=thread-safety-analysis (object built cleanly); negative control without the lock fails with the expected requires holding mutex 'wallet->cs_wallet' diagnostic.

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<SettingCase> 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<QListView*>(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));
}
}
}
31 changes: 31 additions & 0 deletions src/qt/test/transactionviewtests.h
Original file line number Diff line number Diff line change
@@ -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 <QObject>

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
29 changes: 18 additions & 11 deletions src/qt/transactionview.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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<QListView*>(typeWidget->view());
std::vector<int> 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);
}
Loading