(.*?)");
- auto match = rx.match(data);
- if (match.hasMatch()) {
- QString errors = match.captured(1);
- static const QRegularExpression rx2("
");
- static const QRegularExpression rxremove("<[^>]*>");
- auto matchIterator = rx2.globalMatch(errors);
- while (matchIterator.hasNext()) {
- auto match2 = matchIterator.next();
- errorMessageList.append(match2.captured(1).remove(rxremove).simplified());
- }
+ const int httpStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+ if (httpStatus >= 200 && httpStatus < 400 && reply->hasRawHeader("Location")) {
+ const QByteArray location = reply->rawHeader("Location");
+ QUrl deckUrl = reply->url().resolved(QUrl::fromEncoded(location, QUrl::StrictMode));
+ if (location.trimmed().isEmpty() || !normalizeAnalysisUrl(deckUrl, QStringLiteral("tappedout.net"))) {
+ showError(tr("The reply from TappedOut contained an invalid deck address."));
+ return;
}
-
- QString errorMessage = errorMessageList.join("\n");
- qCWarning(TappedOutInterfaceLog) << "Tappedout: bad reply, http status" << httpStatus << "size" << data.size()
- << "message" << errorMessage;
-
- QMessageBox::critical(nullptr, tr("Error"), errorMessage);
+ QPointer
guard(this);
+ deleteLater();
+ if (!QDesktopServices::openUrl(deckUrl) && guard) {
+ showError(tr("Could not open your browser. Open this address to see the analyzed deck:\n%1")
+ .arg(deckUrl.toDisplayString()));
+ }
+ return;
}
- reply->deleteLater();
- deleteLater();
+ const QString data = QString::fromUtf8(reply->readAll());
+ QStringList errorMessageList = {tr("Unable to analyze the deck on TappedOut.")};
+ static const QRegularExpression errorsRx("(.*?)",
+ QRegularExpression::DotMatchesEverythingOption);
+ const auto match = errorsRx.match(data);
+ if (match.hasMatch()) {
+ static const QRegularExpression itemRx("
(.*?)", QRegularExpression::DotMatchesEverythingOption);
+ auto items = itemRx.globalMatch(match.captured(1));
+ while (items.hasNext()) {
+ const QString error = QTextDocumentFragment::fromHtml(items.next().captured(1)).toPlainText().simplified();
+ if (!error.isEmpty()) {
+ errorMessageList.append(error);
+ }
+ }
+ }
+ qCWarning(TappedOutInterfaceLog) << "TappedOut analysis failed, HTTP status" << httpStatus;
+ showError(errorMessageList.join('\n'));
}
void TappedOutInterface::getAnalyzeRequestData(const DeckList &deck, QByteArray &data)
@@ -90,13 +106,9 @@ void TappedOutInterface::getAnalyzeRequestData(const DeckList &deck, QByteArray
DeckList mainboard, sideboard;
copyDeckSplitMainAndSide(deck, mainboard, sideboard);
- QUrl params;
- QUrlQuery urlQuery;
- urlQuery.addQueryItem("name", deck.getName());
- urlQuery.addQueryItem("mainboard", mainboard.writeToString_Plain(false, true));
- urlQuery.addQueryItem("sideboard", sideboard.writeToString_Plain(false, true));
- params.setQuery(urlQuery);
- data.append(params.query(QUrl::EncodeReserved).toUtf8());
+ data.append("name=" + QUrl::toPercentEncoding(deck.getName()));
+ data.append("&mainboard=" + QUrl::toPercentEncoding(mainboard.writeToString_Plain(false, true)));
+ data.append("&sideboard=" + QUrl::toPercentEncoding(sideboard.writeToString_Plain(false, true)));
}
void TappedOutInterface::analyzeDeck(const DeckList &deck)
@@ -117,7 +129,9 @@ void TappedOutInterface::copyDeckSplitMainAndSide(const DeckList &source, DeckLi
{
auto copyMainOrSide = [&mainboard, &sideboard](const auto node, const auto card) {
CardInfoPtr dbCard = CardDatabaseManager::query()->getCardInfo(card->getName());
- if (!dbCard || dbCard->getIsToken() || node->getName() == DECK_ZONE_MAYBEBOARD) {
+ // Exclude confirmed tokens, but preserve cards missing from the local
+ // database, including newly released cards in an imported deck.
+ if ((dbCard && dbCard->getIsToken()) || node->getName() == DECK_ZONE_MAYBEBOARD) {
return;
}
diff --git a/cockatrice/src/client/network/interfaces/tapped_out_interface.h b/cockatrice/src/client/network/interfaces/tapped_out_interface.h
index 615991b07..27e8c2273 100644
--- a/cockatrice/src/client/network/interfaces/tapped_out_interface.h
+++ b/cockatrice/src/client/network/interfaces/tapped_out_interface.h
@@ -14,6 +14,7 @@
inline Q_LOGGING_CATEGORY(TappedOutInterfaceLog, "tapped_out_interface");
class QByteArray;
+class QString;
class QNetworkAccessManager;
class QNetworkReply;
class DeckList;
@@ -29,6 +30,8 @@ class TappedOutInterface : public QObject
Q_OBJECT
private:
QNetworkAccessManager *manager;
+ bool replyHandled = false;
+ void showError(const QString &message);
void copyDeckSplitMainAndSide(const DeckList &source, DeckList &mainboard, DeckList &sideboard);
private slots:
From 2e0a718cd73ee38bfc0ddcb3ee439092e172cf2c Mon Sep 17 00:00:00 2001
From: BruebachL <44814898+BruebachL@users.noreply.github.com>
Date: Wed, 4 Nov 2026 06:23:43 +0100
Subject: [PATCH 16/57] [Tests] Vendor QtTesting behind an opt-in WITH_GUI_TEST
flag (#7693)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: Lukas Brübach
---
.gitmodules | 3 ++
CMakeLists.txt | 38 +++++++++++++++++++++++
cockatrice/CMakeLists.txt | 5 +++
cockatrice/resources/config/qtlogging.ini | 1 +
external/qttesting | 1 +
5 files changed, 48 insertions(+)
create mode 160000 external/qttesting
diff --git a/.gitmodules b/.gitmodules
index 732fec7ca..e69c8e686 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -4,3 +4,6 @@
[submodule "doxygen-awesome-css"]
path = doc/doxygen/theme
url = https://github.com/jothepro/doxygen-awesome-css.git
+[submodule "external/qttesting"]
+ path = external/qttesting
+ url = https://github.com/Kitware/QtTesting.git
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 677ef0c12..563f174e8 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -20,6 +20,8 @@ option(WITH_CLIENT "Build Cockatrice client" ON)
option(WITH_ORACLE "Build Cockatrice card database tool (Oracle)" ON)
# Compile Servatrice
option(WITH_SERVER "Build Cockatrice server (Servatrice)" OFF)
+# Compile GUI tests (QtTesting widget-level playback)
+option(WITH_GUI_TEST "Build the QtTesting-based GUI test harness" OFF)
# Compile tests
option(TEST "Build tests" OFF)
# Build with AddressSanitizer and UndefinedBehaviorSanitizer
@@ -29,6 +31,14 @@ option(ENABLE_COVERAGE "Build instrumented for GCC line coverage" OFF)
# Use vcpkg regardless of OS
option(USE_VCPKG "Use vcpkg regardless of OS" OFF)
+# The GUI test harness lives in a submodule; a checkout without it must still configure.
+if(WITH_GUI_TEST AND NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/external/qttesting/CMakeLists.txt")
+ message(WARNING "WITH_GUI_TEST is ON but external/qttesting is missing, so GUI tests are disabled.\n"
+ "Run 'git submodule update --init external/qttesting' to enable them."
+ )
+ set(WITH_GUI_TEST OFF)
+endif()
+
# Default to "Release" build type
# User-provided value for CMAKE_BUILD_TYPE must be checked before the PROJECT() call
if(DEFINED CMAKE_BUILD_TYPE)
@@ -463,6 +473,34 @@ if(WITH_ORACLE)
set(CPACK_INSTALL_CMAKE_PROJECTS "Oracle;Oracle;ALL;/" ${CPACK_INSTALL_CMAKE_PROJECTS})
endif()
+if(WITH_GUI_TEST)
+ set(QtTesting_QT_VERSION "6")
+ # QtTesting registers its own unit tests when BUILD_TESTING is on. They target the
+ # Qt version the harness was written for and fail on Qt 6.11 (toolbutton
+ # long-press recording), and they are not Cockatrice's tests, so suppress them
+ # for the duration of the subdirectory instead of painting ctest red.
+ if(DEFINED BUILD_TESTING)
+ set(_cockatrice_saved_build_testing ${BUILD_TESTING})
+ endif()
+ set(BUILD_TESTING
+ OFF
+ CACHE BOOL "Build the testing tree." FORCE
+ )
+ add_subdirectory(external/qttesting)
+ if(DEFINED _cockatrice_saved_build_testing)
+ set(BUILD_TESTING
+ ${_cockatrice_saved_build_testing}
+ CACHE BOOL "Build the testing tree." FORCE
+ )
+ else()
+ unset(BUILD_TESTING CACHE)
+ endif()
+ # cl.exe has no warnings-as-errors here, and it rejects -Wno-error with D8021.
+ if(NOT MSVC)
+ target_compile_options(qttesting PRIVATE -Wno-error)
+ endif()
+endif()
+
if(TEST)
include(CTest)
add_subdirectory(tests)
diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt
index ccf2dfcea..79c26916f 100644
--- a/cockatrice/CMakeLists.txt
+++ b/cockatrice/CMakeLists.txt
@@ -566,6 +566,11 @@ target_link_libraries(
cockatrice_target_enable_crash_reporting(cockatrice)
+if(WITH_GUI_TEST)
+ target_link_libraries(cockatrice PRIVATE QtTesting)
+ target_compile_definitions(cockatrice PRIVATE WITH_GUI_TEST)
+endif()
+
if(UNIX)
if(APPLE)
set(MACOSX_BUNDLE_INFO_STRING "${PROJECT_NAME}")
diff --git a/cockatrice/resources/config/qtlogging.ini b/cockatrice/resources/config/qtlogging.ini
index 7ac0d9ca4..e4b845af9 100644
--- a/cockatrice/resources/config/qtlogging.ini
+++ b/cockatrice/resources/config/qtlogging.ini
@@ -10,6 +10,7 @@
# or set