diff --git a/src/BlockchainBackend.cpp b/src/BlockchainBackend.cpp index 8a9b065..1db39b2 100644 --- a/src/BlockchainBackend.cpp +++ b/src/BlockchainBackend.cpp @@ -88,27 +88,42 @@ BlockchainBackend::BlockchainBackend(LogosAPI* logosAPI, QObject* parent) return; } + // NOTE: do NOT call requestObject() here. ui-host invokes initLogos() + // (and therefore this constructor) synchronously via Qt::DirectConnection + // and only signals "READY" once it returns. requestObject() blocks for up + // to its 20s timeout when the backend module isn't running yet — which is + // the normal case, since the node is started later from this UI. Blocking + // here makes ui-host miss its readiness deadline, so the host kills it and + // the whole view fails to load. The newBlock subscription is only + // meaningful once the node is running, so it is deferred to + // subscribeToBlockEvents(), called after a successful startBlockchain(). + qDebug() << "BlockchainBackend: initialized"; +} + +void BlockchainBackend::subscribeToBlockEvents() +{ + if (m_blockEventsSubscribed || !m_blockchainClient) + return; + LogosObject* replica = m_blockchainClient->requestObject(BLOCKCHAIN_MODULE_NAME); - if (replica) { - m_blockchainClient->onEvent( - replica, "newBlock", - [this](const QString&, const QVariantList& data) { - const QString timestamp = - QDateTime::currentDateTime().toString("HH:mm:ss"); - QString line; - if (!data.isEmpty()) - line = QString("[%1] New block: %2") - .arg(timestamp, data.first().toString()); - else - line = QString("[%1] New block (no data)").arg(timestamp); - m_logModel->append(line); - }); - } else { - setStatus(ErrorSubscribeFailed); - } + if (!replica) + return; - qDebug() << "BlockchainBackend: initialized"; + m_blockchainClient->onEvent( + replica, "newBlock", + [this](const QString&, const QVariantList& data) { + const QString timestamp = + QDateTime::currentDateTime().toString("HH:mm:ss"); + QString line; + if (!data.isEmpty()) + line = QString("[%1] New block: %2") + .arg(timestamp, data.first().toString()); + else + line = QString("[%1] New block (no data)").arg(timestamp); + m_logModel->append(line); + }); + m_blockEventsSubscribed = true; } BlockchainBackend::~BlockchainBackend() @@ -132,6 +147,7 @@ void BlockchainBackend::startBlockchain() if (resultCode == 0 || resultCode == 1) { setStatus(Running); + subscribeToBlockEvents(); QTimer::singleShot(500, this, [this]() { refreshAccounts(); }); } else if (resultCode == 2) { setStatus(ErrorConfigMissing); diff --git a/src/BlockchainBackend.h b/src/BlockchainBackend.h index ee58990..1460216 100644 --- a/src/BlockchainBackend.h +++ b/src/BlockchainBackend.h @@ -52,11 +52,17 @@ public slots: private: void fetchBalancesForAccounts(const QStringList& list); + // Subscribes to the backend "newBlock" event. Deferred out of the + // constructor because requestObject() blocks until the backend module is + // running; calling it during the synchronous initLogos() would stall + // ui-host past its readiness deadline and the view would fail to load. + void subscribeToBlockEvents(); LogosAPI* m_logosAPI = nullptr; LogosAPIClient* m_blockchainClient = nullptr; AccountsModel* m_accountsModel = nullptr; LogModel* m_logModel = nullptr; + bool m_blockEventsSubscribed = false; static const QString BLOCKCHAIN_MODULE_NAME; }; diff --git a/tests/ui-tests.mjs b/tests/ui-tests.mjs new file mode 100644 index 0000000..bcb0270 --- /dev/null +++ b/tests/ui-tests.mjs @@ -0,0 +1,35 @@ +import { resolve } from "node:path"; + +// CI sets LOGOS_QT_MCP automatically; for interactive use: +// nix build .#test-framework -o result-mcp +const root = + process.env.LOGOS_QT_MCP || + new URL("../result-mcp", import.meta.url).pathname; +const { test, run } = await import(resolve(root, "test-framework/framework.mjs")); + +// Smoke test: the blockchain UI module must load in the host +// (logos-standalone-app / logos-basecamp), connect to its process-isolated +// C++ backend over Qt Remote Objects, and render the QML view — even when the +// backend node module is not running yet (the node is started from this UI). +test("blockchain_ui: backend connects and config view renders", async (app) => { + await app.waitFor( + async () => { + // Once the BlockchainBackend replica is Valid, the loading state is + // replaced by the ConfigChoiceView. This static label proves the QML + // (including the Logos.Theme / Logos.Controls design-system imports) + // loaded and the backend replica connected. + await app.expectTexts(["Choose how to set up your node config"]); + }, + { + timeout: 30000, + interval: 1000, + description: "blockchain UI to load and backend to connect", + } + ); +}); + +test("blockchain_ui: config setup actions are visible", async (app) => { + await app.expectTexts(["Generate config", "Set path to config"]); +}); + +run();