qml: defer ReactiveListModel/ReactiveObject initial fetch to componentComplete()
setBaseUrl() and setSource() used to fire refresh() inline as soon as both `baseUrl` and `source` were populated — but setToken() never triggered a refresh. QML evaluates literal property assignments before bindings to other objects' properties, so a model declared with literal `source` plus bindings to `BackendConnection.url` / `BackendConnection.token` (the exact shape of make:bridge:window's output) could fire its GET *before* the `token` binding had landed. The unauthenticated request hit Symfony's SessionAuthenticator, came back 401, and the model parked at `ready === false` with an empty list. Mercure subscribed anonymously (the model explicitly clears the SSE client's bearer), so subsequent server-side mutations propagated fine — masking the initial-fetch failure as "list is empty until something changes". Hit by the second window in examples/todo. Both classes now implement QQmlParserStatus and trigger the initial refresh from componentComplete(), where every binding (literal *and* singleton-derived) is guaranteed to have landed. After completion, individual setter changes still trigger refresh inline — so token rotation / URL reassignment after first load behave unchanged. Regression test under framework/qml/tests/tst_reactive_list_model.qml using the v0.2.0 qmltestrunner harness. Adds a TestHttpServer helper that mimics SessionAuthenticator's 401-on-no-bearer behaviour so the regression is observable; verified the test fails against the unfixed production code (`Actual: ""` vs `Expected: "Bearer testtoken"` on the captured Authorization header). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
76
framework/qml/tests/TestHttpServer.h
Normal file
76
framework/qml/tests/TestHttpServer.h
Normal file
@@ -0,0 +1,76 @@
|
||||
// Tiny localhost HTTP server for qmltest fixtures. Listens on a free
|
||||
// ephemeral port; for any incoming request, captures the request line +
|
||||
// headers and replies with a fixed JSON body. Exposed to QML as the
|
||||
// `TestHttpServer` element so tests can instantiate one inline:
|
||||
//
|
||||
// TestHttpServer {
|
||||
// id: srv
|
||||
// responseBody: '[{"id":"1","title":"a","done":false}]'
|
||||
// }
|
||||
// ReactiveListModel { baseUrl: srv.url; ... }
|
||||
// compare(srv.lastAuthHeader, "Bearer testtoken")
|
||||
//
|
||||
// Just enough HTTP to serve a single line-of-sight request — no
|
||||
// chunked encoding, no keepalive, no Content-Length parsing on the
|
||||
// way in. The framework's network paths only ever issue GET /…
|
||||
// against this stub during the test, so that's all we need.
|
||||
//
|
||||
// `apiGetCount` counts only requests under `/api/…` so tests can
|
||||
// distinguish the model's HTTP fetches from Mercure's SSE reconnect
|
||||
// attempts (which hit `/.well-known/mercure`).
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QTcpServer>
|
||||
#include <QtQmlIntegration>
|
||||
|
||||
namespace PhpQml::Bridge::Tests {
|
||||
|
||||
class TestHttpServer : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(int port READ port CONSTANT)
|
||||
Q_PROPERTY(QString url READ url CONSTANT)
|
||||
Q_PROPERTY(QString responseBody READ responseBody WRITE setResponseBody NOTIFY responseBodyChanged)
|
||||
Q_PROPERTY(int responseStatus READ responseStatus WRITE setResponseStatus NOTIFY responseStatusChanged)
|
||||
Q_PROPERTY(int apiGetCount READ apiGetCount NOTIFY apiGetCountChanged)
|
||||
Q_PROPERTY(QString lastAuthHeader READ lastAuthHeader NOTIFY lastAuthHeaderChanged)
|
||||
Q_PROPERTY(QString lastRequestLine READ lastRequestLine NOTIFY lastRequestLineChanged)
|
||||
|
||||
public:
|
||||
explicit TestHttpServer(QObject* parent = nullptr);
|
||||
|
||||
int port() const { return m_server.serverPort(); }
|
||||
QString url() const;
|
||||
QString responseBody() const { return m_responseBody; }
|
||||
int responseStatus() const { return m_responseStatus; }
|
||||
int apiGetCount() const { return m_apiGetCount; }
|
||||
QString lastAuthHeader() const { return m_lastAuthHeader; }
|
||||
QString lastRequestLine() const { return m_lastRequestLine; }
|
||||
|
||||
void setResponseBody(const QString& v);
|
||||
void setResponseStatus(int v);
|
||||
|
||||
signals:
|
||||
void responseBodyChanged();
|
||||
void responseStatusChanged();
|
||||
void apiGetCountChanged();
|
||||
void lastAuthHeaderChanged();
|
||||
void lastRequestLineChanged();
|
||||
|
||||
private slots:
|
||||
void onNewConnection();
|
||||
|
||||
private:
|
||||
QTcpServer m_server;
|
||||
QString m_responseBody = QStringLiteral("[]");
|
||||
int m_responseStatus = 200;
|
||||
int m_apiGetCount = 0;
|
||||
QString m_lastAuthHeader;
|
||||
QString m_lastRequestLine;
|
||||
};
|
||||
|
||||
} // namespace PhpQml::Bridge::Tests
|
||||
Reference in New Issue
Block a user