@dusted/anqst 1.7.1 → 1.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AnQstWebBase/AnQstWebBaseAbi.cmake +1 -0
- package/AnQstWebBase/CMakeLists.txt +116 -0
- package/AnQstWebBase/CMakeUserPresets.json +14 -0
- package/AnQstWebBase/README.md +65 -0
- package/AnQstWebBase/src/AnQstBase93.cpp +91 -0
- package/AnQstWebBase/src/AnQstBase93.h +15 -0
- package/AnQstWebBase/src/AnQstBridgeProxy.cpp +30 -0
- package/AnQstWebBase/src/AnQstBridgeProxy.h +41 -0
- package/AnQstWebBase/src/AnQstHostBridgeFacade.cpp +345 -0
- package/AnQstWebBase/src/AnQstHostBridgeFacade.h +99 -0
- package/AnQstWebBase/src/AnQstWebBaseAbi.h +4 -0
- package/AnQstWebBase/src/AnQstWebBaseAbi.h.in +4 -0
- package/AnQstWebBase/src/AnQstWebHostBase.cpp +1822 -0
- package/AnQstWebBase/src/AnQstWebHostBase.h +227 -0
- package/AnQstWebBase/src/AnQstWidgetDebugDialog.cpp +425 -0
- package/AnQstWebBase/src/AnQstWidgetDebugDialog.h +105 -0
- package/AnQstWebBase/src/AngularHttpBaseServer.cpp +965 -0
- package/AnQstWebBase/src/AngularHttpBaseServer.h +97 -0
- package/AnQstWebBase/src/UI/AnQstWidgetDebugDialog.ui +235 -0
- package/AnQstWebBase/tests/CMakeLists.txt +22 -0
- package/AnQstWebBase/tests/test_AnQstWebHostBase.cpp +1102 -0
- package/dist/src/abi-hash-stamp.js +5 -0
- package/dist/src/abi-hash.js +33 -0
- package/dist/src/app.js +67 -19
- package/dist/src/boundary-codec-render.js +17 -10
- package/dist/src/emit.js +76 -30
- package/dist/src/project.js +11 -1
- package/dist/src/webbase.js +94 -0
- package/package.json +2 -1
|
@@ -0,0 +1,1102 @@
|
|
|
1
|
+
#include "AnQstWebHostBase.h"
|
|
2
|
+
#include "AnQstBase93.h"
|
|
3
|
+
#include "AnQstHostBridgeFacade.h"
|
|
4
|
+
#include "AngularHttpBaseServer.h"
|
|
5
|
+
#include "AnQstWidgetDebugDialog.h"
|
|
6
|
+
|
|
7
|
+
#include <QApplication>
|
|
8
|
+
#include <QCheckBox>
|
|
9
|
+
#include <QComboBox>
|
|
10
|
+
#include <QCoreApplication>
|
|
11
|
+
#include <QDragEnterEvent>
|
|
12
|
+
#include <QDialog>
|
|
13
|
+
#include <QDialogButtonBox>
|
|
14
|
+
#include <QDir>
|
|
15
|
+
#include <QDropEvent>
|
|
16
|
+
#include <QElapsedTimer>
|
|
17
|
+
#include <QFile>
|
|
18
|
+
#include <QKeyEvent>
|
|
19
|
+
#include <QLabel>
|
|
20
|
+
#include <QLineEdit>
|
|
21
|
+
#include <QMimeData>
|
|
22
|
+
#include <QPlainTextEdit>
|
|
23
|
+
#include <QPushButton>
|
|
24
|
+
#include <QSignalSpy>
|
|
25
|
+
#include <QShortcut>
|
|
26
|
+
#include <QTabWidget>
|
|
27
|
+
#include <QTcpServer>
|
|
28
|
+
#include <QTcpSocket>
|
|
29
|
+
#include <QTemporaryDir>
|
|
30
|
+
#include <QTextStream>
|
|
31
|
+
#include <QTimer>
|
|
32
|
+
#include <QWebEngineScriptCollection>
|
|
33
|
+
#include <QWebEngineView>
|
|
34
|
+
#include <cstdlib>
|
|
35
|
+
#include <stdexcept>
|
|
36
|
+
#include <vector>
|
|
37
|
+
|
|
38
|
+
#if __has_include(<catch2/catch_session.hpp>) && __has_include(<catch2/catch_test_macros.hpp>)
|
|
39
|
+
#include <catch2/catch_session.hpp>
|
|
40
|
+
#include <catch2/catch_test_macros.hpp>
|
|
41
|
+
#elif __has_include(<catch2/catch.hpp>)
|
|
42
|
+
#define CATCH_CONFIG_RUNNER
|
|
43
|
+
#include <catch2/catch.hpp>
|
|
44
|
+
#else
|
|
45
|
+
#error "Catch2 headers are not available."
|
|
46
|
+
#endif
|
|
47
|
+
|
|
48
|
+
using namespace ANQST_WEBBASE_NAMESPACE;
|
|
49
|
+
|
|
50
|
+
namespace {
|
|
51
|
+
|
|
52
|
+
class DummyBridge final : public QObject {
|
|
53
|
+
Q_OBJECT
|
|
54
|
+
public:
|
|
55
|
+
explicit DummyBridge(QObject* parent = nullptr)
|
|
56
|
+
: QObject(parent) {}
|
|
57
|
+
|
|
58
|
+
public slots:
|
|
59
|
+
QString ping() const { return QStringLiteral("pong"); }
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
QApplication& ensureApp() {
|
|
63
|
+
static int argc = 1;
|
|
64
|
+
static char appName[] = "anqstwebbase_tests";
|
|
65
|
+
static char* argv[] = { appName, nullptr };
|
|
66
|
+
static QApplication app(argc, argv);
|
|
67
|
+
return app;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
bool waitForSignal(QSignalSpy& spy, int timeoutMs = 2000) {
|
|
71
|
+
if (spy.count() > 0) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
return spy.wait(timeoutMs);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
QVariantMap firstPayload(QSignalSpy& spy) {
|
|
78
|
+
REQUIRE(spy.count() > 0);
|
|
79
|
+
const auto args = spy.takeFirst();
|
|
80
|
+
REQUIRE(args.count() == 1);
|
|
81
|
+
REQUIRE(args.at(0).canConvert<QVariantMap>());
|
|
82
|
+
return args.at(0).toMap();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
QByteArray readSocketUntil(QTcpSocket& socket, const QByteArray& needle, int timeoutMs = 3000) {
|
|
86
|
+
QByteArray data;
|
|
87
|
+
QElapsedTimer timer;
|
|
88
|
+
timer.start();
|
|
89
|
+
while (timer.elapsed() < timeoutMs) {
|
|
90
|
+
QCoreApplication::processEvents(QEventLoop::AllEvents, 50);
|
|
91
|
+
socket.waitForReadyRead(50);
|
|
92
|
+
data += socket.readAll();
|
|
93
|
+
if (data.contains(needle)) {
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return data;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
bool spyHasWebEngineError(
|
|
101
|
+
const QSignalSpy& spy,
|
|
102
|
+
const QString& channel,
|
|
103
|
+
const QStringList& detailFragments = QStringList(),
|
|
104
|
+
QString* matchedDetail = nullptr) {
|
|
105
|
+
for (int index = 0; index < spy.count(); ++index) {
|
|
106
|
+
const auto args = spy.at(index);
|
|
107
|
+
if (args.count() != 2) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (args.at(0).toString() != channel) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const QString detail = args.at(1).toString();
|
|
114
|
+
bool matches = true;
|
|
115
|
+
for (const QString& fragment : detailFragments) {
|
|
116
|
+
if (!detail.contains(fragment)) {
|
|
117
|
+
matches = false;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (!matches) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (matchedDetail != nullptr) {
|
|
125
|
+
*matchedDetail = detail;
|
|
126
|
+
}
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
bool waitForWebEngineError(
|
|
133
|
+
QSignalSpy& spy,
|
|
134
|
+
const QString& channel,
|
|
135
|
+
const QStringList& detailFragments = QStringList(),
|
|
136
|
+
int timeoutMs = 4000,
|
|
137
|
+
QString* matchedDetail = nullptr) {
|
|
138
|
+
if (spyHasWebEngineError(spy, channel, detailFragments, matchedDetail)) {
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
QElapsedTimer timer;
|
|
142
|
+
timer.start();
|
|
143
|
+
while (timer.elapsed() < timeoutMs) {
|
|
144
|
+
const int remainingMs = timeoutMs - static_cast<int>(timer.elapsed());
|
|
145
|
+
if (remainingMs <= 0) {
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
spy.wait(qMin(remainingMs, 100));
|
|
149
|
+
if (spyHasWebEngineError(spy, channel, detailFragments, matchedDetail)) {
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return spyHasWebEngineError(spy, channel, detailFragments, matchedDetail);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
void writeHtmlFile(const QString& filePath, const QString& html) {
|
|
157
|
+
QFile file(filePath);
|
|
158
|
+
REQUIRE(file.open(QIODevice::WriteOnly | QIODevice::Truncate));
|
|
159
|
+
QTextStream stream(&file);
|
|
160
|
+
stream << html;
|
|
161
|
+
file.close();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
void scheduleDebugDialogInteraction(
|
|
165
|
+
int hostIndex,
|
|
166
|
+
int providerIndex,
|
|
167
|
+
const QString& directoryValue,
|
|
168
|
+
const QString& urlValue,
|
|
169
|
+
bool openBrowserChecked,
|
|
170
|
+
bool acceptDialog) {
|
|
171
|
+
QTimer::singleShot(0, qApp, [=]() {
|
|
172
|
+
auto* dialog = qobject_cast<QDialog*>(QApplication::activeModalWidget());
|
|
173
|
+
REQUIRE(dialog != nullptr);
|
|
174
|
+
|
|
175
|
+
auto* hostCombo = dialog->findChild<QComboBox*>(QStringLiteral("cbAnQstAngularAppHost"));
|
|
176
|
+
auto* providerCombo = dialog->findChild<QComboBox*>(QStringLiteral("cbWidgetResource"));
|
|
177
|
+
auto* directoryEdit = dialog->findChild<QLineEdit*>(QStringLiteral("leDirectory"));
|
|
178
|
+
auto* urlEdit = dialog->findChild<QLineEdit*>(QStringLiteral("leURL"));
|
|
179
|
+
auto* openBrowser = dialog->findChild<QCheckBox*>(QStringLiteral("rbOpenBrowser"));
|
|
180
|
+
auto* buttonBox = dialog->findChild<QDialogButtonBox*>(QStringLiteral("buttonBox"));
|
|
181
|
+
REQUIRE(hostCombo != nullptr);
|
|
182
|
+
REQUIRE(providerCombo != nullptr);
|
|
183
|
+
REQUIRE(directoryEdit != nullptr);
|
|
184
|
+
REQUIRE(urlEdit != nullptr);
|
|
185
|
+
REQUIRE(openBrowser != nullptr);
|
|
186
|
+
REQUIRE(buttonBox != nullptr);
|
|
187
|
+
|
|
188
|
+
hostCombo->setCurrentIndex(hostIndex);
|
|
189
|
+
providerCombo->setCurrentIndex(providerIndex);
|
|
190
|
+
directoryEdit->setText(directoryValue);
|
|
191
|
+
urlEdit->setText(urlValue);
|
|
192
|
+
openBrowser->setChecked(openBrowserChecked);
|
|
193
|
+
|
|
194
|
+
if (!acceptDialog) {
|
|
195
|
+
dialog->reject();
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
auto* okButton = buttonBox->button(QDialogButtonBox::Ok);
|
|
200
|
+
REQUIRE(okButton != nullptr);
|
|
201
|
+
QElapsedTimer timer;
|
|
202
|
+
timer.start();
|
|
203
|
+
while (!okButton->isEnabled() && timer.elapsed() < 4000) {
|
|
204
|
+
QCoreApplication::processEvents(QEventLoop::AllEvents, 30);
|
|
205
|
+
}
|
|
206
|
+
REQUIRE(okButton->isEnabled());
|
|
207
|
+
okButton->click();
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
} // namespace
|
|
212
|
+
|
|
213
|
+
TEST_CASE("shared AnQst base93 runtime preserves canonical vectors and round-trips", "[base93][runtime]") {
|
|
214
|
+
const std::vector<std::uint8_t> empty{};
|
|
215
|
+
CHECK(anqstBase93Encode(empty) == QString());
|
|
216
|
+
CHECK(anqstBase93Decode(QString()) == empty);
|
|
217
|
+
|
|
218
|
+
const std::vector<std::uint8_t> one{1u};
|
|
219
|
+
CHECK(anqstBase93Encode(one) == QStringLiteral(" !"));
|
|
220
|
+
CHECK(anqstBase93Decode(QStringLiteral(" !")) == one);
|
|
221
|
+
|
|
222
|
+
const std::vector<std::uint8_t> maxWord{255u, 255u, 255u, 255u};
|
|
223
|
+
CHECK(anqstBase93Encode(maxWord) == QStringLiteral("ZG[H$"));
|
|
224
|
+
CHECK(anqstBase93Decode(QStringLiteral("ZG[H$")) == maxWord);
|
|
225
|
+
|
|
226
|
+
std::vector<std::uint8_t> allBytes(256u);
|
|
227
|
+
for (std::size_t i = 0; i < allBytes.size(); ++i) {
|
|
228
|
+
allBytes[i] = static_cast<std::uint8_t>(i);
|
|
229
|
+
}
|
|
230
|
+
const QString encoded = anqstBase93Encode(allBytes);
|
|
231
|
+
CHECK(encoded.contains(QLatin1Char('"')) == false);
|
|
232
|
+
CHECK(encoded.contains(QLatin1Char('\\')) == false);
|
|
233
|
+
CHECK(anqstBase93Decode(encoded) == allBytes);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
TEST_CASE("setContentRoot is single-assignment and emits structured warning", "[host][lifecycle]") {
|
|
237
|
+
ensureApp();
|
|
238
|
+
AnQstWebHostBase host;
|
|
239
|
+
QSignalSpy errorSpy(&host, &AnQstWebHostBase::onHostError);
|
|
240
|
+
|
|
241
|
+
QTemporaryDir dir;
|
|
242
|
+
REQUIRE(dir.isValid());
|
|
243
|
+
|
|
244
|
+
REQUIRE(host.setContentRoot(dir.path()));
|
|
245
|
+
REQUIRE_FALSE(host.setContentRoot(dir.path()));
|
|
246
|
+
REQUIRE(waitForSignal(errorSpy));
|
|
247
|
+
|
|
248
|
+
const QVariantMap payload = firstPayload(errorSpy);
|
|
249
|
+
CHECK(payload.value("code").toString() == "HOST_CONTENT_ROOT_RECALL_IGNORED");
|
|
250
|
+
CHECK(payload.value("category").toString() == "lifecycle");
|
|
251
|
+
CHECK(payload.value("severity").toString() == "warn");
|
|
252
|
+
CHECK(payload.value("recoverable").toBool());
|
|
253
|
+
CHECK(payload.contains("message"));
|
|
254
|
+
CHECK(payload.contains("context"));
|
|
255
|
+
CHECK(payload.contains("timestamp"));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
TEST_CASE("setBridgeObject is single-assignment and emits structured warning", "[host][bridge]") {
|
|
259
|
+
ensureApp();
|
|
260
|
+
AnQstWebHostBase host;
|
|
261
|
+
QSignalSpy errorSpy(&host, &AnQstWebHostBase::onHostError);
|
|
262
|
+
|
|
263
|
+
DummyBridge bridge1;
|
|
264
|
+
DummyBridge bridge2;
|
|
265
|
+
|
|
266
|
+
REQUIRE(host.setBridgeObject(&bridge1));
|
|
267
|
+
REQUIRE_FALSE(host.setBridgeObject(&bridge2));
|
|
268
|
+
REQUIRE(waitForSignal(errorSpy));
|
|
269
|
+
|
|
270
|
+
const QVariantMap payload = firstPayload(errorSpy);
|
|
271
|
+
CHECK(payload.value("code").toString() == "HOST_BRIDGE_RECALL_IGNORED");
|
|
272
|
+
CHECK(payload.value("category").toString() == "lifecycle");
|
|
273
|
+
CHECK(payload.value("severity").toString() == "warn");
|
|
274
|
+
CHECK(payload.value("recoverable").toBool());
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
TEST_CASE("bridge bootstrap script can be reinstalled explicitly", "[host][bridge][bootstrap]") {
|
|
278
|
+
ensureApp();
|
|
279
|
+
AnQstWebHostBase host;
|
|
280
|
+
QSignalSpy errorSpy(&host, &AnQstWebHostBase::onHostError);
|
|
281
|
+
|
|
282
|
+
REQUIRE(host.installBridgeBootstrapScript(QString(), true));
|
|
283
|
+
CHECK(errorSpy.count() == 0);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
TEST_CASE("text selection and scrollbar policies default to disabled", "[host][view][defaults]") {
|
|
287
|
+
ensureApp();
|
|
288
|
+
AnQstWebHostBase host;
|
|
289
|
+
auto* view = host.findChild<QWebEngineView*>();
|
|
290
|
+
REQUIRE(view != nullptr);
|
|
291
|
+
|
|
292
|
+
static const QString kNoSelectScript = QStringLiteral("AnQstDisableTextSelection");
|
|
293
|
+
static const QString kScriptName = QStringLiteral("AnQstDisableScrollbars");
|
|
294
|
+
auto& scripts = view->page()->scripts();
|
|
295
|
+
CHECK_FALSE(scripts.findScript(kNoSelectScript).isNull());
|
|
296
|
+
CHECK_FALSE(scripts.findScript(kScriptName).isNull());
|
|
297
|
+
|
|
298
|
+
host.setTextSelectionEnabled(true);
|
|
299
|
+
CHECK(scripts.findScript(kNoSelectScript).isNull());
|
|
300
|
+
host.setTextSelectionEnabled(false);
|
|
301
|
+
CHECK_FALSE(scripts.findScript(kNoSelectScript).isNull());
|
|
302
|
+
|
|
303
|
+
host.setScrollbarsEnabled(true);
|
|
304
|
+
CHECK(scripts.findScript(kScriptName).isNull());
|
|
305
|
+
host.setScrollbarsEnabled(false);
|
|
306
|
+
CHECK_FALSE(scripts.findScript(kScriptName).isNull());
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
TEST_CASE("bridge bootstrap script failure emits structured error", "[host][bridge][bootstrap]") {
|
|
310
|
+
ensureApp();
|
|
311
|
+
AnQstWebHostBase host;
|
|
312
|
+
QSignalSpy errorSpy(&host, &AnQstWebHostBase::onHostError);
|
|
313
|
+
|
|
314
|
+
REQUIRE_FALSE(host.installBridgeBootstrapScript(QStringLiteral(" "), true));
|
|
315
|
+
REQUIRE(waitForSignal(errorSpy));
|
|
316
|
+
|
|
317
|
+
const QVariantMap payload = firstPayload(errorSpy);
|
|
318
|
+
CHECK(payload.value("code").toString() == "HOST_BRIDGE_BOOTSTRAP_UNAVAILABLE");
|
|
319
|
+
CHECK(payload.value("category").toString() == "bridge");
|
|
320
|
+
CHECK(payload.value("severity").toString() == "error");
|
|
321
|
+
CHECK(payload.value("recoverable").toBool() == false);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
TEST_CASE("bridge Call handler is invoked", "[host][behavior][call]") {
|
|
325
|
+
ensureApp();
|
|
326
|
+
AnQstWebHostBase host;
|
|
327
|
+
|
|
328
|
+
host.setCallHandler([](const QString& service, const QString& member, const QVariantList& args) -> QVariant {
|
|
329
|
+
return QStringLiteral("%1:%2:%3").arg(service, member, args.isEmpty() ? QStringLiteral("none") : args.at(0).toString());
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const QVariant callResult = host.anQstBridge_call(QStringLiteral("DemoBehaviorService"), QStringLiteral("callGreeting"), {QStringLiteral("Alice")});
|
|
333
|
+
CHECK(callResult.toString() == QStringLiteral("DemoBehaviorService:callGreeting:Alice"));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
TEST_CASE("hover targets preserve tagged drag-drop payload text after validating array carriers", "[host][dragdrop][hover]") {
|
|
337
|
+
ensureApp();
|
|
338
|
+
AnQstWebHostBase host;
|
|
339
|
+
auto* facade = host.findChild<AnQstHostBridgeFacade*>();
|
|
340
|
+
REQUIRE(facade != nullptr);
|
|
341
|
+
facade->setDispatchEnabled(true);
|
|
342
|
+
host.registerHoverTarget(
|
|
343
|
+
QStringLiteral("DemoBehaviorService"),
|
|
344
|
+
QStringLiteral("hoveringDraft"),
|
|
345
|
+
QStringLiteral("application/anqst-test-hover"),
|
|
346
|
+
0);
|
|
347
|
+
|
|
348
|
+
QSignalSpy hoverSpy(&host, &AnQstWebHostBase::anQstBridge_hoverUpdated);
|
|
349
|
+
QSignalSpy errorSpy(&host, &AnQstWebHostBase::onHostError);
|
|
350
|
+
|
|
351
|
+
QMimeData mime;
|
|
352
|
+
mime.setData(QStringLiteral("application/anqst-test-hover"), QByteArrayLiteral("A[\"draft-wire\"]"));
|
|
353
|
+
QDragEnterEvent enterEvent(QPoint(11, 13), Qt::CopyAction, &mime, Qt::LeftButton, Qt::NoModifier);
|
|
354
|
+
|
|
355
|
+
REQUIRE(host.eventFilter(&host, &enterEvent));
|
|
356
|
+
REQUIRE(waitForSignal(hoverSpy));
|
|
357
|
+
CHECK(errorSpy.count() == 0);
|
|
358
|
+
|
|
359
|
+
const auto args = hoverSpy.takeFirst();
|
|
360
|
+
REQUIRE(args.count() == 5);
|
|
361
|
+
CHECK(args.at(0).toString() == QStringLiteral("DemoBehaviorService"));
|
|
362
|
+
CHECK(args.at(1).toString() == QStringLiteral("hoveringDraft"));
|
|
363
|
+
CHECK(args.at(2).toString() == QStringLiteral("A[\"draft-wire\"]"));
|
|
364
|
+
CHECK(args.at(3).toDouble() == 11.0);
|
|
365
|
+
CHECK(args.at(4).toDouble() == 13.0);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
TEST_CASE("drop targets reject legacy object MIME payloads with diagnostics", "[host][dragdrop][drop]") {
|
|
369
|
+
ensureApp();
|
|
370
|
+
AnQstWebHostBase host;
|
|
371
|
+
host.registerDropTarget(
|
|
372
|
+
QStringLiteral("DemoBehaviorService"),
|
|
373
|
+
QStringLiteral("droppedDraft"),
|
|
374
|
+
QStringLiteral("application/anqst-test-drop"));
|
|
375
|
+
|
|
376
|
+
QSignalSpy dropSpy(&host, &AnQstWebHostBase::anQstBridge_dropReceived);
|
|
377
|
+
QSignalSpy errorSpy(&host, &AnQstWebHostBase::onHostError);
|
|
378
|
+
|
|
379
|
+
QMimeData mime;
|
|
380
|
+
mime.setData(QStringLiteral("application/anqst-test-drop"), QByteArrayLiteral("{\"legacy\":true}"));
|
|
381
|
+
QDropEvent dropEvent(QPointF(5.0, 7.0), Qt::CopyAction, &mime, Qt::LeftButton, Qt::NoModifier);
|
|
382
|
+
|
|
383
|
+
REQUIRE(host.eventFilter(&host, &dropEvent));
|
|
384
|
+
CHECK(dropSpy.count() == 0);
|
|
385
|
+
REQUIRE(waitForSignal(errorSpy));
|
|
386
|
+
|
|
387
|
+
const QVariantMap payload = firstPayload(errorSpy);
|
|
388
|
+
CHECK(payload.value("code").toString() == QStringLiteral("HOST_DRAGDROP_PAYLOAD_INVALID"));
|
|
389
|
+
CHECK(payload.value("category").toString() == QStringLiteral("bridge"));
|
|
390
|
+
CHECK(payload.value("severity").toString() == QStringLiteral("error"));
|
|
391
|
+
CHECK(payload.value("recoverable").toBool());
|
|
392
|
+
CHECK(payload.value("message").toString().contains(QStringLiteral("unknown transport tag")));
|
|
393
|
+
CHECK(payload.value("context").toMap().value("mimeType").toString() == QStringLiteral("application/anqst-test-drop"));
|
|
394
|
+
CHECK(payload.value("context").toMap().value("transportTag").toString() == QStringLiteral("{"));
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
TEST_CASE("bridge Emitter and Input handlers are forwarded", "[host][behavior][emitter][input]") {
|
|
398
|
+
ensureApp();
|
|
399
|
+
AnQstWebHostBase host;
|
|
400
|
+
|
|
401
|
+
QVariantList capturedEmitterArgs;
|
|
402
|
+
QVariant capturedInputValue;
|
|
403
|
+
host.setEmitterHandler([&](const QString&, const QString&, const QVariantList& args) {
|
|
404
|
+
capturedEmitterArgs = args;
|
|
405
|
+
});
|
|
406
|
+
host.setInputHandler([&](const QString&, const QString&, const QVariant& value) {
|
|
407
|
+
capturedInputValue = value;
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
host.anQstBridge_emit(QStringLiteral("DemoBehaviorService"), QStringLiteral("emitterTelemetry"), {QStringLiteral("tag"), 7});
|
|
411
|
+
host.anQstBridge_setInput(QStringLiteral("DemoBehaviorService"), QStringLiteral("inputTypedValue"), QStringLiteral("hello"));
|
|
412
|
+
|
|
413
|
+
REQUIRE(capturedEmitterArgs.count() == 2);
|
|
414
|
+
CHECK(capturedEmitterArgs.at(0).toString() == QStringLiteral("tag"));
|
|
415
|
+
CHECK(capturedEmitterArgs.at(1).toInt() == 7);
|
|
416
|
+
CHECK(capturedInputValue.toString() == QStringLiteral("hello"));
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
TEST_CASE("facade emitter handler failures emit diagnostics without throwing", "[host][facade][emitter][diagnostics]") {
|
|
420
|
+
ensureApp();
|
|
421
|
+
AnQstHostBridgeFacade facade;
|
|
422
|
+
QSignalSpy diagnosticSpy(&facade, &AnQstHostBridgeFacade::bridgeHostError);
|
|
423
|
+
|
|
424
|
+
facade.setEmitterHandler([](const QString&, const QString&, const QVariantList&) {
|
|
425
|
+
throw std::runtime_error("planned emitter failure");
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
REQUIRE_NOTHROW(facade.emitMessage(QStringLiteral("DemoBehaviorService"), QStringLiteral("emitterTelemetry"), {}));
|
|
429
|
+
REQUIRE(waitForSignal(diagnosticSpy));
|
|
430
|
+
|
|
431
|
+
const QVariantMap payload = firstPayload(diagnosticSpy);
|
|
432
|
+
CHECK(payload.value("code").toString() == "EmitterHandlerError");
|
|
433
|
+
CHECK(payload.value("category").toString() == "bridge");
|
|
434
|
+
CHECK(payload.value("severity").toString() == "error");
|
|
435
|
+
CHECK(payload.value("recoverable").toBool());
|
|
436
|
+
CHECK(payload.value("context").toMap().value("service").toString() == QStringLiteral("DemoBehaviorService"));
|
|
437
|
+
CHECK(payload.value("context").toMap().value("member").toString() == QStringLiteral("emitterTelemetry"));
|
|
438
|
+
CHECK(payload.value("context").toMap().value("detail").toString() == QStringLiteral("planned emitter failure"));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
TEST_CASE("facade input handler failures emit diagnostics without throwing", "[host][facade][input][diagnostics]") {
|
|
442
|
+
ensureApp();
|
|
443
|
+
AnQstHostBridgeFacade facade;
|
|
444
|
+
QSignalSpy diagnosticSpy(&facade, &AnQstHostBridgeFacade::bridgeHostError);
|
|
445
|
+
|
|
446
|
+
facade.setInputHandler([](const QString&, const QString&, const QVariant&) {
|
|
447
|
+
throw std::runtime_error("planned input failure");
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
REQUIRE_NOTHROW(facade.setInput(QStringLiteral("DemoBehaviorService"), QStringLiteral("inputTypedValue"), QStringLiteral("hello")));
|
|
451
|
+
REQUIRE(waitForSignal(diagnosticSpy));
|
|
452
|
+
|
|
453
|
+
const QVariantMap payload = firstPayload(diagnosticSpy);
|
|
454
|
+
CHECK(payload.value("code").toString() == "InputHandlerError");
|
|
455
|
+
CHECK(payload.value("category").toString() == "bridge");
|
|
456
|
+
CHECK(payload.value("severity").toString() == "error");
|
|
457
|
+
CHECK(payload.value("recoverable").toBool());
|
|
458
|
+
CHECK(payload.value("context").toMap().value("service").toString() == QStringLiteral("DemoBehaviorService"));
|
|
459
|
+
CHECK(payload.value("context").toMap().value("member").toString() == QStringLiteral("inputTypedValue"));
|
|
460
|
+
CHECK(payload.value("context").toMap().value("detail").toString() == QStringLiteral("planned input failure"));
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
TEST_CASE("Slot queueing dispatches when handler is registered", "[host][behavior][slot]") {
|
|
464
|
+
ensureApp();
|
|
465
|
+
AnQstWebHostBase host;
|
|
466
|
+
DummyBridge bridge;
|
|
467
|
+
host.setSlotInvocationTimeoutMs(2000);
|
|
468
|
+
|
|
469
|
+
QTemporaryDir dir;
|
|
470
|
+
REQUIRE(dir.isValid());
|
|
471
|
+
REQUIRE(host.setContentRoot(dir.path()));
|
|
472
|
+
REQUIRE(host.setBridgeObject(&bridge));
|
|
473
|
+
QMetaObject::invokeMethod(&host, "handleLoadFinished", Q_ARG(bool, true));
|
|
474
|
+
|
|
475
|
+
QObject::connect(&host, &AnQstWebHostBase::anQstBridge_slotInvocationRequested, &host, [&](const QString& requestId, const QString&, const QString&, const QVariantList& args) {
|
|
476
|
+
const QString payload = args.isEmpty() ? QStringLiteral("none") : args.at(0).toString();
|
|
477
|
+
host.anQstBridge_resolveSlot(requestId, true, QStringLiteral("echo:%1").arg(payload), QString());
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
QTimer::singleShot(50, &host, [&]() {
|
|
481
|
+
host.anQstBridge_registerSlot(QStringLiteral("DemoBehaviorService"), QStringLiteral("slotPrompt"));
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
QVariant result;
|
|
485
|
+
QString error;
|
|
486
|
+
const bool ok = host.invokeSlot(QStringLiteral("DemoBehaviorService"), QStringLiteral("slotPrompt"), {QStringLiteral("abc")}, &result, &error);
|
|
487
|
+
CHECK(ok);
|
|
488
|
+
CHECK(error.isEmpty());
|
|
489
|
+
CHECK(result.toString() == QStringLiteral("echo:abc"));
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
TEST_CASE("facade distinguishes slot registration timeout diagnostics", "[host][facade][slot][diagnostics]") {
|
|
493
|
+
ensureApp();
|
|
494
|
+
AnQstHostBridgeFacade facade;
|
|
495
|
+
facade.setSlotInvocationTimeoutMs(20);
|
|
496
|
+
QSignalSpy diagnosticSpy(&facade, &AnQstHostBridgeFacade::bridgeHostError);
|
|
497
|
+
|
|
498
|
+
QVariant result;
|
|
499
|
+
QString error;
|
|
500
|
+
const bool ok = facade.invokeSlot(QStringLiteral("DemoBehaviorService"), QStringLiteral("slotPrompt"), {}, &result, &error);
|
|
501
|
+
|
|
502
|
+
CHECK_FALSE(ok);
|
|
503
|
+
CHECK(error == QStringLiteral("slot invocation timeout"));
|
|
504
|
+
REQUIRE(waitForSignal(diagnosticSpy));
|
|
505
|
+
|
|
506
|
+
const QVariantMap payload = firstPayload(diagnosticSpy);
|
|
507
|
+
CHECK(payload.value("code").toString() == "HandlerNotRegisteredError");
|
|
508
|
+
CHECK(payload.value("category").toString() == "runtime");
|
|
509
|
+
CHECK(payload.value("severity").toString() == "error");
|
|
510
|
+
CHECK(payload.value("recoverable").toBool());
|
|
511
|
+
CHECK(payload.value("context").toMap().value("reason").toString() == QStringLiteral("registration_timeout"));
|
|
512
|
+
CHECK(payload.value("context").toMap().value("slotRegistered").toBool() == false);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
TEST_CASE("facade distinguishes slot reply timeout diagnostics", "[host][facade][slot][diagnostics]") {
|
|
516
|
+
ensureApp();
|
|
517
|
+
AnQstHostBridgeFacade facade;
|
|
518
|
+
facade.setSlotInvocationTimeoutMs(20);
|
|
519
|
+
QSignalSpy diagnosticSpy(&facade, &AnQstHostBridgeFacade::bridgeHostError);
|
|
520
|
+
|
|
521
|
+
facade.registerSlot(QStringLiteral("DemoBehaviorService"), QStringLiteral("slotPrompt"));
|
|
522
|
+
|
|
523
|
+
QVariant result;
|
|
524
|
+
QString error;
|
|
525
|
+
const bool ok = facade.invokeSlot(QStringLiteral("DemoBehaviorService"), QStringLiteral("slotPrompt"), {}, &result, &error);
|
|
526
|
+
|
|
527
|
+
CHECK_FALSE(ok);
|
|
528
|
+
CHECK(error == QStringLiteral("slot invocation timeout"));
|
|
529
|
+
REQUIRE(waitForSignal(diagnosticSpy));
|
|
530
|
+
|
|
531
|
+
const QVariantMap payload = firstPayload(diagnosticSpy);
|
|
532
|
+
CHECK(payload.value("code").toString() == "BridgeTimeoutError");
|
|
533
|
+
CHECK(payload.value("category").toString() == "runtime");
|
|
534
|
+
CHECK(payload.value("severity").toString() == "error");
|
|
535
|
+
CHECK(payload.value("recoverable").toBool());
|
|
536
|
+
CHECK(payload.value("context").toMap().value("reason").toString() == QStringLiteral("reply_timeout"));
|
|
537
|
+
CHECK(payload.value("context").toMap().value("slotRegistered").toBool());
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
TEST_CASE("Output value emits through bridge signal when host is ready", "[host][behavior][output]") {
|
|
541
|
+
ensureApp();
|
|
542
|
+
AnQstWebHostBase host;
|
|
543
|
+
DummyBridge bridge;
|
|
544
|
+
QSignalSpy outputSpy(&host, &AnQstWebHostBase::anQstBridge_outputUpdated);
|
|
545
|
+
|
|
546
|
+
QTemporaryDir dir;
|
|
547
|
+
REQUIRE(dir.isValid());
|
|
548
|
+
REQUIRE(host.setContentRoot(dir.path()));
|
|
549
|
+
REQUIRE(host.setBridgeObject(&bridge));
|
|
550
|
+
QMetaObject::invokeMethod(&host, "handleLoadFinished", Q_ARG(bool, true));
|
|
551
|
+
|
|
552
|
+
host.setOutputValue(QStringLiteral("DemoBehaviorService"), QStringLiteral("outputParentState"), QStringLiteral("state-1"));
|
|
553
|
+
REQUIRE(waitForSignal(outputSpy, 4000));
|
|
554
|
+
|
|
555
|
+
const auto args = outputSpy.takeFirst();
|
|
556
|
+
REQUIRE(args.count() == 3);
|
|
557
|
+
CHECK(args.at(0).toString() == QStringLiteral("DemoBehaviorService"));
|
|
558
|
+
CHECK(args.at(1).toString() == QStringLiteral("outputParentState"));
|
|
559
|
+
CHECK(args.at(2).toString() == QStringLiteral("state-1"));
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
TEST_CASE("Output snapshot replays when ready host finishes loading again", "[host][behavior][output]") {
|
|
563
|
+
ensureApp();
|
|
564
|
+
AnQstWebHostBase host;
|
|
565
|
+
DummyBridge bridge;
|
|
566
|
+
QSignalSpy outputSpy(&host, &AnQstWebHostBase::anQstBridge_outputUpdated);
|
|
567
|
+
|
|
568
|
+
QTemporaryDir dir;
|
|
569
|
+
REQUIRE(dir.isValid());
|
|
570
|
+
REQUIRE(host.setContentRoot(dir.path()));
|
|
571
|
+
REQUIRE(host.setBridgeObject(&bridge));
|
|
572
|
+
QMetaObject::invokeMethod(&host, "handleLoadFinished", Q_ARG(bool, true));
|
|
573
|
+
|
|
574
|
+
host.setOutputValue(QStringLiteral("DemoBehaviorService"), QStringLiteral("outputParentState"), QStringLiteral("state-1"));
|
|
575
|
+
REQUIRE(waitForSignal(outputSpy, 4000));
|
|
576
|
+
outputSpy.clear();
|
|
577
|
+
|
|
578
|
+
QMetaObject::invokeMethod(&host, "handleLoadFinished", Q_ARG(bool, true));
|
|
579
|
+
REQUIRE(waitForSignal(outputSpy, 4000));
|
|
580
|
+
|
|
581
|
+
const auto args = outputSpy.takeFirst();
|
|
582
|
+
REQUIRE(args.count() == 3);
|
|
583
|
+
CHECK(args.at(0).toString() == QStringLiteral("DemoBehaviorService"));
|
|
584
|
+
CHECK(args.at(1).toString() == QStringLiteral("outputParentState"));
|
|
585
|
+
CHECK(args.at(2).toString() == QStringLiteral("state-1"));
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
TEST_CASE("Output snapshot replays to development WebSocket client on handshake", "[host][behavior][output]") {
|
|
589
|
+
ensureApp();
|
|
590
|
+
AnQstHostBridgeFacade facade;
|
|
591
|
+
AngularHttpBaseServer server;
|
|
592
|
+
|
|
593
|
+
QTemporaryDir dir;
|
|
594
|
+
REQUIRE(dir.isValid());
|
|
595
|
+
REQUIRE(server.configureContent(AngularHttpBaseServer::ContentRootMode::Filesystem, dir.path(), QStringLiteral("index.html")));
|
|
596
|
+
server.setFacade(&facade);
|
|
597
|
+
REQUIRE(server.start(false, 43900));
|
|
598
|
+
|
|
599
|
+
facade.setOutputValue(QStringLiteral("DemoBehaviorService"), QStringLiteral("outputParentState"), QStringLiteral("state-1"));
|
|
600
|
+
|
|
601
|
+
QTcpSocket client;
|
|
602
|
+
client.connectToHost(QHostAddress::LocalHost, server.wsPort());
|
|
603
|
+
REQUIRE(client.waitForConnected(2000));
|
|
604
|
+
client.write(
|
|
605
|
+
"GET /anqst-bridge HTTP/1.1\r\n"
|
|
606
|
+
"Host: localhost\r\n"
|
|
607
|
+
"Upgrade: websocket\r\n"
|
|
608
|
+
"Connection: Upgrade\r\n"
|
|
609
|
+
"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
|
|
610
|
+
"Sec-WebSocket-Version: 13\r\n"
|
|
611
|
+
"\r\n");
|
|
612
|
+
client.flush();
|
|
613
|
+
|
|
614
|
+
const QByteArray data = readSocketUntil(client, "state-1");
|
|
615
|
+
CHECK(data.contains("101 Switching Protocols"));
|
|
616
|
+
CHECK(data.contains("outputUpdated"));
|
|
617
|
+
CHECK(data.contains("DemoBehaviorService"));
|
|
618
|
+
CHECK(data.contains("outputParentState"));
|
|
619
|
+
CHECK(data.contains("state-1"));
|
|
620
|
+
|
|
621
|
+
server.stop();
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
TEST_CASE("Slot invocation queues across bridge readiness loss", "[host][behavior][slot]") {
|
|
625
|
+
ensureApp();
|
|
626
|
+
AnQstHostBridgeFacade facade;
|
|
627
|
+
facade.setSlotInvocationTimeoutMs(1000);
|
|
628
|
+
QSignalSpy slotSpy(&facade, &AnQstHostBridgeFacade::bridgeSlotInvocationRequested);
|
|
629
|
+
|
|
630
|
+
facade.setDispatchEnabled(true);
|
|
631
|
+
facade.registerSlot(QStringLiteral("DemoBehaviorService"), QStringLiteral("slotPrompt"));
|
|
632
|
+
facade.setDispatchEnabled(false);
|
|
633
|
+
|
|
634
|
+
QTimer::singleShot(20, [&facade]() {
|
|
635
|
+
facade.registerSlot(QStringLiteral("DemoBehaviorService"), QStringLiteral("slotPrompt"));
|
|
636
|
+
facade.setDispatchEnabled(true);
|
|
637
|
+
});
|
|
638
|
+
QObject::connect(&facade, &AnQstHostBridgeFacade::bridgeSlotInvocationRequested, &facade, [&facade](const QString& requestId) {
|
|
639
|
+
facade.resolveSlot(requestId, true, QStringLiteral("ok"), QString());
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
QVariant result;
|
|
643
|
+
QString error;
|
|
644
|
+
const bool ok = facade.invokeSlot(QStringLiteral("DemoBehaviorService"), QStringLiteral("slotPrompt"), {}, &result, &error);
|
|
645
|
+
|
|
646
|
+
CHECK(ok);
|
|
647
|
+
CHECK(result.toString() == QStringLiteral("ok"));
|
|
648
|
+
CHECK(error.isEmpty());
|
|
649
|
+
CHECK(slotSpy.count() == 1);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
TEST_CASE("resolveAssetPath blocks non-local schemes and emits policy error", "[host][policy]") {
|
|
653
|
+
ensureApp();
|
|
654
|
+
AnQstWebHostBase host;
|
|
655
|
+
QSignalSpy errorSpy(&host, &AnQstWebHostBase::onHostError);
|
|
656
|
+
|
|
657
|
+
QTemporaryDir dir;
|
|
658
|
+
REQUIRE(dir.isValid());
|
|
659
|
+
REQUIRE(host.setContentRoot(dir.path()));
|
|
660
|
+
|
|
661
|
+
const QUrl resolved = host.resolveAssetPath("https://example.org/index.html");
|
|
662
|
+
REQUIRE_FALSE(resolved.isValid());
|
|
663
|
+
REQUIRE(waitForSignal(errorSpy));
|
|
664
|
+
|
|
665
|
+
const QVariantMap payload = firstPayload(errorSpy);
|
|
666
|
+
CHECK(payload.value("code").toString() == "HOST_POLICY_SCHEME_BLOCKED");
|
|
667
|
+
CHECK(payload.value("category").toString() == "policy");
|
|
668
|
+
CHECK(payload.value("severity").toString() == "error");
|
|
669
|
+
CHECK(payload.value("recoverable").toBool());
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
TEST_CASE("loadEntryPoint emits missing entry error for absent file", "[host][load]") {
|
|
673
|
+
ensureApp();
|
|
674
|
+
AnQstWebHostBase host;
|
|
675
|
+
QSignalSpy errorSpy(&host, &AnQstWebHostBase::onHostError);
|
|
676
|
+
|
|
677
|
+
QTemporaryDir dir;
|
|
678
|
+
REQUIRE(dir.isValid());
|
|
679
|
+
REQUIRE(host.setContentRoot(dir.path()));
|
|
680
|
+
|
|
681
|
+
REQUIRE_FALSE(host.loadEntryPoint("does-not-exist.html"));
|
|
682
|
+
REQUIRE(waitForSignal(errorSpy));
|
|
683
|
+
|
|
684
|
+
const QVariantMap payload = firstPayload(errorSpy);
|
|
685
|
+
CHECK(payload.value("code").toString() == "HOST_LOAD_ENTRY_NOT_FOUND");
|
|
686
|
+
CHECK(payload.value("category").toString() == "load");
|
|
687
|
+
CHECK(payload.value("severity").toString() == "error");
|
|
688
|
+
CHECK(payload.value("recoverable").toBool() == false);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
TEST_CASE("load failure emits raw WebEngine error without bridge", "[host][webengine][raw]") {
|
|
692
|
+
ensureApp();
|
|
693
|
+
AnQstWebHostBase host;
|
|
694
|
+
QSignalSpy webEngineSpy(&host, &AnQstWebHostBase::onWebEngineError);
|
|
695
|
+
|
|
696
|
+
QMetaObject::invokeMethod(&host, "handleLoadFinished", Q_ARG(bool, false));
|
|
697
|
+
|
|
698
|
+
REQUIRE(waitForWebEngineError(
|
|
699
|
+
webEngineSpy,
|
|
700
|
+
QStringLiteral("webengine.load_failed"),
|
|
701
|
+
{QStringLiteral("Host failed to load entry point.")}));
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
TEST_CASE("blocked navigation emits raw WebEngine error", "[host][webengine][policy]") {
|
|
705
|
+
ensureApp();
|
|
706
|
+
AnQstWebHostBase host;
|
|
707
|
+
QSignalSpy webEngineSpy(&host, &AnQstWebHostBase::onWebEngineError);
|
|
708
|
+
|
|
709
|
+
QMetaObject::invokeMethod(
|
|
710
|
+
&host,
|
|
711
|
+
"handleNavigationPolicyError",
|
|
712
|
+
Q_ARG(QUrl, QUrl(QStringLiteral("https://example.org/blocked.js"))));
|
|
713
|
+
|
|
714
|
+
REQUIRE(waitForWebEngineError(
|
|
715
|
+
webEngineSpy,
|
|
716
|
+
QStringLiteral("webengine.navigation_blocked"),
|
|
717
|
+
{
|
|
718
|
+
QStringLiteral("Navigation blocked by local-content policy."),
|
|
719
|
+
QStringLiteral("https://example.org/blocked.js"),
|
|
720
|
+
}));
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
TEST_CASE("javascript runtime errors emit detailed WebEngine diagnostics without bridge", "[host][webengine][javascript]") {
|
|
724
|
+
ensureApp();
|
|
725
|
+
AnQstWebHostBase host;
|
|
726
|
+
|
|
727
|
+
QSignalSpy webEngineSpy(&host, &AnQstWebHostBase::onWebEngineError);
|
|
728
|
+
|
|
729
|
+
QTemporaryDir dir;
|
|
730
|
+
REQUIRE(dir.isValid());
|
|
731
|
+
writeHtmlFile(
|
|
732
|
+
dir.filePath("index.html"),
|
|
733
|
+
"<!doctype html>\n"
|
|
734
|
+
"<html><body><script>\n"
|
|
735
|
+
"window.addEventListener('load', function () {\n"
|
|
736
|
+
" setTimeout(function () {\n"
|
|
737
|
+
" throw new Error('planned js failure for onWebEngineError test');\n"
|
|
738
|
+
" }, 0);\n"
|
|
739
|
+
"});\n"
|
|
740
|
+
"</script></body></html>\n");
|
|
741
|
+
|
|
742
|
+
REQUIRE(host.setContentRoot(dir.path()));
|
|
743
|
+
REQUIRE(host.loadEntryPoint(QStringLiteral("index.html")));
|
|
744
|
+
|
|
745
|
+
QString matchedDetail;
|
|
746
|
+
REQUIRE(waitForWebEngineError(
|
|
747
|
+
webEngineSpy,
|
|
748
|
+
QStringLiteral("js.window.error"),
|
|
749
|
+
{
|
|
750
|
+
QStringLiteral("Unhandled window error."),
|
|
751
|
+
QStringLiteral("planned js failure for onWebEngineError test"),
|
|
752
|
+
QStringLiteral("Stack:"),
|
|
753
|
+
},
|
|
754
|
+
10000,
|
|
755
|
+
&matchedDetail));
|
|
756
|
+
CHECK(matchedDetail.contains(QStringLiteral("Message:")));
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
TEST_CASE("host emits ready when entry loads and bridge is attached", "[host][ready]") {
|
|
760
|
+
ensureApp();
|
|
761
|
+
AnQstWebHostBase host;
|
|
762
|
+
DummyBridge bridge;
|
|
763
|
+
|
|
764
|
+
QSignalSpy readySpy(&host, &AnQstWebHostBase::onHostReady);
|
|
765
|
+
QSignalSpy errorSpy(&host, &AnQstWebHostBase::onHostError);
|
|
766
|
+
|
|
767
|
+
QTemporaryDir dir;
|
|
768
|
+
REQUIRE(dir.isValid());
|
|
769
|
+
REQUIRE(host.setContentRoot(dir.path()));
|
|
770
|
+
REQUIRE(host.setBridgeObject(&bridge));
|
|
771
|
+
QMetaObject::invokeMethod(&host, "handleLoadFinished", Q_ARG(bool, true));
|
|
772
|
+
REQUIRE(waitForSignal(readySpy, 4000));
|
|
773
|
+
CHECK(errorSpy.count() == 0);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
TEST_CASE("debug dialog resource provider order matches contract", "[host][debug][dialog]") {
|
|
777
|
+
ensureApp();
|
|
778
|
+
AnQstWidgetDebugDialog::InitialState initial;
|
|
779
|
+
initial.widgetName = QStringLiteral("DemoWidget");
|
|
780
|
+
initial.hostMode = AnQstWidgetDebugDialog::HostMode::Application;
|
|
781
|
+
initial.resourceProvider = AnQstWidgetDebugDialog::ResourceProvider::Qrc;
|
|
782
|
+
initial.resourceUrl = QStringLiteral("http://localhost:4200/");
|
|
783
|
+
initial.resourceDirectory = QDir::currentPath();
|
|
784
|
+
|
|
785
|
+
AnQstWidgetDebugDialog dialog(initial);
|
|
786
|
+
auto* providerCombo = dialog.findChild<QComboBox*>(QStringLiteral("cbWidgetResource"));
|
|
787
|
+
REQUIRE(providerCombo != nullptr);
|
|
788
|
+
REQUIRE(providerCombo->count() >= 3);
|
|
789
|
+
CHECK(providerCombo->itemText(0).contains(QStringLiteral("QRC")));
|
|
790
|
+
CHECK(providerCombo->itemText(1).contains(QStringLiteral("directory"), Qt::CaseInsensitive));
|
|
791
|
+
CHECK(providerCombo->itemText(2).contains(QStringLiteral("HTTP"), Qt::CaseInsensitive));
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
TEST_CASE("debug dialog preserves open-browser choice on accept", "[host][debug][dialog]") {
|
|
795
|
+
ensureApp();
|
|
796
|
+
AnQstWidgetDebugDialog::InitialState initial;
|
|
797
|
+
initial.widgetName = QStringLiteral("DemoWidget");
|
|
798
|
+
initial.hostMode = AnQstWidgetDebugDialog::HostMode::Application;
|
|
799
|
+
initial.resourceProvider = AnQstWidgetDebugDialog::ResourceProvider::Qrc;
|
|
800
|
+
initial.resourceUrl = QStringLiteral("http://localhost:4200/");
|
|
801
|
+
initial.resourceDirectory = QDir::currentPath();
|
|
802
|
+
|
|
803
|
+
AnQstWidgetDebugDialog dialog(initial);
|
|
804
|
+
auto* hostCombo = dialog.findChild<QComboBox*>(QStringLiteral("cbAnQstAngularAppHost"));
|
|
805
|
+
auto* openBrowser = dialog.findChild<QCheckBox*>(QStringLiteral("rbOpenBrowser"));
|
|
806
|
+
REQUIRE(hostCombo != nullptr);
|
|
807
|
+
REQUIRE(openBrowser != nullptr);
|
|
808
|
+
|
|
809
|
+
hostCombo->setCurrentIndex(1);
|
|
810
|
+
openBrowser->setChecked(true);
|
|
811
|
+
dialog.accept();
|
|
812
|
+
|
|
813
|
+
const auto result = dialog.resultState();
|
|
814
|
+
CHECK(result.accepted);
|
|
815
|
+
CHECK(result.hostMode == AnQstWidgetDebugDialog::HostMode::Browser);
|
|
816
|
+
CHECK(result.openBrowserChecked);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
TEST_CASE("debug dialog JS console seeds history, appends lines, and submits enter without closing", "[host][debug][console]") {
|
|
820
|
+
ensureApp();
|
|
821
|
+
AnQstWidgetDebugDialog::InitialState initial;
|
|
822
|
+
initial.widgetName = QStringLiteral("DemoWidget");
|
|
823
|
+
initial.jsConsoleHistory = QStringList{
|
|
824
|
+
QStringLiteral("[info] first"),
|
|
825
|
+
QStringLiteral("[error] second"),
|
|
826
|
+
};
|
|
827
|
+
initial.jsConsoleCommandHistory = QStringList{
|
|
828
|
+
QStringLiteral("console.log('line1')"),
|
|
829
|
+
QStringLiteral("console.log('line2')"),
|
|
830
|
+
};
|
|
831
|
+
|
|
832
|
+
AnQstWidgetDebugDialog dialog(initial);
|
|
833
|
+
auto* tabWidget = dialog.findChild<QTabWidget*>(QStringLiteral("tabWidget"));
|
|
834
|
+
auto* tabConsole = dialog.findChild<QWidget*>(QStringLiteral("tabJSConsole"));
|
|
835
|
+
auto* logView = dialog.findChild<QPlainTextEdit*>(QStringLiteral("txtEditJSLog"));
|
|
836
|
+
auto* input = dialog.findChild<QLineEdit*>(QStringLiteral("lineEditJSConsoleInput"));
|
|
837
|
+
REQUIRE(tabWidget != nullptr);
|
|
838
|
+
REQUIRE(tabConsole != nullptr);
|
|
839
|
+
REQUIRE(logView != nullptr);
|
|
840
|
+
REQUIRE(input != nullptr);
|
|
841
|
+
|
|
842
|
+
QSignalSpy submitSpy(&dialog, &AnQstWidgetDebugDialog::jsConsoleCommandSubmitted);
|
|
843
|
+
|
|
844
|
+
dialog.show();
|
|
845
|
+
QCoreApplication::processEvents(QEventLoop::AllEvents, 50);
|
|
846
|
+
|
|
847
|
+
REQUIRE(logView->toPlainText().contains(QStringLiteral("[info] first")));
|
|
848
|
+
REQUIRE(logView->toPlainText().contains(QStringLiteral("[error] second")));
|
|
849
|
+
|
|
850
|
+
tabWidget->setCurrentWidget(tabConsole);
|
|
851
|
+
QCoreApplication::processEvents(QEventLoop::AllEvents, 50);
|
|
852
|
+
|
|
853
|
+
QKeyEvent upToLine2(QEvent::KeyPress, Qt::Key_Up, Qt::NoModifier);
|
|
854
|
+
QCoreApplication::sendEvent(input, &upToLine2);
|
|
855
|
+
CHECK(input->text() == QStringLiteral("console.log('line2')"));
|
|
856
|
+
|
|
857
|
+
QKeyEvent upToLine1(QEvent::KeyPress, Qt::Key_Up, Qt::NoModifier);
|
|
858
|
+
QCoreApplication::sendEvent(input, &upToLine1);
|
|
859
|
+
CHECK(input->text() == QStringLiteral("console.log('line1')"));
|
|
860
|
+
|
|
861
|
+
QKeyEvent downToLine2(QEvent::KeyPress, Qt::Key_Down, Qt::NoModifier);
|
|
862
|
+
QCoreApplication::sendEvent(input, &downToLine2);
|
|
863
|
+
CHECK(input->text() == QStringLiteral("console.log('line2')"));
|
|
864
|
+
|
|
865
|
+
QKeyEvent downToBlank(QEvent::KeyPress, Qt::Key_Down, Qt::NoModifier);
|
|
866
|
+
QCoreApplication::sendEvent(input, &downToBlank);
|
|
867
|
+
CHECK(input->text().isEmpty());
|
|
868
|
+
|
|
869
|
+
input->setText(QStringLiteral("console.log('hi')"));
|
|
870
|
+
QKeyEvent keyPress(QEvent::KeyPress, Qt::Key_Return, Qt::NoModifier);
|
|
871
|
+
QCoreApplication::sendEvent(input, &keyPress);
|
|
872
|
+
|
|
873
|
+
REQUIRE(submitSpy.count() == 1);
|
|
874
|
+
CHECK(submitSpy.takeFirst().at(0).toString() == QStringLiteral("console.log('hi')"));
|
|
875
|
+
CHECK(dialog.result() == 0);
|
|
876
|
+
CHECK(input->text().isEmpty());
|
|
877
|
+
|
|
878
|
+
QKeyEvent upToSubmitted(QEvent::KeyPress, Qt::Key_Up, Qt::NoModifier);
|
|
879
|
+
QCoreApplication::sendEvent(input, &upToSubmitted);
|
|
880
|
+
CHECK(input->text() == QStringLiteral("console.log('hi')"));
|
|
881
|
+
|
|
882
|
+
dialog.appendJsConsoleLine(QStringLiteral("[info] live"));
|
|
883
|
+
CHECK(logView->toPlainText().contains(QStringLiteral("[info] live")));
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
TEST_CASE("debug shortcut is bound to Shift+F12", "[host][debug][shortcut]") {
|
|
887
|
+
ensureApp();
|
|
888
|
+
AnQstWebHostBase host;
|
|
889
|
+
|
|
890
|
+
const QList<QShortcut*> shortcuts = host.findChildren<QShortcut*>();
|
|
891
|
+
bool hasF12Shortcut = false;
|
|
892
|
+
bool hasShiftF12Shortcut = false;
|
|
893
|
+
for (QShortcut* shortcut : shortcuts) {
|
|
894
|
+
if (shortcut == nullptr) {
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
if (shortcut->key() == QKeySequence(Qt::Key_F12)) {
|
|
898
|
+
hasF12Shortcut = true;
|
|
899
|
+
}
|
|
900
|
+
if (shortcut->key() == QKeySequence(Qt::SHIFT | Qt::Key_F12)) {
|
|
901
|
+
hasShiftF12Shortcut = true;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
CHECK_FALSE(hasF12Shortcut);
|
|
906
|
+
CHECK(hasShiftF12Shortcut);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
TEST_CASE("enableDebug cancel keeps widget state unchanged", "[host][debug][dialog]") {
|
|
910
|
+
ensureApp();
|
|
911
|
+
AnQstWebHostBase host;
|
|
912
|
+
QSignalSpy debugSpy(&host, &AnQstWebHostBase::developmentModeEnabled);
|
|
913
|
+
|
|
914
|
+
QTemporaryDir dir;
|
|
915
|
+
REQUIRE(dir.isValid());
|
|
916
|
+
writeHtmlFile(dir.filePath("index.html"), "<html><body>ok</body></html>");
|
|
917
|
+
REQUIRE(host.setContentRoot(dir.path()));
|
|
918
|
+
REQUIRE(host.loadEntryPoint("index.html"));
|
|
919
|
+
|
|
920
|
+
scheduleDebugDialogInteraction(
|
|
921
|
+
1,
|
|
922
|
+
1,
|
|
923
|
+
dir.path(),
|
|
924
|
+
QStringLiteral("http://localhost:4200/"),
|
|
925
|
+
true,
|
|
926
|
+
false);
|
|
927
|
+
REQUIRE_FALSE(host.enableDebug());
|
|
928
|
+
CHECK_FALSE(host.isDevelopmentModeEnabled());
|
|
929
|
+
CHECK(host.developmentModeUrl().isEmpty());
|
|
930
|
+
CHECK(debugSpy.count() == 0);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
TEST_CASE("debug dialog applies all host/provider combinations", "[host][debug][matrix]") {
|
|
934
|
+
ensureApp();
|
|
935
|
+
AnQstWebHostBase host;
|
|
936
|
+
|
|
937
|
+
QTemporaryDir dir;
|
|
938
|
+
REQUIRE(dir.isValid());
|
|
939
|
+
writeHtmlFile(dir.filePath("qwebchannel.js"), "console.log('ok');");
|
|
940
|
+
REQUIRE(host.setContentRoot(QStringLiteral("qrc:/qtwebchannel")));
|
|
941
|
+
REQUIRE(host.loadEntryPoint("qwebchannel.js"));
|
|
942
|
+
|
|
943
|
+
QTcpServer upstream;
|
|
944
|
+
REQUIRE(upstream.listen(QHostAddress::Any, 0));
|
|
945
|
+
QObject::connect(&upstream, &QTcpServer::newConnection, &upstream, [&upstream]() {
|
|
946
|
+
QTcpSocket* incoming = upstream.nextPendingConnection();
|
|
947
|
+
REQUIRE(incoming != nullptr);
|
|
948
|
+
QObject::connect(incoming, &QTcpSocket::readyRead, incoming, [incoming]() {
|
|
949
|
+
incoming->readAll();
|
|
950
|
+
incoming->write(
|
|
951
|
+
"HTTP/1.1 200 OK\r\n"
|
|
952
|
+
"Content-Type: text/plain\r\n"
|
|
953
|
+
"Content-Length: 2\r\n"
|
|
954
|
+
"Connection: close\r\n"
|
|
955
|
+
"\r\n"
|
|
956
|
+
"ok");
|
|
957
|
+
incoming->disconnectFromHost();
|
|
958
|
+
});
|
|
959
|
+
});
|
|
960
|
+
const QString upstreamUrl = QStringLiteral("http://localhost:%1/").arg(upstream.serverPort());
|
|
961
|
+
|
|
962
|
+
const struct Scenario {
|
|
963
|
+
int hostIndex;
|
|
964
|
+
int providerIndex;
|
|
965
|
+
bool expectBrowserHost;
|
|
966
|
+
} scenarios[] = {
|
|
967
|
+
{0, 0, false},
|
|
968
|
+
{0, 1, false},
|
|
969
|
+
{0, 2, false},
|
|
970
|
+
{1, 0, true},
|
|
971
|
+
{1, 1, true},
|
|
972
|
+
{1, 2, true},
|
|
973
|
+
};
|
|
974
|
+
|
|
975
|
+
for (const Scenario& scenario : scenarios) {
|
|
976
|
+
scheduleDebugDialogInteraction(
|
|
977
|
+
scenario.hostIndex,
|
|
978
|
+
scenario.providerIndex,
|
|
979
|
+
dir.path(),
|
|
980
|
+
upstreamUrl,
|
|
981
|
+
false,
|
|
982
|
+
true);
|
|
983
|
+
REQUIRE(host.enableDebug());
|
|
984
|
+
CHECK(host.isDevelopmentModeEnabled() == scenario.expectBrowserHost);
|
|
985
|
+
if (scenario.expectBrowserHost) {
|
|
986
|
+
CHECK_FALSE(host.developmentModeUrl().isEmpty());
|
|
987
|
+
auto* placeholder = host.findChild<QLabel*>(QStringLiteral("AnQstDevModePlaceholder"));
|
|
988
|
+
REQUIRE(placeholder != nullptr);
|
|
989
|
+
CHECK_FALSE(placeholder->isHidden());
|
|
990
|
+
} else {
|
|
991
|
+
CHECK(host.developmentModeUrl().isEmpty());
|
|
992
|
+
auto* placeholder = host.findChild<QLabel*>(QStringLiteral("AnQstDevModePlaceholder"));
|
|
993
|
+
REQUIRE(placeholder != nullptr);
|
|
994
|
+
CHECK(placeholder->isHidden());
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
TEST_CASE("reattach button restores Application host", "[host][debug][reattach]") {
|
|
1000
|
+
ensureApp();
|
|
1001
|
+
AnQstWebHostBase host;
|
|
1002
|
+
|
|
1003
|
+
QTemporaryDir dir;
|
|
1004
|
+
REQUIRE(dir.isValid());
|
|
1005
|
+
writeHtmlFile(dir.filePath("index.html"), "<html><body>ok</body></html>");
|
|
1006
|
+
REQUIRE(host.setContentRoot(dir.path()));
|
|
1007
|
+
REQUIRE(host.loadEntryPoint("index.html"));
|
|
1008
|
+
|
|
1009
|
+
scheduleDebugDialogInteraction(
|
|
1010
|
+
1,
|
|
1011
|
+
1,
|
|
1012
|
+
dir.path(),
|
|
1013
|
+
QStringLiteral("http://localhost:4200/"),
|
|
1014
|
+
false,
|
|
1015
|
+
true);
|
|
1016
|
+
REQUIRE(host.enableDebug());
|
|
1017
|
+
REQUIRE(host.isDevelopmentModeEnabled());
|
|
1018
|
+
|
|
1019
|
+
auto* reattachButton = host.findChild<QPushButton*>(QStringLiteral("AnQstDevModeReattachButton"));
|
|
1020
|
+
REQUIRE(reattachButton != nullptr);
|
|
1021
|
+
reattachButton->click();
|
|
1022
|
+
|
|
1023
|
+
CHECK_FALSE(host.isDevelopmentModeEnabled());
|
|
1024
|
+
CHECK(host.developmentModeUrl().isEmpty());
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
TEST_CASE("proxy mode forwards streamed HTTP responses", "[host][debug][proxy][stream]") {
|
|
1028
|
+
ensureApp();
|
|
1029
|
+
|
|
1030
|
+
QTcpServer upstream;
|
|
1031
|
+
REQUIRE(upstream.listen(QHostAddress::Any, 0));
|
|
1032
|
+
const quint16 upstreamPort = upstream.serverPort();
|
|
1033
|
+
bool sawUpstreamRequest = false;
|
|
1034
|
+
|
|
1035
|
+
QObject::connect(&upstream, &QTcpServer::newConnection, &upstream, [&upstream, &sawUpstreamRequest]() {
|
|
1036
|
+
QTcpSocket* incoming = upstream.nextPendingConnection();
|
|
1037
|
+
REQUIRE(incoming != nullptr);
|
|
1038
|
+
QObject::connect(incoming, &QTcpSocket::readyRead, incoming, [incoming, &sawUpstreamRequest]() {
|
|
1039
|
+
incoming->readAll();
|
|
1040
|
+
sawUpstreamRequest = true;
|
|
1041
|
+
incoming->write(
|
|
1042
|
+
"HTTP/1.1 200 OK\r\n"
|
|
1043
|
+
"Content-Type: text/plain\r\n"
|
|
1044
|
+
"Transfer-Encoding: chunked\r\n"
|
|
1045
|
+
"Connection: close\r\n"
|
|
1046
|
+
"\r\n");
|
|
1047
|
+
incoming->write("5\r\nhello\r\n");
|
|
1048
|
+
QTimer::singleShot(60, incoming, [incoming]() {
|
|
1049
|
+
if (incoming->state() == QAbstractSocket::ConnectedState) {
|
|
1050
|
+
incoming->write("6\r\n world\r\n");
|
|
1051
|
+
incoming->write("0\r\n\r\n");
|
|
1052
|
+
incoming->flush();
|
|
1053
|
+
QTimer::singleShot(60, incoming, [incoming]() {
|
|
1054
|
+
if (incoming->state() == QAbstractSocket::ConnectedState) {
|
|
1055
|
+
incoming->disconnectFromHost();
|
|
1056
|
+
}
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
});
|
|
1060
|
+
});
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
AngularHttpBaseServer proxy;
|
|
1064
|
+
proxy.setBridgeObjectName(QStringLiteral("TestBridge"));
|
|
1065
|
+
REQUIRE(proxy.configureProxyTarget(QUrl(QStringLiteral("http://localhost:%1/").arg(upstreamPort))));
|
|
1066
|
+
REQUIRE(proxy.start(false, 43800));
|
|
1067
|
+
|
|
1068
|
+
QTcpSocket client;
|
|
1069
|
+
client.connectToHost(QHostAddress::LocalHost, proxy.httpPort());
|
|
1070
|
+
REQUIRE(client.waitForConnected(2000));
|
|
1071
|
+
client.write(
|
|
1072
|
+
"GET /stream HTTP/1.1\r\n"
|
|
1073
|
+
"Host: localhost\r\n"
|
|
1074
|
+
"Connection: close\r\n"
|
|
1075
|
+
"\r\n");
|
|
1076
|
+
client.flush();
|
|
1077
|
+
|
|
1078
|
+
QByteArray response;
|
|
1079
|
+
QElapsedTimer timer;
|
|
1080
|
+
timer.start();
|
|
1081
|
+
while (timer.elapsed() < 5000) {
|
|
1082
|
+
QCoreApplication::processEvents(QEventLoop::AllEvents, 50);
|
|
1083
|
+
client.waitForReadyRead(100);
|
|
1084
|
+
response += client.readAll();
|
|
1085
|
+
if (client.state() == QAbstractSocket::UnconnectedState) {
|
|
1086
|
+
break;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
CHECK(sawUpstreamRequest);
|
|
1091
|
+
CHECK(response.contains("hello"));
|
|
1092
|
+
CHECK(response.contains(" world"));
|
|
1093
|
+
proxy.stop();
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
int main(int argc, char* argv[]) {
|
|
1097
|
+
const int result = Catch::Session().run(argc, argv);
|
|
1098
|
+
// QWebEngine can segfault during global/static teardown in headless CI.
|
|
1099
|
+
std::_Exit(result);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
#include "test_AnQstWebHostBase.moc"
|