Skip to content
Merged
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
1 change: 1 addition & 0 deletions qml/controls/CoreCheckBox.qml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import QtQuick 2.15
import QtQuick.Controls 2.15
import org.bitcoincore.qt 1.0

AbstractButton {
id: root
Expand Down
3 changes: 2 additions & 1 deletion qml/controls/InformationPage.qml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Page {

background: null
clip: true
contentWidth: root.maximumWidth

header: NavigationBar {
id: navbar
Expand All @@ -60,7 +61,7 @@ Page {

ColumnLayout {
id: information
width: Math.min(parent.width, 600)
width: Math.min(parent.width, root.maximumWidth)
anchors.horizontalCenter: parent.horizontalCenter
spacing: 0
Loader {
Expand Down
2 changes: 1 addition & 1 deletion qml/models/nodemodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -808,7 +808,7 @@ void NodeModel::ConnectToBannedListChangedSignal()
m_handler_notify_banned_list_changed = m_node.handleBannedListChanged([this]() {
QMetaObject::invokeMethod(this, [this] {
Q_EMIT bannedListChanged();
});
}, Qt::QueuedConnection);
});
}

Expand Down
17 changes: 8 additions & 9 deletions qml/pages/node/BannedPeers.qml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ Page {
signal back()
background: null

function unbanPeer(row) {
if (!banListModel.unbanAt(row)) {
unbanActionError.message = qsTr("Could not unban peer. The ban list may have changed.")
unbanActionError.open()
}
}

header: NavigationBar2 {
leftItem: NavButton {
objectName: "bannedPeersBackButton"
Expand Down Expand Up @@ -96,15 +103,7 @@ Page {
bold: false
horizontalPadding: 24
text: qsTr("Unban")
onClicked: {
if (banListModel.unbanAt(index)) {
banListModel.refresh()
} else {
unbanActionError.message = qsTr("Could not unban peer. The ban list may have changed.")
unbanActionError.open()
banListModel.refresh()
}
}
onClicked: root.unbanPeer(index)
}
}
}
Expand Down
1 change: 1 addition & 0 deletions qml/pages/settings/SettingsStorage.qml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import "../../components"

InformationPage {
id: root
objectName: "settingsStoragePage"
property var settingsModel: optionsModel
property bool customStorage: false
property int customStorageAmount
Expand Down
4 changes: 3 additions & 1 deletion qml/pages/settings/SettingsWallet.qml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ Page {

signal back
property bool showBackButton: true
readonly property int maximumContentWidth: 450

background: null
contentWidth: root.maximumContentWidth

header: SettingsHeader {
title: qsTr("External signer")
Expand All @@ -31,7 +33,7 @@ Page {
clip: true

ColumnLayout {
width: Math.min(parent.width, 450)
width: Math.min(parent.width, root.maximumContentWidth)
anchors.horizontalCenter: parent.horizontalCenter
spacing: 0

Expand Down
2 changes: 1 addition & 1 deletion qml/pages/wallet/ImportWalletOptions.qml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Page {
signal next
background: null
readonly property bool hasImportError: walletController.walletLoadError.length > 0
readonly property real heroWidth: Math.min(parent.width - 40, 520)
readonly property real heroWidth: Math.min(root.width - 40, 520)
readonly property int heroTopMargin: 64
readonly property int importHeroTopMargin: heroTopMargin + 10
readonly property int heroIconSize: 60
Expand Down
2 changes: 2 additions & 0 deletions test/qml/bitcoin_qmltests.qrc
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<file>tst_contextmenupicker.qml</file>
<file>tst_contextmenu.qml</file>
<file>tst_contextmenutoggle.qml</file>
<file>tst_corecheckbox.qml</file>
<file>tst_createbackup.qml</file>
<file>tst_createname.qml</file>
<file>tst_createpassword.qml</file>
Expand All @@ -20,6 +21,7 @@
<file>tst_dropdownbutton.qml</file>
<file>tst_externalsignerreviewactions.qml</file>
<file>tst_feeselection.qml</file>
<file>tst_importwalletoptions.qml</file>
<file>tst_mainrouting.qml</file>
<file>tst_mempoolinformationrows.qml</file>
<file>tst_mempoolinformationsettings.qml</file>
Expand Down
7 changes: 7 additions & 0 deletions test/qml/qml_tests_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2483,6 +2483,7 @@ class MockBanListModel : public QAbstractListModel
Q_OBJECT
Q_PROPERTY(int count READ count NOTIFY countChanged)
Q_PROPERTY(bool unbanResult MEMBER m_unban_result NOTIFY actionStateChanged)
Q_PROPERTY(bool resetOnUnban MEMBER m_reset_on_unban NOTIFY actionStateChanged)
Q_PROPERTY(int unbanCalls READ unbanCalls NOTIFY actionCallsChanged)
Q_PROPERTY(int refreshCalls READ refreshCalls NOTIFY refreshCallsChanged)

Expand Down Expand Up @@ -2527,6 +2528,10 @@ class MockBanListModel : public QAbstractListModel
{
++m_unban_calls;
Q_EMIT actionCallsChanged();
if (m_reset_on_unban) {
beginResetModel();
endResetModel();
}
return row >= 0 && row < count() && m_unban_result;
}

Expand All @@ -2539,6 +2544,7 @@ class MockBanListModel : public QAbstractListModel
Q_INVOKABLE void resetTestState()
{
m_unban_result = true;
m_reset_on_unban = false;
m_unban_calls = 0;
m_refresh_calls = 0;
Q_EMIT actionStateChanged();
Expand All @@ -2554,6 +2560,7 @@ class MockBanListModel : public QAbstractListModel

private:
bool m_unban_result{true};
bool m_reset_on_unban{false};
int m_unban_calls{0};
int m_refresh_calls{0};
};
Expand Down
66 changes: 66 additions & 0 deletions test/qml/tst_corecheckbox.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright (c) 2026 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

import QtQuick 2.15
import QtTest 1.2
import org.bitcoincore.qt 1.0
import "../../qml/controls"

TestCase {
name: "CoreCheckBox"
when: windowShown
width: 400
height: 200

property bool initialIsDesktop: true

Component {
id: checkBoxComponent
CoreCheckBox {}
}

function initTestCase() {
// AppMode is a process-wide singleton shared with the other test files.
initialIsDesktop = AppMode.isDesktop
}

function cleanupTestCase() {
AppMode.isDesktop = initialIsDesktop
}

function test_hover_enabled_follows_app_mode() {
const box = createTemporaryObject(checkBoxComponent, this)
verify(box !== null)

AppMode.isDesktop = true
compare(box.hoverEnabled, true)

AppMode.isDesktop = false
compare(box.hoverEnabled, false)

AppMode.isDesktop = true
compare(box.hoverEnabled, true)
}

function test_toggles_and_reflects_checked_state() {
const box = createTemporaryObject(checkBoxComponent, this)
verify(box !== null)
verify(box.checkable)
verify(!box.checked)
compare(box.contentItem.color, "#00000000")
compare(box.contentItem.border.color, box.borderColor)

box.toggle()

verify(box.checked)
compare(box.contentItem.color, box.fillColor)
compare(box.contentItem.border.color, box.fillColor)

box.toggle()

verify(!box.checked)
compare(box.contentItem.color, "#00000000")
compare(box.contentItem.border.color, box.borderColor)
}
}
72 changes: 72 additions & 0 deletions test/qml/tst_importwalletoptions.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright (c) 2026 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

import QtQuick 2.15
import QtTest 1.2
import "../../qml/pages/wallet"

TestCase {
name: "ImportWalletOptions"
when: windowShown
width: 520
height: 720

Item {
id: pageContainer
width: 460
height: 680
}

Component {
id: importWalletOptionsComponent

ImportWalletOptions {
width: 460
height: 680
}
}

function init() {
walletController.reset()
}

function test_hero_width_without_a_parent() {
const page = createTemporaryObject(importWalletOptionsComponent, null)
verify(page !== null)
compare(page.parent, null)
compare(page.heroWidth, 420)

const errorView = findChild(page, "importWalletErrorView")
verify(errorView !== null)
compare(errorView.width, 420)
}

function test_hero_width_follows_the_page_width() {
const page = createTemporaryObject(importWalletOptionsComponent, pageContainer)
verify(page !== null)
compare(page.heroWidth, 420)

page.width = 300
compare(page.heroWidth, 260)

page.width = 900
compare(page.heroWidth, 520)
}

function test_error_view_reports_the_wallet_load_error() {
const page = createTemporaryObject(importWalletOptionsComponent, pageContainer)
verify(page !== null)
verify(!page.hasImportError)

walletController.walletLoadError = "Corrupted wallet file."

verify(page.hasImportError)
const errorView = findChild(page, "importWalletErrorView")
verify(errorView !== null)
compare(errorView.width, page.heroWidth)
const errorDescription = findChild(page, "importWalletErrorDescription")
verify(errorDescription !== null)
compare(errorDescription.text, "Corrupted wallet file.")
}
}
26 changes: 26 additions & 0 deletions test/qml/tst_nodesettings.qml
Original file line number Diff line number Diff line change
Expand Up @@ -250,4 +250,30 @@ TestCase {
verify(walletStack.depth > 1)
compare(walletSettingsPage.showBackButton, false)
}

function test_section_pages_keep_implicit_width_independent_of_layout() {
const page = createNodeSettingsPage()

const sections = [
{ index: 1, name: "settingsWallet" },
{ index: 4, name: "settingsStoragePage" },
{ index: 7, name: "mempoolInformationSettingsPage" }
]

for (let i = 0; i < sections.length; ++i) {
page.currentSection = sections[i].index
wait(0)

const sectionPage = findChild(page, sections[i].name)
verify(sectionPage !== null, sections[i].name + " was not created")

const implicitWidthBefore = sectionPage.implicitWidth
const widthBefore = sectionPage.width
sectionPage.width = widthBefore / 2
verify(sectionPage.width !== widthBefore,
sections[i].name + " ignored the width it was given")
compare(sectionPage.implicitWidth, implicitWidthBefore,
sections[i].name + " implicit width followed the width it was given")
}
}
}
47 changes: 40 additions & 7 deletions test/qml/tst_peeractions.qml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,15 @@ TestCase {
verify(message.text.indexOf(expectedText) >= 0)
}

function verifyUnbanActionError(page) {
const popup = findChild(page, "unbanActionErrorPopup")
verify(popup !== null)
tryCompare(popup, "opened", true)
const message = findChild(popup, "actionErrorMessage")
verify(message !== null)
verify(message.text.indexOf("Could not unban peer.") >= 0)
}

function test_disconnect_success_refreshes_peer_table_without_error() {
const page = createPeerDetailsPage()
const button = findChild(page, "peerDisconnectButton")
Expand Down Expand Up @@ -151,7 +160,7 @@ TestCase {
verifyPeerActionError(page, "Could not ban peer.")
}

function test_unban_success_refreshes_ban_list_without_error() {
function test_unban_success_leaves_refresh_to_the_model_without_error() {
const page = createBannedPeersPage()
const button = findChild(page, "unbanButton_0")
const popup = findChild(page, "unbanActionErrorPopup")
Expand All @@ -161,7 +170,7 @@ TestCase {
button.clicked()

compare(banListModel.unbanCalls, 1)
compare(banListModel.refreshCalls, 1)
compare(banListModel.refreshCalls, 0)
compare(popup.opened, false)
}

Expand All @@ -174,12 +183,36 @@ TestCase {
button.clicked()

compare(banListModel.unbanCalls, 1)
compare(banListModel.refreshCalls, 1)
compare(banListModel.refreshCalls, 0)
verifyUnbanActionError(page)
}

function test_unban_survives_synchronous_model_reset() {
banListModel.resetOnUnban = true
const page = createBannedPeersPage()
const button = findChild(page, "unbanButton_0")
const popup = findChild(page, "unbanActionErrorPopup")
verify(button !== null)
verify(popup !== null)
tryCompare(popup, "opened", true)
const message = findChild(popup, "actionErrorMessage")
verify(message !== null)
verify(message.text.indexOf("Could not unban peer.") >= 0)

button.clicked()

compare(banListModel.unbanCalls, 1)
compare(banListModel.refreshCalls, 0)
compare(popup.opened, false)
}

function test_unban_failure_with_synchronous_model_reset_opens_error_popup() {
banListModel.resetOnUnban = true
banListModel.unbanResult = false
const page = createBannedPeersPage()
const button = findChild(page, "unbanButton_0")
verify(button !== null)

button.clicked()

compare(banListModel.unbanCalls, 1)
compare(banListModel.refreshCalls, 0)
verifyUnbanActionError(page)
}
}
Loading
Loading