diff --git a/CMakeLists.txt b/CMakeLists.txt index 836430637..e02d603b4 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,7 +13,7 @@ cmake_minimum_required(VERSION 3.24.0) cmake_policy(SET CMP0005 NEW) cmake_policy(SET CMP0048 NEW) # manages project version -project(QtMeshEditor VERSION 3.3.1 LANGUAGES C CXX) +project(QtMeshEditor VERSION 3.4.0 LANGUAGES C CXX) message(STATUS "Building QtMeshEditor version ${PROJECT_VERSION}") set(QTMESHEDITOR_VERSION_STRING "\"${PROJECT_VERSION}\"") diff --git a/README.md b/README.md index 6933488e6..3b120be3a 100755 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Available on the [GitHub Actions Marketplace](https://github.com/marketplace/act **Versioning** - **Always follow the latest GitHub release** — use the Marketplace floating tag `fernandotonon/QtMeshEditor@v1` (same pattern as the [Marketplace example](https://github.com/marketplace/actions/qtmesheditor)). The composite action defaults to `image-tag: latest`, so the Docker CLI tracks the newest published `ghcr.io/fernandotonon/qtmesh` image. -- **Reproducible builds** — pin the action and the container to the same semver as this repository’s `project(QtMeshEditor VERSION …)` in `CMakeLists.txt` (currently **3.1.0**). After bumping the version in CMake, run `./scripts/sync-doc-versions-from-cmake.sh` to refresh the pinned refs in `README.md` and the docs site fallback; CI enforces the match with `./scripts/sync-doc-versions-from-cmake.sh --check`. +- **Reproducible builds** — pin the action and the container to the same semver as this repository’s `project(QtMeshEditor VERSION …)` in `CMakeLists.txt` (currently **3.4.0**). After bumping the version in CMake, run `./scripts/sync-doc-versions-from-cmake.sh` to refresh the pinned refs in `README.md` and the docs site fallback; CI enforces the match with `./scripts/sync-doc-versions-from-cmake.sh --check`. Pinned workflow template (action + `ghcr.io` image aligned): @@ -51,10 +51,10 @@ jobs: - uses: actions/checkout@v4 - name: Run QtMesh scan - uses: fernandotonon/QtMeshEditor@3.3.1 + uses: fernandotonon/QtMeshEditor@3.4.0 with: command: scan - image-tag: "3.3.1" + image-tag: "3.4.0" env: QTMESH_CLOUD_TOKEN: ${{ secrets.QTMESH_CLOUD_TOKEN }} ``` @@ -79,39 +79,39 @@ Release tags are listed on the [releases page](https://github.com/fernandotonon/ ```yaml # Validate a specific mesh -- uses: fernandotonon/QtMeshEditor@3.3.1 +- uses: fernandotonon/QtMeshEditor@3.4.0 with: command: validate input-file: ./models/character.fbx - image-tag: "3.3.1" + image-tag: "3.4.0" # Convert FBX → glTF -- uses: fernandotonon/QtMeshEditor@3.3.1 +- uses: fernandotonon/QtMeshEditor@3.4.0 with: command: convert input-file: ./models/character.fbx output-file: ./output/character.gltf2 - image-tag: "3.3.1" + image-tag: "3.4.0" # Resample Mixamo animations (200+ keyframes → 30) -- uses: fernandotonon/QtMeshEditor@3.3.1 +- uses: fernandotonon/QtMeshEditor@3.4.0 with: command: anim input-file: ./animations/dance.fbx output-file: ./output/dance_optimized.fbx options: --resample 30 - image-tag: "3.3.1" + image-tag: "3.4.0" # Get mesh info as JSON -- uses: fernandotonon/QtMeshEditor@3.3.1 +- uses: fernandotonon/QtMeshEditor@3.4.0 id: info with: command: info input-file: ./models/character.fbx options: --json - image-tag: "3.3.1" + image-tag: "3.4.0" -# Docker (alternative — :latest tracks newest image; pin :3.1.0 to match semver action ref) +# Docker (alternative — :latest tracks newest image; pin :3.4.0 to match semver action ref) docker run --rm -v $(pwd):/workspace ghcr.io/fernandotonon/qtmesh:latest scan ./assets --fail-on error ``` diff --git a/src/CloudAccountMenuButton.cpp b/src/CloudAccountMenuButton.cpp index c8562e2f6..e86d04fb9 100644 --- a/src/CloudAccountMenuButton.cpp +++ b/src/CloudAccountMenuButton.cpp @@ -2,6 +2,7 @@ #include "AppSettingsKeys.h" #include "CloudCredentialStore.h" +#include "SentryReporter.h" #include #include @@ -177,7 +178,11 @@ CloudAccountMenuButton::CloudAccountMenuButton(QWidget* parent) m_button->setMenu(m_menu); layout->addWidget(m_button); - connect(m_menu, &QMenu::aboutToShow, this, &CloudAccountMenuButton::refresh); + connect(m_menu, &QMenu::aboutToShow, this, [this]() { + SentryReporter::addBreadcrumb(QStringLiteral("ui.action"), + QStringLiteral("Cloud toolbar menu opened")); + refresh(); + }); refresh(); } @@ -237,21 +242,37 @@ void CloudAccountMenuButton::buildMenu() m_openProjectsAction = m_menu->addAction(tr("Open My Projects")); m_openProjectsAction->setObjectName(QStringLiteral("actionQtMeshCloudOpenProjects")); - connect(m_openProjectsAction, &QAction::triggered, this, &CloudAccountMenuButton::openProjectsRequested); + connect(m_openProjectsAction, &QAction::triggered, this, [this]() { + SentryReporter::addBreadcrumb(QStringLiteral("ui.action"), + QStringLiteral("Cloud toolbar: Open My Projects")); + emit openProjectsRequested(); + }); m_uploadAction = m_menu->addAction(tr("Upload Files...")); m_uploadAction->setObjectName(QStringLiteral("actionQtMeshCloudUploadFiles")); - connect(m_uploadAction, &QAction::triggered, this, &CloudAccountMenuButton::uploadFilesRequested); + connect(m_uploadAction, &QAction::triggered, this, [this]() { + SentryReporter::addBreadcrumb(QStringLiteral("ui.action"), + QStringLiteral("Cloud toolbar: Upload Files")); + emit uploadFilesRequested(); + }); m_mainSeparator = m_menu->addSeparator(); m_signOutAction = m_menu->addAction(tr("Sign out")); m_signOutAction->setObjectName(QStringLiteral("actionQtMeshCloudSignOut")); - connect(m_signOutAction, &QAction::triggered, this, &CloudAccountMenuButton::signOutRequested); + connect(m_signOutAction, &QAction::triggered, this, [this]() { + SentryReporter::addBreadcrumb(QStringLiteral("ui.action"), + QStringLiteral("Cloud toolbar: Sign out")); + emit signOutRequested(); + }); m_signInAction = m_menu->addAction(tr("Sign in to QtMesh Cloud")); m_signInAction->setObjectName(QStringLiteral("actionQtMeshCloudSignIn")); - connect(m_signInAction, &QAction::triggered, this, &CloudAccountMenuButton::signInRequested); + connect(m_signInAction, &QAction::triggered, this, [this]() { + SentryReporter::addBreadcrumb(QStringLiteral("ui.action"), + QStringLiteral("Cloud toolbar: Sign in")); + emit signInRequested(); + }); } void CloudAccountMenuButton::updateHeader(const QString& displayName, bool signedIn) diff --git a/src/CloudAccountMenuButton_test.cpp b/src/CloudAccountMenuButton_test.cpp index 61993e073..53bd02aa1 100644 --- a/src/CloudAccountMenuButton_test.cpp +++ b/src/CloudAccountMenuButton_test.cpp @@ -8,10 +8,39 @@ #include #include #include +#include #include +#include +#include #include #include +namespace { + +QAction* findMenuAction(CloudAccountMenuButton& button, const QString& objectName) +{ + for (QAction* action : button.menu()->actions()) { + if (action->objectName() == objectName) + return action; + } + return nullptr; +} + +bool menuListsHeader(CloudAccountMenuButton& button) +{ + for (QAction* action : button.menu()->actions()) { + auto* widgetAction = qobject_cast(action); + if (!widgetAction) + continue; + QWidget* widget = widgetAction->defaultWidget(); + if (widget && widget->objectName() == QStringLiteral("cloudAccountMenuHeader")) + return true; + } + return false; +} + +} // namespace + class CloudAccountMenuButtonTest : public ::testing::Test { protected: QString previousOrganizationName; @@ -34,6 +63,14 @@ class CloudAccountMenuButtonTest : public ::testing::Test { QCoreApplication::setOrganizationName(previousOrganizationName); QCoreApplication::setApplicationName(previousApplicationName); } + + static void saveTestSession(const QString& email = QStringLiteral("dev@example.com")) + { + CloudSession session; + session.token = QStringLiteral("test-token"); + session.email = email; + ASSERT_TRUE(CloudCredentialStore::saveSession(session)); + } }; TEST_F(CloudAccountMenuButtonTest, InitialsFromDisplayName) @@ -45,25 +82,198 @@ TEST_F(CloudAccountMenuButtonTest, InitialsFromDisplayName) EXPECT_EQ(CloudAccountMenuButton::initialsFromDisplayName(QStringLiteral("Ada")), QStringLiteral("A")); EXPECT_EQ(CloudAccountMenuButton::initialsFromDisplayName(QStringLiteral(" ")), QString()); + EXPECT_EQ(CloudAccountMenuButton::initialsFromDisplayName(QStringLiteral("123")), + QString()); + EXPECT_EQ(CloudAccountMenuButton::initialsFromDisplayName(QStringLiteral("Jean-Pierre Martin")), + QStringLiteral("JM")); + EXPECT_EQ(CloudAccountMenuButton::initialsFromDisplayName(QStringLiteral("x y")), + QStringLiteral("XY")); } -TEST_F(CloudAccountMenuButtonTest, SignedOutButtonRepaintsWithoutCrash) +TEST_F(CloudAccountMenuButtonTest, RefreshSignedOutState) { CloudAccountMenuButton button; - button.show(); button.refresh(); - button.repaint(); - QApplication::processEvents(); + auto* signIn = findMenuAction(button, QStringLiteral("actionQtMeshCloudSignIn")); + auto* signOut = findMenuAction(button, QStringLiteral("actionQtMeshCloudSignOut")); + auto* openProjects = findMenuAction(button, QStringLiteral("actionQtMeshCloudOpenProjects")); + auto* upload = findMenuAction(button, QStringLiteral("actionQtMeshCloudUploadFiles")); + ASSERT_NE(signIn, nullptr); + ASSERT_NE(signOut, nullptr); + ASSERT_NE(openProjects, nullptr); + ASSERT_NE(upload, nullptr); + + EXPECT_TRUE(signIn->isVisible()); + EXPECT_TRUE(signIn->isEnabled()); + EXPECT_FALSE(signOut->isVisible()); + EXPECT_FALSE(openProjects->isEnabled()); + EXPECT_TRUE(upload->isEnabled()); + EXPECT_FALSE(menuListsHeader(button)); + EXPECT_EQ(button.toolButton()->toolTip(), QStringLiteral("Sign in to QtMesh Cloud")); +} + +TEST_F(CloudAccountMenuButtonTest, RefreshSignedInShowsHeaderAndAccountActions) +{ + saveTestSession(); + QSettings().setValue(AppSettingsKeys::cloudUserName(), QStringLiteral("Dev User")); + + CloudAccountMenuButton button; + button.refresh(); + + auto* signIn = findMenuAction(button, QStringLiteral("actionQtMeshCloudSignIn")); + auto* signOut = findMenuAction(button, QStringLiteral("actionQtMeshCloudSignOut")); + auto* openProjects = findMenuAction(button, QStringLiteral("actionQtMeshCloudOpenProjects")); + ASSERT_NE(signIn, nullptr); + ASSERT_NE(signOut, nullptr); + ASSERT_NE(openProjects, nullptr); + + EXPECT_FALSE(signIn->isVisible()); + EXPECT_TRUE(signOut->isVisible()); + EXPECT_TRUE(signOut->isEnabled()); + EXPECT_TRUE(openProjects->isEnabled()); + EXPECT_TRUE(menuListsHeader(button)); + + auto* headerName = button.findChild(QStringLiteral("cloudAccountMenuHeaderName"), + Qt::FindChildrenRecursively); + ASSERT_NE(headerName, nullptr); + EXPECT_EQ(headerName->text(), QStringLiteral("Dev User")); + + auto* subtitle = button.findChild(QStringLiteral("cloudAccountMenuHeaderSubtitle"), + Qt::FindChildrenRecursively); + ASSERT_NE(subtitle, nullptr); + EXPECT_EQ(subtitle->text(), QStringLiteral("Signed in to QtMesh Cloud")); + + EXPECT_TRUE(button.toolButton()->toolTip().contains(QStringLiteral("Dev User"))); +} + +TEST_F(CloudAccountMenuButtonTest, RefreshUsesSlugWhenUserNameMissing) +{ + saveTestSession(); + QSettings().setValue(AppSettingsKeys::cloudUserSlug(), QStringLiteral("dev-slug")); + + CloudAccountMenuButton button; + button.refresh(); + + auto* headerName = button.findChild(QStringLiteral("cloudAccountMenuHeaderName"), + Qt::FindChildrenRecursively); + ASSERT_NE(headerName, nullptr); + EXPECT_EQ(headerName->text(), QStringLiteral("dev-slug")); +} + +TEST_F(CloudAccountMenuButtonTest, RefreshUsesEmailWhenNameAndSlugMissing) +{ + saveTestSession(QStringLiteral("person@example.com")); + + CloudAccountMenuButton button; + button.refresh(); + + auto* headerName = button.findChild(QStringLiteral("cloudAccountMenuHeaderName"), + Qt::FindChildrenRecursively); + ASSERT_NE(headerName, nullptr); + EXPECT_EQ(headerName->text(), QStringLiteral("person@example.com")); +} + +TEST_F(CloudAccountMenuButtonTest, SignedInWithoutDisplayNameHidesHeader) +{ CloudSession session; session.token = QStringLiteral("test-token"); - session.email = QStringLiteral("dev@example.com"); ASSERT_TRUE(CloudCredentialStore::saveSession(session)); + + CloudAccountMenuButton button; + button.refresh(); + + EXPECT_FALSE(menuListsHeader(button)); + EXPECT_EQ(button.toolButton()->toolTip(), QStringLiteral("Sign in to QtMesh Cloud")); +} + +TEST_F(CloudAccountMenuButtonTest, MenuEmitsSignInRequested) +{ + CloudAccountMenuButton button; + QSignalSpy spy(&button, &CloudAccountMenuButton::signInRequested); + + auto* signIn = findMenuAction(button, QStringLiteral("actionQtMeshCloudSignIn")); + ASSERT_NE(signIn, nullptr); + signIn->trigger(); + EXPECT_EQ(spy.count(), 1); +} + +TEST_F(CloudAccountMenuButtonTest, MenuEmitsSignOutRequested) +{ + saveTestSession(); + QSettings().setValue(AppSettingsKeys::cloudUserName(), QStringLiteral("Dev User")); + + CloudAccountMenuButton button; + button.refresh(); + QSignalSpy spy(&button, &CloudAccountMenuButton::signOutRequested); + + auto* signOut = findMenuAction(button, QStringLiteral("actionQtMeshCloudSignOut")); + ASSERT_NE(signOut, nullptr); + signOut->trigger(); + EXPECT_EQ(spy.count(), 1); +} + +TEST_F(CloudAccountMenuButtonTest, MenuEmitsUploadFilesRequested) +{ + CloudAccountMenuButton button; + QSignalSpy spy(&button, &CloudAccountMenuButton::uploadFilesRequested); + + auto* upload = findMenuAction(button, QStringLiteral("actionQtMeshCloudUploadFiles")); + ASSERT_NE(upload, nullptr); + upload->trigger(); + EXPECT_EQ(spy.count(), 1); +} + +TEST_F(CloudAccountMenuButtonTest, MenuEmitsOpenProjectsRequested) +{ + saveTestSession(); QSettings().setValue(AppSettingsKeys::cloudUserName(), QStringLiteral("Dev User")); + + CloudAccountMenuButton button; + button.refresh(); + + auto* openProjects = findMenuAction(button, QStringLiteral("actionQtMeshCloudOpenProjects")); + ASSERT_NE(openProjects, nullptr); + ASSERT_TRUE(openProjects->isEnabled()); + + QSignalSpy spy(&button, &CloudAccountMenuButton::openProjectsRequested); + openProjects->trigger(); + EXPECT_EQ(spy.count(), 1); +} + +TEST_F(CloudAccountMenuButtonTest, AboutToShowRefreshesSignedInState) +{ + CloudAccountMenuButton button; + button.show(); + button.refresh(); + QApplication::processEvents(); + + saveTestSession(); + QSettings().setValue(AppSettingsKeys::cloudUserName(), QStringLiteral("Late User")); + + emit button.menu()->aboutToShow(); + QApplication::processEvents(); + + auto* signOut = findMenuAction(button, QStringLiteral("actionQtMeshCloudSignOut")); + ASSERT_NE(signOut, nullptr); + EXPECT_TRUE(signOut->isVisible()); + EXPECT_TRUE(menuListsHeader(button)); +} + +TEST_F(CloudAccountMenuButtonTest, SignedOutButtonRepaintsWithoutCrash) +{ + CloudAccountMenuButton button; + button.show(); button.refresh(); button.repaint(); QApplication::processEvents(); + saveTestSession(); + QSettings().setValue(AppSettingsKeys::cloudUserName(), QStringLiteral("Dev User")); + button.refresh(); + button.toolButton()->repaint(); + QApplication::processEvents(); + auto* subtitle = button.findChild(QStringLiteral("cloudAccountMenuHeaderSubtitle"), Qt::FindChildrenRecursively); ASSERT_NE(subtitle, nullptr); @@ -75,14 +285,14 @@ TEST_F(CloudAccountMenuButtonTest, SignedOutButtonRepaintsWithoutCrash) button.repaint(); QApplication::processEvents(); - bool headerListedInMenu = false; - for (QAction* action : button.menu()->actions()) { - auto* widgetAction = qobject_cast(action); - if (!widgetAction) - continue; - QWidget* widget = widgetAction->defaultWidget(); - if (widget && widget->objectName() == QStringLiteral("cloudAccountMenuHeader")) - headerListedInMenu = true; - } - EXPECT_FALSE(headerListedInMenu); + EXPECT_FALSE(menuListsHeader(button)); +} + +TEST_F(CloudAccountMenuButtonTest, ToolButtonAndMenuObjectNames) +{ + CloudAccountMenuButton button; + EXPECT_NE(button.toolButton(), nullptr); + EXPECT_EQ(button.toolButton()->objectName(), QStringLiteral("cloudAccountButton")); + EXPECT_NE(button.menu(), nullptr); + EXPECT_EQ(button.menu()->objectName(), QStringLiteral("menuCloud")); } diff --git a/src/CloudCredentialStore_test.cpp b/src/CloudCredentialStore_test.cpp index 370c5f1a5..954fef085 100644 --- a/src/CloudCredentialStore_test.cpp +++ b/src/CloudCredentialStore_test.cpp @@ -4,7 +4,11 @@ #include #include +#include +#include +#include #include +#include class CloudCredentialStoreTest : public ::testing::Test { protected: @@ -28,6 +32,12 @@ class CloudCredentialStoreTest : public ::testing::Test { QCoreApplication::setOrganizationName(previousOrganizationName); QCoreApplication::setApplicationName(previousApplicationName); } + + static QString sessionFilePath() + { + const QString dir = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation); + return dir + QStringLiteral("/cloud_session.dat"); + } }; TEST_F(CloudCredentialStoreTest, RoundTripAndClear) @@ -48,6 +58,51 @@ TEST_F(CloudCredentialStoreTest, RoundTripAndClear) EXPECT_FALSE(CloudCredentialStore::hasSession()); } +TEST_F(CloudCredentialStoreTest, SaveSessionRejectsEmptyToken) +{ + CloudSession session; + session.email = QStringLiteral("user@example.com"); + EXPECT_FALSE(CloudCredentialStore::saveSession(session)); + EXPECT_FALSE(CloudCredentialStore::hasSession()); +} + +TEST_F(CloudCredentialStoreTest, HasSessionFalseWhenCleared) +{ + EXPECT_FALSE(CloudCredentialStore::hasSession()); +} + +TEST_F(CloudCredentialStoreTest, LoadSessionReturnsEmptyForCorruptFile) +{ + const QString path = sessionFilePath(); + ASSERT_FALSE(path.isEmpty()); + + const QFileInfo info(path); + if (QDir dir = info.dir(); !dir.exists()) + ASSERT_TRUE(dir.mkpath(QStringLiteral("."))); + + QFile file(path); + ASSERT_TRUE(file.open(QIODevice::WriteOnly | QIODevice::Truncate)); + ASSERT_EQ(file.write("not-json"), 8); + file.close(); + + const CloudSession loaded = CloudCredentialStore::loadSession(); + EXPECT_FALSE(loaded.hasToken()); + EXPECT_TRUE(loaded.email.isEmpty()); +} + +TEST_F(CloudCredentialStoreTest, MigrateNoOpWhenNoLegacyToken) +{ + QSettings settings; + settings.setValue(AppSettingsKeys::cloudUserName(), QStringLiteral("still-here")); + settings.sync(); + + CloudCredentialStore::migrateLegacySettingsIfNeeded(); + + EXPECT_FALSE(CloudCredentialStore::hasSession()); + EXPECT_EQ(settings.value(AppSettingsKeys::cloudUserName()).toString(), + QStringLiteral("still-here")); +} + TEST_F(CloudCredentialStoreTest, MigratesLegacyPlaintextSettings) { QSettings settings; @@ -66,3 +121,16 @@ TEST_F(CloudCredentialStoreTest, MigratesLegacyPlaintextSettings) EXPECT_TRUE(settings.value(AppSettingsKeys::cloudTokenExpiresAt()).toString().isEmpty()); EXPECT_TRUE(settings.value(AppSettingsKeys::cloudUserEmail()).toString().isEmpty()); } + +TEST_F(CloudCredentialStoreTest, RoundTripWithoutEmail) +{ + CloudSession session; + session.token = QStringLiteral("token-only"); + session.expiresAt = 99; + ASSERT_TRUE(CloudCredentialStore::saveSession(session)); + + const CloudSession loaded = CloudCredentialStore::loadSession(); + EXPECT_EQ(loaded.token, session.token); + EXPECT_EQ(loaded.expiresAt, session.expiresAt); + EXPECT_TRUE(loaded.email.isEmpty()); +} diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index aceba0952..65fcbae2f 100755 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -2297,33 +2297,18 @@ void MainWindow::setupCloudAccountStatusControl() { m_cloudAccountControl = new CloudAccountMenuButton(this); - connect(m_cloudAccountControl, &CloudAccountMenuButton::signInRequested, this, [this]() { - SentryReporter::addBreadcrumb(QStringLiteral("ui.action"), - QStringLiteral("Cloud toolbar: Sign in")); - signInToQtMeshCloud(); - }); - connect(m_cloudAccountControl, &CloudAccountMenuButton::signOutRequested, this, [this]() { - SentryReporter::addBreadcrumb(QStringLiteral("ui.action"), - QStringLiteral("Cloud toolbar: Sign out")); - signOutOfQtMeshCloud(); - }); - connect(m_cloudAccountControl, &CloudAccountMenuButton::uploadFilesRequested, this, [this]() { - SentryReporter::addBreadcrumb(QStringLiteral("ui.action"), - QStringLiteral("Cloud toolbar: Upload Files")); - uploadFilesToQtMeshCloud(); - }); + connect(m_cloudAccountControl, &CloudAccountMenuButton::signInRequested, this, + &MainWindow::signInToQtMeshCloud); + connect(m_cloudAccountControl, &CloudAccountMenuButton::signOutRequested, this, + &MainWindow::signOutOfQtMeshCloud); + connect(m_cloudAccountControl, &CloudAccountMenuButton::uploadFilesRequested, this, + &MainWindow::uploadFilesToQtMeshCloud); connect(m_cloudAccountControl, &CloudAccountMenuButton::openProjectsRequested, this, [this]() { - SentryReporter::addBreadcrumb(QStringLiteral("ui.action"), - QStringLiteral("Cloud toolbar: Open My Projects")); if (!QDesktopServices::openUrl(QUrl(QStringLiteral(QTMESH_CLOUD_WEB_URL)))) { QMessageBox::warning(this, tr("QtMesh Cloud"), tr("Could not open QtMesh Cloud in your browser.")); } }); - connect(m_cloudAccountControl->menu(), &QMenu::aboutToShow, this, []() { - SentryReporter::addBreadcrumb(QStringLiteral("ui.action"), - QStringLiteral("Cloud toolbar menu opened")); - }); // Push the account control to the bottom of the left objects toolbar (VS Code-style). auto* toolbarStretch = new QWidget(ui->objectsToolbar); @@ -2413,6 +2398,8 @@ void MainWindow::signInToQtMeshCloud() const auto failSignIn = [&](const QString& message) { pollTimer.stop(); + if (!prompt.isVisible()) + return; QMessageBox::warning(this, tr("QtMesh Cloud Sign In"), message); prompt.reject(); }; @@ -2458,15 +2445,23 @@ void MainWindow::signInToQtMeshCloud() }; connect(copyButton, &QPushButton::clicked, this, [userCode = code.userCode]() { + SentryReporter::addBreadcrumb(QStringLiteral("ui.action"), + QStringLiteral("Cloud sign-in: Copy Code")); if (QApplication::clipboard()) QApplication::clipboard()->setText(userCode); }); connect(cancelButton, &QPushButton::clicked, &prompt, [&pollTimer, &prompt]() { + SentryReporter::addBreadcrumb(QStringLiteral("ui.action"), + QStringLiteral("Cloud sign-in: Cancel")); pollTimer.stop(); prompt.reject(); }); connect(openButton, &QPushButton::clicked, &prompt, [this, statusLabel, verificationUri = code.verificationUriComplete]() { + SentryReporter::addBreadcrumb(QStringLiteral("ui.action"), + QStringLiteral("Cloud sign-in: Open Browser")); if (!QDesktopServices::openUrl(QUrl(verificationUri))) { + SentryReporter::addBreadcrumb(QStringLiteral("ui.action"), + QStringLiteral("Cloud sign-in: Open Browser failed")); QMessageBox::warning(this, tr("QtMesh Cloud"), tr("Could not open QtMesh Cloud in your browser.")); return; @@ -2479,7 +2474,7 @@ void MainWindow::signInToQtMeshCloud() prompt.resize(420, prompt.sizeHint().height()); pollTimer.start(intervalMs); - pollOnce(); + QTimer::singleShot(0, &prompt, pollOnce); prompt.exec(); pollTimer.stop(); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 6f5b8dc6d..46efc390d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -503,6 +503,13 @@ if(BUILD_TESTS) "${CMAKE_CURRENT_SOURCE_DIR}/../src/MaterialEditorQML_perf_test.cpp" ) endif() + + # Cloud account toolbar widget tests + if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/../src/CloudAccountMenuButton_test.cpp") + create_test_executable(CloudAccountMenuButton_test + "${CMAKE_CURRENT_SOURCE_DIR}/../src/CloudAccountMenuButton_test.cpp" + ) + endif() # 4. QML Component Test Runner (existing) - special handling for CI create_test_executable_no_autotest(MaterialEditorQML_qml_test_runner @@ -539,6 +546,10 @@ if(BUILD_TESTS) if(TARGET MaterialEditorQML_perf_test) gtest_discover_tests(MaterialEditorQML_perf_test DISCOVERY_MODE PRE_TEST) endif() + + if(TARGET CloudAccountMenuButton_test) + gtest_discover_tests(CloudAccountMenuButton_test DISCOVERY_MODE PRE_TEST) + endif() if(TARGET MaterialEditorQML_qml_test_runner) # Don't use gtest_discover_tests for QML runner to avoid CI issues diff --git a/website/src/hooks/useQtmeshActionRef.js b/website/src/hooks/useQtmeshActionRef.js index 51b31d911..8cc2105fe 100644 --- a/website/src/hooks/useQtmeshActionRef.js +++ b/website/src/hooks/useQtmeshActionRef.js @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; const QTMESH_RELEASES_LATEST_API = 'https://api.github.com/repos/fernandotonon/QtMeshEditor/releases/latest'; -const QTMESH_ACTION_REF_FALLBACK = 'fernandotonon/QtMeshEditor@3.3.1'; +const QTMESH_ACTION_REF_FALLBACK = 'fernandotonon/QtMeshEditor@3.4.0'; const CACHE_KEY = 'qtmesh.actionRef.cache.v1'; const CACHE_TTL_MS = 6 * 60 * 60 * 1000;