tests: add QML wallet wizard coverage
What changed, and why it matters
This commit adds automated user-interface tests for the Monero wallet setup wizard. It does not change how real users create or open wallets; it only adds test code and exposes a few internal UI control names so the tests can interact with wizard pages. There is no security fix or vulnerability here.
No security action needed. Treat as normal test-infrastructure change. Reviewers may want to confirm the test harness only runs when --test-qml is passed and that test wallets are created in the temporary directory, which the diff supports.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit introduces a QML test harness (QmlTestHarness.cpp/.h), a new tst_Wizard.qml test suite, and CMake/GitHub Actions plumbing to run QML tests with –test-qml. Wizard QML pages gain id/alias properties (walletInput, wizardNav, recentWallets, etc.) so the tests can drive them. PasswordDialog exposes its accept button. The harness registers wallet types, sets fake platform flags, and uses a temporary directory for test wallets. No runtime wallet logic is modified.
Changed components
tests/qml/QmlTestHarness.cpptests/qml/QmlTestHarness.htests/qml/tst_Wizard.qmlwizard/WizardCreateWallet1.qmlwizard/WizardCreateWallet2.qmlwizard/WizardCreateWallet3.qmlwizard/WizardCreateWallet4.qmlwizard/WizardHome.qmlwizard/WizardOpenWallet1.qmlwizard/WizardRestoreWallet1.qmlwizard/WizardRestoreWallet2.qmlwizard/WizardRestoreWallet3.qmlcomponents/PasswordDialog.qmlCMakeLists.txtsrc/CMakeLists.txtsrc/main/main.cpp.github/workflows/build.ymlInspect captured patch +686 / −2
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 53cb0f3..722b3fa 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -40,7 +40,7 @@ jobs:
- name: install monero dependencies
run: sudo apt -y install build-essential cmake ninja-build libboost-all-dev libunbound-dev graphviz doxygen pkg-config libssl-dev libzmq3-dev libsodium-dev libhidapi-dev libnorm-dev libusb-1.0-0-dev libpgm-dev libprotobuf-dev protobuf-compiler
- name: install monero gui dependencies
- run: sudo apt -y install qtbase5-dev qtdeclarative5-dev qml-module-qtqml-models2 qml-module-qtquick-controls qml-module-qtquick-controls2 qml-module-qtquick-dialogs qml-module-qtquick-xmllistmodel qml-module-qt-labs-settings qml-module-qt-labs-platform qml-module-qt-labs-folderlistmodel qttools5-dev-tools qml-module-qtquick-templates2 libqt5svg5-dev libgcrypt20-dev xvfb
+ run: sudo apt -y install qtbase5-dev qtdeclarative5-dev qml-module-qtqml-models2 qml-module-qtquick-controls qml-module-qtquick-controls2 qml-module-qtquick-dialogs qml-module-qtquick-xmllistmodel qml-module-qt-labs-settings qml-module-qt-labs-platform qml-module-qt-labs-folderlistmodel qml-module-qttest qttools5-dev-tools qml-module-qtquick-templates2 libqt5svg5-dev libgcrypt20-dev xvfb
- name: build
run: DEV_MODE=ON make release
- name: test qml
@@ -71,7 +71,7 @@ jobs:
run: cmake --build . --target deploy
working-directory: build/release
- name: test qml
- run: build/release/bin/monero-wallet-gui --test-qml
+ run: build/release/bin/monero-wallet-gui --test-qml -o test.log,txt || { rc=$?; cat test.log; exit "$rc"; }
macos-bundle:
runs-on: macos-15
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 876029d..23fc0ea 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,6 +1,8 @@
cmake_minimum_required(VERSION 3.12)
project(monero-gui)
+include(CMakeDependentOption)
+
message(STATUS "Initiating compile using CMake ${CMAKE_VERSION}")
set(VERSION_MAJOR "18")
@@ -15,6 +17,7 @@ option(WITH_SCANNER "Enable webcam QR scanner" OFF)
option(WITH_DESKTOP_ENTRY "Ask to install desktop entry on first startup" ON)
option(WITH_UPDATER "Regularly check for new updates" ON)
option(DEV_MODE "Checkout latest monero master on build" OFF)
+cmake_dependent_option(QML_TESTS "Build QML tests" ON "NOT STATIC;NOT ANDROID;NOT IOS" OFF)
if(DEV_MODE)
# DEV_MODE checks out the monero submodule to master, which requires C++17.
@@ -122,6 +125,10 @@ set(QT5_LIBRARIES
Qt5Xml
)
+if(QML_TESTS)
+ list(APPEND QT5_LIBRARIES Qt5QuickTest Qt5Test)
+endif()
+
if(WITH_SCANNER)
list(APPEND QT5_LIBRARIES Qt5Multimedia)
endif()
diff --git a/components/PasswordDialog.qml b/components/PasswordDialog.qml
index f369593..0ffa980 100644
--- a/components/PasswordDialog.qml
+++ b/components/PasswordDialog.qml
@@ -43,6 +43,7 @@ FocusScope {
visible: false
property alias password: passwordInput1.text
+ property alias acceptButton: okButton
property string walletName
property var okButtonText
property string okButtonIcon
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index d29be1c..bcbda89 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -43,6 +43,9 @@ file(GLOB SOURCE_FILES
"qt/*.h"
"qt/*.cpp"
)
+if(QML_TESTS)
+ list(APPEND SOURCE_FILES "${CMAKE_SOURCE_DIR}/tests/qml/QmlTestHarness.cpp")
+endif()
if(APPLE)
list(APPEND SOURCE_FILES "qt/macoshelper.mm")
endif()
@@ -112,6 +115,14 @@ target_compile_definitions(monero-wallet-gui
${Qt5Qml_DEFINITIONS}
)
+if(QML_TESTS)
+ target_include_directories(monero-wallet-gui PRIVATE "${CMAKE_SOURCE_DIR}/tests/qml")
+ target_compile_definitions(monero-wallet-gui PRIVATE
+ QML_TESTS
+ QML_TEST_SOURCE_DIR="${CMAKE_SOURCE_DIR}/tests/qml"
+ )
+endif()
+
if(APPLE)
if(NOT ICU_ROOT)
execute_process(COMMAND brew --prefix icu4c OUTPUT_VARIABLE ICU_ROOT OUTPUT_STRIP_TRAILING_WHITESPACE)
diff --git a/src/main/main.cpp b/src/main/main.cpp
index 21993ea..6afbaa8 100644
--- a/src/main/main.cpp
+++ b/src/main/main.cpp
@@ -72,6 +72,9 @@
#include "qt/KeysFiles.h"
#include "qt/MoneroSettings.h"
#include "qt/NetworkAccessBlockingFactory.h"
+#ifdef QML_TESTS
+#include "QmlTestHarness.h"
+#endif
#ifdef Q_OS_MAC
#include "qt/macoshelper.h"
#endif
@@ -162,6 +165,12 @@ int main(int argc, char *argv[])
{
Q_INIT_RESOURCE(translations);
+#ifdef QML_TESTS
+ int qmlTestResult;
+ if (runQmlTestsIfRequested(argc, argv, qmlTestResult))
+ return qmlTestResult;
+#endif
+
// platform dependant settings
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
bool isDesktop = true;
diff --git a/tests/qml/QmlTestHarness.cpp b/tests/qml/QmlTestHarness.cpp
new file mode 100644
index 0000000..cd00465
--- /dev/null
+++ b/tests/qml/QmlTestHarness.cpp
@@ -0,0 +1,108 @@
+// Copyright (c) 2026, The Monero Project
+//
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without modification, are
+// permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this list of
+// conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice, this list
+// of conditions and the following disclaimer in the documentation and/or other
+// materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its contributors may be
+// used to endorse or promote products derived from this software without specific
+// prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "QmlTestHarness.h"
+
+#include <QDir>
+#include <QQmlContext>
+#include <QQmlEngine>
+#include <QTemporaryDir>
+#include <QVector>
+#include <QtQml>
+#include <QtQuickTest/quicktest.h>
+
+#include "TranslationManager.h"
+#include "libwalletqt/Wallet.h"
+#include "libwalletqt/WalletManager.h"
+#include "main/clipboardAdapter.h"
+#include "main/oshelper.h"
+#include "qt/KeysFiles.h"
+
+class QmlTestSetup : public QObject
+{
+ Q_OBJECT
+
+public:
+ QmlTestSetup() : m_accountsDir(QDir::tempPath() + QStringLiteral("/monero-gui-qml-test-XXXXXX")) {}
+
+public slots:
+ void qmlEngineAvailable(QQmlEngine *engine)
+ {
+ qmlRegisterType<clipboardAdapter>("moneroComponents.Clipboard", 1, 0, "Clipboard");
+ qmlRegisterType<WalletKeysFilesModel>("moneroComponents.WalletKeysFilesModel", 1, 0, "WalletKeysFilesModel");
+ qmlRegisterType<WalletManager>("moneroComponents.WalletManager", 1, 0, "WalletManager");
+ qmlRegisterUncreatableType<Wallet>("moneroComponents.Wallet", 1, 0, "Wallet", "Wallet can't be instantiated directly");
+ qmlRegisterType<NetworkType>("moneroComponents.NetworkType", 1, 0, "NetworkType");
+
+ engine->addImportPath(QStringLiteral(":/fonts"));
+ engine->rootContext()->setContextProperty(QStringLiteral("translationManager"), TranslationManager::instance());
+ engine->rootContext()->setContextProperty(QStringLiteral("oshelper"), &m_osHelper);
+ engine->rootContext()->setContextProperty(
+ QStringLiteral("moneroAccountsDir"),
+ QDir(m_accountsDir.path()).filePath(QStringLiteral("Monero/wallets")));
+ engine->rootContext()->setContextProperty(QStringLiteral("defaultAccountName"), QStringLiteral("qml-test-wallet"));
+ engine->rootContext()->setContextProperty(QStringLiteral("isAndroid"), false);
+ engine->rootContext()->setContextProperty(QStringLiteral("isIOS"), false);
+ engine->rootContext()->setContextProperty(QStringLiteral("isLinux"), false);
+ engine->rootContext()->setContextProperty(QStringLiteral("isMac"), false);
+ engine->rootContext()->setContextProperty(QStringLiteral("isWindows"), false);
+ engine->rootContext()->setContextProperty(QStringLiteral("isTails"), false);
+ engine->rootContext()->setContextProperty(QStringLiteral("isOpenGL"), false);
+ engine->rootContext()->setContextProperty(QStringLiteral("qtRuntimeVersion"), QString::fromLatin1(qVersion()));
+ }
+
+private:
+ OSHelper m_osHelper;
+ QTemporaryDir m_accountsDir;
+};
+
+bool runQmlTestsIfRequested(int argc, char *argv[], int &result)
+{
+ bool requested = false;
+ QVector<char *> testArgv;
+ testArgv.reserve(argc);
+
+ for (int i = 0; i < argc; ++i) {
+ if (QString::fromLocal8Bit(argv[i]) == QStringLiteral("--test-qml")) {
+ requested = true;
+ } else {
+ testArgv.append(argv[i]);
+ }
+ }
+
+ if (!requested)
+ return false;
+
+ int testArgc = testArgv.size();
+ QmlTestSetup setup;
+ result = quick_test_main_with_setup(
+ testArgc, testArgv.data(), "monero-gui-qml-tests", QML_TEST_SOURCE_DIR, &setup);
+ return true;
+}
+
+#include "QmlTestHarness.moc"
diff --git a/tests/qml/QmlTestHarness.h b/tests/qml/QmlTestHarness.h
new file mode 100644
index 0000000..35438db
--- /dev/null
+++ b/tests/qml/QmlTestHarness.h
@@ -0,0 +1,34 @@
+// Copyright (c) 2026, The Monero Project
+//
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without modification, are
+// permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this list of
+// conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice, this list
+// of conditions and the following disclaimer in the documentation and/or other
+// materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its contributors may be
+// used to endorse or promote products derived from this software without specific
+// prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef QMLTESTHARNESS_H
+#define QMLTESTHARNESS_H
+
+bool runQmlTestsIfRequested(int argc, char *argv[], int &result);
+
+#endif // QMLTESTHARNESS_H
diff --git a/tests/qml/tst_Wizard.qml b/tests/qml/tst_Wizard.qml
new file mode 100644
index 0000000..613cb58
--- /dev/null
+++ b/tests/qml/tst_Wizard.qml
@@ -0,0 +1,491 @@
+// Copyright (c) 2026, The Monero Project
+//
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without modification, are
+// permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this list of
+// conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice, this list
+// of conditions and the following disclaimer in the documentation and/or other
+// materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its contributors may be
+// used to endorse or promote products derived from this software without specific
+// prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import QtQuick 2.9
+import QtTest 1.2
+
+import moneroComponents.NetworkType 1.0
+import moneroComponents.Wallet 1.0
+import moneroComponents.WalletManager 1.0
+
+import "../../components" as MoneroComponents
+import "../../wizard"
+
+Item {
+ id: appWindow
+ width: 1000
+ height: 800
+
+ property alias persistentSettings: persistentSettings
+ property alias wizard: wizardController
+ property string accountsDir: moneroAccountsDir
+ property int walletMode: persistentSettings.walletMode
+ property bool ctrlPressed: false
+ property bool themeTransition: false
+ property var currentWallet
+ property string walletPassword: ""
+ property int restoreHeight: 0
+ property string lastWalletOpenError: ""
+ property bool walletCreated: false
+ property bool walletOpenRequested: false
+ property bool qrScannerEnabled: false
+ property bool hideBalanceForced: false
+ property bool active: true
+
+ function updateBalance() {}
+
+ function showStatusMessage() {}
+
+ function releaseFocus() {}
+
+ function openWallet() {
+ passwordDialog.open(persistentSettings.wallet_path)
+ }
+
+ QtObject {
+ id: persistentSettings
+ property int walletMode: 2
+ property int nettype: NetworkType.MAINNET
+ property int kdfRounds: 1
+ property string language_wallet: "English"
+ property bool customDecorations: false
+ property string bootstrapNodeAddress: ""
+ property string blockchainDataDir: ""
+ property string language: "English (US)"
+ property string account_name: ""
+ property string wallet_path: ""
+ property int restore_height: 0
+ property bool allow_background_mining: false
+ property bool is_recovering: false
+ property bool is_recovering_from_device: false
+ property bool portable: false
+ property bool pruneBlockchain: false
+ property bool useRemoteNode: false
+ function setPortable() { return true }
+ function setWritable() { return true }
+ }
+
+ QtObject {
+ id: daemonManager
+ function checkLmdbExists() { return false }
+ }
+
+ ListModel {
+ id: remoteNodesModel
+ property int selected: 0
+ function currentRemoteNode() { return { "address": "" } }
+ function applyRemoteNode() {}
+ function removeSelectNextIfNeeded() {}
+ }
+
+ QtObject {
+ id: splash
+ function close() {}
+ }
+
+ QtObject {
+ id: leftPanel
+ property bool enabled: true
+ }
+
+ QtObject {
+ id: middlePanel
+ property bool enabled: true
+ }
+
+ QtObject {
+ id: titleBar
+ property string state: ""
+ }
+
+ QtObject {
+ id: rootItem
+ property string state: "wizard"
+ }
+
+ QtObject {
+ id: devicePassphraseDialog
+ property var onAcceptedCallback
+ property var onWalletEntryCallback
+ property var onRejectedCallback
+ function open() {}
+ }
+
+ WalletManager {
+ id: walletManager
+ }
+
+ MoneroComponents.PasswordDialog {
+ id: passwordDialog
+ anchors.fill: parent
+
+ onAccepted: {
+ appWindow.walletPassword = password
+ appWindow.lastWalletOpenError = ""
+ var wallet = walletManager.openWallet(
+ persistentSettings.wallet_path, password,
+ persistentSettings.nettype, persistentSettings.kdfRounds)
+ if (wallet.status === Wallet.Status_Ok) {
+ appWindow.currentWallet = wallet
+ appWindow.walletOpenRequested = true
+ } else {
+ var error = wallet.errorString
+ appWindow.lastWalletOpenError = error
+ walletManager.closeWallet()
+ appWindow.currentWallet = undefined
+ showError(error)
+ }
+ }
+ }
+
+ WizardController {
+ id: wizardController
+ anchors.fill: parent
+ onUseMoneroClicked: {
+ appWindow.walletCreated = true
+ appWindow.openWallet()
+ }
+ }
+
+ TestCase {
+ name: "Wizard"
+ when: windowShown
+
+ function walletPath(walletName) {
+ return appWindow.accountsDir + "/" + walletName + "/" + walletName
+ }
+
+ function showWizardHome() {
+ wizardController.wizardState = "wizardHome"
+ tryCompare(wizardController, "wizardState", "wizardHome")
+ }
+
+ function goToCreatePasswordPage(walletName) {
+ showWizardHome()
+ wizardController.wizardStateView.wizardHomeView.createWalletButton.menuClicked()
+ tryCompare(wizardController, "wizardState", "wizardCreateWallet1")
+
+ var createWallet1 = wizardController.wizardStateView.wizardCreateWallet1View
+ createWallet1.walletInput.walletName.text = walletName
+ verify(createWallet1.wizardNav.btnNext.enabled)
+ createWallet1.wizardNav.btnNext.doClick()
+ tryCompare(wizardController, "wizardState", "wizardCreateWallet2")
+
+ // The first click hides five seed words; the second confirms them. Holding
+ // Ctrl is the Wizard's built-in way to skip manual seed entry in automated
+ // and accessibility-driven flows.
+ appWindow.ctrlPressed = true
+ var createWallet2 = wizardController.wizardStateView.wizardCreateWallet2View
+ createWallet2.wizardNav.btnNext.doClick()
+ compare(createWallet2.state, "verify")
+ createWallet2.wizardNav.btnNext.doClick()
+ appWindow.ctrlPressed = false
+ tryCompare(wizardController, "wizardState", "wizardCreateWallet3")
+
+ return wizardController.wizardStateView.wizardCreateWallet3View
+ }
+
+ function createWalletThroughWizard(walletName, password) {
+ var createWallet3 = goToCreatePasswordPage(walletName)
+
+ createWallet3.pwField = password
+ createWallet3.pwConfirmField = password
+ verify(createWallet3.wizardNav.btnNext.enabled)
+ createWallet3.wizardNav.btnNext.doClick()
+
+ if (appWindow.walletMode >= 2) {
+ tryCompare(wizardController, "wizardState", "wizardCreateWallet4")
+ wizardController.wizardStateView.wizardCreateWallet4View.wizardNav.btnNext.doClick()
+ }
+ tryCompare(wizardController, "wizardState", "wizardCreateWallet5")
+
+ var seed = wizardController.walletOptionsSeed
+ appWindow.walletCreated = false
+ appWindow.walletOpenRequested = false
+ wizardController.wizardStateView.wizardCreateWallet5View.wizardNav.btnNext.doClick()
+ tryCompare(appWindow, "walletCreated", true, 30000)
+ compare(walletManager.localPathToUrl(persistentSettings.wallet_path),
+ walletManager.localPathToUrl(walletPath(walletName)))
+ var address = acceptWalletPassword(password)
+ closeOpenedWallet()
+
+ return {
+ "address": address,
+ "password": password,
+ "path": persistentSettings.wallet_path,
+ "seed": seed
+ }
+ }
+
+ function acceptWalletPassword(password) {
+ tryCompare(passwordDialog, "visible", true)
+ passwordDialog.password = password
+ passwordDialog.acceptButton.doClick()
+ tryCompare(appWindow, "walletOpenRequested", true)
+ compare(appWindow.currentWallet.status, Wallet.Status_Ok,
+ appWindow.currentWallet.errorString)
+ var address = appWindow.currentWallet.address(0, 0)
+ verify(address.length > 0)
+ return address
+ }
+
+ function openRecentWallet(walletName, password) {
+ persistentSettings.wallet_path = ""
+ appWindow.walletOpenRequested = false
+ appWindow.lastWalletOpenError = ""
+ showWizardHome()
+ var wizardHome = wizardController.wizardStateView.wizardHomeView
+ wizardHome.openWalletButton.menuClicked()
+ tryCompare(wizardController, "wizardState", "wizardOpenWallet1")
+
+ var openWalletView = wizardController.wizardStateView.wizardOpenWallet1View
+ tryVerify(function() { return openWalletView.walletCount > 0 })
+ var recentWallet = null
+ for (var i = 0; i < openWalletView.recentWallets.count; ++i) {
+ var candidate = openWalletView.recentWallets.itemAt(i)
+ if (candidate.walletFileName === walletName) {
+ recentWallet = candidate
+ break
+ }
+ }
+ verify(recentWallet !== null, "Recent wallet not found: " + walletName)
+ recentWallet.forceActiveFocus()
+ verify(recentWallet.activeFocus)
+ keyClick(Qt.Key_Return)
+ verify(persistentSettings.wallet_path.length > 0)
+ tryCompare(passwordDialog, "visible", true)
+
+ passwordDialog.password = password
+ passwordDialog.acceptButton.doClick()
+ return persistentSettings.wallet_path
+ }
+
+ function closeOpenedWallet() {
+ if (typeof appWindow.currentWallet !== "undefined") {
+ walletManager.closeWallet()
+ appWindow.currentWallet = undefined
+ }
+ }
+
+ function init() {
+ appWindow.ctrlPressed = false
+ appWindow.walletCreated = false
+ appWindow.walletOpenRequested = false
+ appWindow.currentWallet = undefined
+ persistentSettings.wallet_path = ""
+ wizardController.restart()
+ showWizardHome()
+ }
+
+ function cleanup() {
+ appWindow.ctrlPressed = false
+ closeOpenedWallet()
+ if (passwordDialog.visible)
+ passwordDialog.onCancel()
+ wizardController.restart()
+ showWizardHome()
+ }
+
+ function test_create_password_wallet_and_open_it() {
+ var createdWallet = createWalletThroughWizard(
+ "wizard-password-test", "correct horse battery staple")
+ var selectedWalletPath = openRecentWallet(
+ "wizard-password-test", "definitely not the wallet password")
+ compare(selectedWalletPath, createdWallet.path)
+ verify(!appWindow.walletOpenRequested)
+ verify(typeof appWindow.currentWallet === "undefined")
+ verify(appWindow.lastWalletOpenError.length > 0)
+ tryCompare(passwordDialog, "visible", true)
+
+ passwordDialog.password = createdWallet.password
+ passwordDialog.acceptButton.doClick()
+ tryCompare(appWindow, "walletOpenRequested", true)
+ compare(appWindow.currentWallet.status, Wallet.Status_Ok,
+ appWindow.currentWallet.errorString)
+ compare(appWindow.currentWallet.path, selectedWalletPath)
+ compare(appWindow.currentWallet.address(0, 0), createdWallet.address)
+ closeOpenedWallet()
+ }
+
+ function test_existing_wallet_is_not_overwritten() {
+ var walletName = "existing-wallet-test"
+ var createdWallet = createWalletThroughWizard(walletName, "original password")
+
+ showWizardHome()
+ wizardController.wizardStateView.wizardHomeView.createWalletButton.menuClicked()
+ tryCompare(wizardController, "wizardState", "wizardCreateWallet1")
+
+ var createWallet1 = wizardController.wizardStateView.wizardCreateWallet1View
+ createWallet1.walletInput.walletName.text = walletName
+ verify(createWallet1.walletInput.walletName.error)
+ verify(createWallet1.walletInput.errorMessageWalletName.text.length > 0)
+ verify(!createWallet1.wizardNav.btnNext.enabled)
+
+ wizardController.restart()
+ var selectedWalletPath = openRecentWallet(walletName, createdWallet.password)
+ compare(selectedWalletPath, createdWallet.path)
+ tryCompare(appWindow, "walletOpenRequested", true)
+ compare(appWindow.currentWallet.status, Wallet.Status_Ok,
+ appWindow.currentWallet.errorString)
+ compare(appWindow.currentWallet.address(0, 0), createdWallet.address)
+ closeOpenedWallet()
+ }
+
+ function test_invalid_wallet_location_disables_next() {
+ showWizardHome()
+ wizardController.wizardStateView.wizardHomeView.createWalletButton.menuClicked()
+ tryCompare(wizardController, "wizardState", "wizardCreateWallet1")
+
+ var createWallet1 = wizardController.wizardStateView.wizardCreateWallet1View
+ createWallet1.walletInput.walletName.text = "invalid-location-test"
+ createWallet1.walletInput.walletLocation.text =
+ appWindow.accountsDir + "-missing-" + Date.now()
+ verify(createWallet1.walletInput.walletLocation.error)
+ verify(createWallet1.walletInput.errorMessageWalletLocation.text.length > 0)
+ verify(!createWallet1.wizardNav.btnNext.enabled)
+ compare(wizardController.wizardState, "wizardCreateWallet1")
+ }
+
+ function test_password_confirmation_must_match() {
+ var createWallet3 = goToCreatePasswordPage("password-mismatch-test")
+ createWallet3.pwField = "one password"
+ createWallet3.pwConfirmField = "a different password"
+ verify(!createWallet3.wizardNav.btnNext.enabled)
+ compare(wizardController.wizardState, "wizardCreateWallet3")
+
+ createWallet3.pwConfirmField = createWallet3.pwField
+ verify(createWallet3.wizardNav.btnNext.enabled)
+
+ createWallet3.wizardNav.btnPrev.doClick()
+ tryCompare(wizardController, "wizardState", "wizardCreateWallet2")
+ var createWallet2 = wizardController.wizardStateView.wizardCreateWallet2View
+ createWallet2.wizardNav.btnPrev.doClick()
+ tryCompare(wizardController, "wizardState", "wizardCreateWallet1")
+ var createWallet1 = wizardController.wizardStateView.wizardCreateWallet1View
+ createWallet1.wizardNav.btnPrev.doClick()
+ tryCompare(wizardController, "wizardState", "wizardHome")
+ }
+
+ function test_seed_confirmation_accepts_hidden_words() {
+ showWizardHome()
+ wizardController.wizardStateView.wizardHomeView.createWalletButton.menuClicked()
+ tryCompare(wizardController, "wizardState", "wizardCreateWallet1")
+
+ var createWallet1 = wizardController.wizardStateView.wizardCreateWallet1View
+ createWallet1.walletInput.walletName.text = "seed-confirmation-test"
+ verify(createWallet1.wizardNav.btnNext.enabled)
+ createWallet1.wizardNav.btnNext.doClick()
+ tryCompare(wizardController, "wizardState", "wizardCreateWallet2")
+
+ var createWallet2 = wizardController.wizardStateView.wizardCreateWallet2View
+ createWallet2.wizardNav.btnNext.doClick()
+ compare(createWallet2.state, "verify")
+ verify(!createWallet2.wizardNav.btnNext.enabled)
+
+ for (var i = 0; i < createWallet2.hiddenWords.length; ++i) {
+ var wordIndex = createWallet2.hiddenWords[i]
+ var seedItem = createWallet2.seedListGrid.children[wordIndex]
+ verify(seedItem.lineEdit.visible)
+ if (i === 0) {
+ seedItem.lineEdit.text = "incorrect"
+ verify(!seedItem.icon.wordsMatch)
+ verify(!createWallet2.wizardNav.btnNext.enabled)
+ compare(createWallet2.state, "verify")
+ seedItem.lineEdit.text = ""
+ }
+ var word = createWallet2.seedArray[wordIndex]
+ seedItem.lineEdit.text = word
+ tryCompare(seedItem.icon, "wordsMatch", true)
+ }
+
+ verify(createWallet2.wizardNav.btnNext.enabled)
+ createWallet2.wizardNav.btnNext.doClick()
+ tryCompare(wizardController, "wizardState", "wizardCreateWallet3")
+ }
+
+ function test_seed_round_trip_restores_primary_address() {
+ var createdWallet = createWalletThroughWizard(
+ "seed-source-test", "source password")
+
+ var selectedSourcePath = openRecentWallet(
+ "seed-source-test", createdWallet.password)
+ compare(selectedSourcePath, createdWallet.path)
+ tryCompare(appWindow, "walletOpenRequested", true)
+ compare(appWindow.currentWallet.status, Wallet.Status_Ok,
+ appWindow.currentWallet.errorString)
+ var sourcePrimaryAddress = appWindow.currentWallet.address(0, 0)
+ compare(sourcePrimaryAddress, createdWallet.address)
+ closeOpenedWallet()
+
+ showWizardHome()
+ wizardController.wizardStateView.wizardHomeView.restoreWalletButton.menuClicked()
+ tryCompare(wizardController, "wizardState", "wizardRestoreWallet1")
+
+ var restoreWallet1 = wizardController.wizardStateView.wizardRestoreWallet1View
+ restoreWallet1.walletInput.walletName.text = "seed-restored-test"
+ restoreWallet1.seedInput.text = createdWallet.seed
+ restoreWallet1.restoreHeight.text = "0"
+ verify(restoreWallet1.wizardNav.btnNext.enabled)
+ restoreWallet1.wizardNav.btnNext.doClick()
+ tryCompare(wizardController, "wizardState", "wizardRestoreWallet2")
+
+ var restoreWallet2 = wizardController.wizardStateView.wizardRestoreWallet2View
+ var restoredPassword = "restored password"
+ restoreWallet2.pwField = restoredPassword
+ restoreWallet2.pwConfirmField = restoreWallet2.pwField
+ verify(restoreWallet2.wizardNav.btnNext.enabled)
+ restoreWallet2.wizardNav.btnNext.doClick()
+
+ if (appWindow.walletMode >= 2) {
+ tryCompare(wizardController, "wizardState", "wizardRestoreWallet3")
+ wizardController.wizardStateView.wizardRestoreWallet3View.wizardNav.btnNext.doClick()
+ }
+ tryCompare(wizardController, "wizardState", "wizardRestoreWallet4")
+
+ var restoreWallet4 = wizardController.wizardStateView.wizardRestoreWallet4View
+ appWindow.walletCreated = false
+ appWindow.walletOpenRequested = false
+ restoreWallet4.wizardNav.btnNext.doClick()
+ tryCompare(appWindow, "walletCreated", true, 30000)
+ var restoredWalletPath = persistentSettings.wallet_path
+ var restoredDirectAddress = acceptWalletPassword(restoredPassword)
+ compare(restoredDirectAddress, sourcePrimaryAddress)
+ closeOpenedWallet()
+
+ var selectedRestoredPath = openRecentWallet(
+ "seed-restored-test", restoredPassword)
+ compare(selectedRestoredPath, restoredWalletPath)
+ tryCompare(appWindow, "walletOpenRequested", true)
+ compare(appWindow.currentWallet.status, Wallet.Status_Ok,
+ appWindow.currentWallet.errorString)
+ compare(appWindow.currentWallet.address(0, 0), restoredDirectAddress)
+ compare(appWindow.currentWallet.address(0, 0), sourcePrimaryAddress)
+ closeOpenedWallet()
+ }
+ }
+}
diff --git a/wizard/WizardCreateWallet1.qml b/wizard/WizardCreateWallet1.qml
index 46b1810..40ebf4d 100644
--- a/wizard/WizardCreateWallet1.qml
+++ b/wizard/WizardCreateWallet1.qml
@@ -41,6 +41,8 @@ Rectangle {
color: "transparent"
property alias pageHeight: pageRoot.height
property alias pageRoot: pageRoot
+ property alias walletInput: walletInput
+ property alias wizardNav: wizardNav
property string viewName: "wizardCreateWallet1"
ColumnLayout {
diff --git a/wizard/WizardCreateWallet2.qml b/wizard/WizardCreateWallet2.qml
index 7664474..f52fd20 100644
--- a/wizard/WizardCreateWallet2.qml
+++ b/wizard/WizardCreateWallet2.qml
@@ -42,6 +42,7 @@ Rectangle {
color: "transparent"
property alias pageHeight: pageRoot.height
property alias pageRoot: pageRoot
+ property alias wizardNav: navigation
property string viewName: "wizardCreateWallet2"
property var seedArray: wizardController.walletOptionsSeed.split(" ")
property var seedListGrid: ""
diff --git a/wizard/WizardCreateWallet3.qml b/wizard/WizardCreateWallet3.qml
index dfb51f1..0ea1629 100644
--- a/wizard/WizardCreateWallet3.qml
+++ b/wizard/WizardCreateWallet3.qml
@@ -40,6 +40,7 @@ Rectangle {
property string viewName: "wizardCreateWallet3"
property alias pwField: passwordFields.password
property alias pwConfirmField: passwordFields.passwordConfirm
+ property alias wizardNav: wizardNav
ColumnLayout {
id: pageRoot
@@ -62,6 +63,7 @@ Rectangle {
}
WizardNav {
+ id: wizardNav
progressSteps: appWindow.walletMode <= 1 ? 4 : 5
progress: 2
btnNext.enabled: passwordFields.calcStrengthAndVerify();
diff --git a/wizard/WizardCreateWallet4.qml b/wizard/WizardCreateWallet4.qml
index 8919b53..a42af60 100644
--- a/wizard/WizardCreateWallet4.qml
+++ b/wizard/WizardCreateWallet4.qml
@@ -37,6 +37,7 @@ Rectangle {
color: "transparent"
property alias pageHeight: pageRoot.height
+ property alias wizardNav: wizardNav
property string viewName: "wizardCreateWallet4"
ColumnLayout {
@@ -65,6 +66,7 @@ Rectangle {
}
WizardNav {
+ id: wizardNav
progressSteps: 5
progress: 3
onPrevClicked: {
diff --git a/wizard/WizardHome.qml b/wizard/WizardHome.qml
index dcd2d6f..d55ddd7 100644
--- a/wizard/WizardHome.qml
+++ b/wizard/WizardHome.qml
@@ -38,6 +38,9 @@ Rectangle {
id: wizardHome
color: "transparent"
property alias pageHeight: pageRoot.height
+ property alias createWalletButton: createWalletButton
+ property alias openWalletButton: openWalletButton
+ property alias restoreWalletButton: restoreWalletButton
property string viewName: "wizardHome"
ColumnLayout {
@@ -73,6 +76,7 @@ Rectangle {
}
WizardMenuItem {
+ id: createWalletButton
headerText: {
var nettype = persistentSettings.nettype;
return qsTr("Create a new wallet") + (nettype === 2 ? " (" + qsTr("stagenet") + ")"
@@ -125,6 +129,7 @@ Rectangle {
}
WizardMenuItem {
+ id: openWalletButton
headerText: qsTr("Open a wallet from file") + translationManager.emptyString
bodyText: qsTr("Import an existing .keys wallet file from your computer.") + translationManager.emptyString
imageIcon: "qrc:///images/open-wallet-from-file.png"
@@ -145,6 +150,7 @@ Rectangle {
}
WizardMenuItem {
+ id: restoreWalletButton
headerText: qsTr("Restore wallet from keys or mnemonic seed") + translationManager.emptyString
bodyText: qsTr("Enter your private keys or 25-word mnemonic seed to restore your wallet.") + translationManager.emptyString
imageIcon: "qrc:///images/restore-wallet.png"
diff --git a/wizard/WizardOpenWallet1.qml b/wizard/WizardOpenWallet1.qml
index 991a72d..8a45523 100644
--- a/wizard/WizardOpenWallet1.qml
+++ b/wizard/WizardOpenWallet1.qml
@@ -47,6 +47,7 @@ Rectangle {
color: "transparent"
property alias pageHeight: pageRoot.height
property alias pageRoot: pageRoot
+ property alias recentWallets: recentList
property string viewName: "wizardOpenWallet1"
property int walletCount: walletKeysFilesModel.rowCount()
@@ -158,6 +159,7 @@ Rectangle {
// index, fileName, modified, accessed, path, networktype, address
id: item
height: flow.itemHeight
+ property string walletFileName: fileName
width: {
if(wizardController.layoutScale <= 1)
return parent.width / 2
diff --git a/wizard/WizardRestoreWallet1.qml b/wizard/WizardRestoreWallet1.qml
index 72b1247..2ee70bd 100644
--- a/wizard/WizardRestoreWallet1.qml
+++ b/wizard/WizardRestoreWallet1.qml
@@ -39,6 +39,10 @@ Rectangle {
color: "transparent"
property alias pageHeight: pageRoot.height
+ property alias walletInput: wizardWalletInput
+ property alias seedInput: seedInput
+ property alias restoreHeight: restoreHeight
+ property alias wizardNav: nav
property string viewName: "wizardRestoreWallet1"
function verify() {
diff --git a/wizard/WizardRestoreWallet2.qml b/wizard/WizardRestoreWallet2.qml
index f66950d..eb57f8d 100644
--- a/wizard/WizardRestoreWallet2.qml
+++ b/wizard/WizardRestoreWallet2.qml
@@ -43,6 +43,7 @@ Rectangle {
property int recoveryMode: 1
property alias pwField: passwordFields.password
property alias pwConfirmField: passwordFields.passwordConfirm
+ property alias wizardNav: wizardNav
ColumnLayout {
@@ -66,6 +67,7 @@ Rectangle {
}
WizardNav {
+ id: wizardNav
progressSteps: appWindow.walletMode <= 1 ? 3 : 4
progress: 1
btnNext.enabled: passwordFields.calcStrengthAndVerify();
diff --git a/wizard/WizardRestoreWallet3.qml b/wizard/WizardRestoreWallet3.qml
index fe433a4..31fb9ab 100644
--- a/wizard/WizardRestoreWallet3.qml
+++ b/wizard/WizardRestoreWallet3.qml
@@ -39,6 +39,7 @@ Rectangle {
color: "transparent"
property alias pageHeight: pageRoot.height
+ property alias wizardNav: wizardNav
property string viewName: "wizardRestoreWallet3"
property int recoveryMode: 1
@@ -68,6 +69,7 @@ Rectangle {
}
WizardNav {
+ id: wizardNav
progressSteps: 4
progress: 2
onPrevClicked: {
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.