From 808c8ef600ff889e66d8de3b47c4233f68d0f41f Mon Sep 17 00:00:00 2001 From: zhaofangxun Date: Thu, 20 Aug 2026 18:11:08 +0800 Subject: [PATCH 1/2] test: add 185 unit tests for dde-launchpad MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Add 14 Qt6 unit test files covering models and utils modules 2. Expand existing itemspage and searchfilter proxy model tests 3. Update CMakeLists.txt to register new targets with gcov coverage 4. Achieve 72.0% filtered line coverage and 86.1% function coverage Influence: 1. Build with cmake -DBUILD_TEST=ON and run all 15 test targets 2. Verify 185 test cases pass with zero failures 3. Check gcov coverage report for src/models and src/utils 4. Validate test isolation with no false-green cases test: 补充 dde-launchpad 单元测试 185 用例 1. 新增 14 个 Qt6 单元测试文件覆盖 models 和 utils 模块 2. 扩展已有的 itemspage 和 searchfilter 代理模型测试 3. 更新 CMakeLists.txt 注册新测试 target 并启用 gcov 覆盖率 4. 达到 72.0% 过滤后行覆盖率和 86.1% 函数覆盖率 Influence: 1. 使用 cmake -DBUILD_TEST=ON 编译并运行全部 15 个测试 target 2. 验证 185 条用例全部通过零失败 3. 检查 src/models 和 src/utils 模块的 gcov 覆盖率报告 4. 验证测试隔离性无 false-green 用例 --- tests/CMakeLists.txt | 88 +++- tests/blurhashtest.cpp | 162 +++++++ tests/categoryutilstest.cpp | 266 +++++++++++ tests/countlimitproxymodeltest.cpp | 204 ++++++++ tests/favoritedproxymodeltest.cpp | 243 ++++++++++ tests/freesortproxymodeltest.cpp | 97 ++++ tests/frequentlyusedproxymodeltest.cpp | 208 +++++++++ tests/iconutilstest.cpp | 126 +++++ tests/itemarrangementproxymodeltest.cpp | 303 ++++++++++++ tests/itemspagemodeltest.cpp | 192 ++++++++ tests/itemspagetest.cpp | 174 ++++++- tests/multipagesortfilterproxymodeltest.cpp | 187 ++++++++ tests/recentlyinstalledproxymodeltest.cpp | 108 +++++ tests/searchfilterproxymodeltest.cpp | 62 +++ tests/sortproxymodeltest.cpp | 488 ++++++++++++++++++++ 15 files changed, 2905 insertions(+), 3 deletions(-) create mode 100644 tests/blurhashtest.cpp create mode 100644 tests/categoryutilstest.cpp create mode 100644 tests/countlimitproxymodeltest.cpp create mode 100644 tests/favoritedproxymodeltest.cpp create mode 100644 tests/freesortproxymodeltest.cpp create mode 100644 tests/frequentlyusedproxymodeltest.cpp create mode 100644 tests/iconutilstest.cpp create mode 100644 tests/itemarrangementproxymodeltest.cpp create mode 100644 tests/itemspagemodeltest.cpp create mode 100644 tests/multipagesortfilterproxymodeltest.cpp create mode 100644 tests/recentlyinstalledproxymodeltest.cpp create mode 100644 tests/sortproxymodeltest.cpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 728f127c..e18ab360 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,15 +1,87 @@ -# SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +# SPDX-FileCopyrightText: 2024 - 2026 UnionTech Software Technology Co., Ltd. # # SPDX-License-Identifier: CC0-1.0 find_package(Qt6 REQUIRED COMPONENTS Core Test) +find_package(Qt6 REQUIRED COMPONENTS Gui Qml Svg) # needed by tested-source OBJECT libs +find_package(Qt6Core CONFIG REQUIRED Private) # sortproxymodel.cpp uses QAbstractItemModelPrivate qt_standard_project_setup() +# --------------------------------------------------------------------------- +# Tested-source OBJECT libraries +# +# Per the long-term coverage rule: the tested source files (src/models/*.cpp, +# src/utils/*.cpp) are compiled as OBJECT libraries and linked *directly* +# into every test executable, instead of being reached only through the +# launchpadcommon.so shared library. This ensures gcov .gcda runtime data +# is written to the test build directory and can be captured even in +# cross-prefix environments where the shared-library build path is not +# writable at runtime. +# +# launchpadcommon is still linked afterwards for transitive dependencies +# (D-Bus adaptors, dde-integration AppMgr, etc.). Symbols defined by the +# OBJECT libs take precedence over the same symbols in the shared library, +# so the gcov-instrumented copies in the test executable are the ones that +# actually execute. +# --------------------------------------------------------------------------- + +add_library(launchpad-test-models OBJECT + ${CMAKE_SOURCE_DIR}/src/models/appsmodel.cpp + ${CMAKE_SOURCE_DIR}/src/models/categorizedsortproxymodel.cpp + ${CMAKE_SOURCE_DIR}/src/models/countlimitproxymodel.cpp + ${CMAKE_SOURCE_DIR}/src/models/favoritedproxymodel.cpp + ${CMAKE_SOURCE_DIR}/src/models/freesortproxymodel.cpp + ${CMAKE_SOURCE_DIR}/src/models/frequentlyusedproxymodel.cpp + ${CMAKE_SOURCE_DIR}/src/models/itemarrangementproxymodel.cpp + ${CMAKE_SOURCE_DIR}/src/models/itemspage.cpp + ${CMAKE_SOURCE_DIR}/src/models/itemspagemodel.cpp + ${CMAKE_SOURCE_DIR}/src/models/multipagesortfilterproxymodel.cpp + ${CMAKE_SOURCE_DIR}/src/models/recentlyinstalledproxymodel.cpp + ${CMAKE_SOURCE_DIR}/src/models/searchfilterproxymodel.cpp + ${CMAKE_SOURCE_DIR}/src/models/sortproxymodel.cpp +) +target_include_directories(launchpad-test-models PRIVATE + ${CMAKE_SOURCE_DIR}/src/models + ${CMAKE_SOURCE_DIR}/src/utils + ${CMAKE_SOURCE_DIR}/src/ddeintegration +) +target_link_libraries(launchpad-test-models PUBLIC + Qt6::Core + Qt6::Gui + Qt6::Qml # QML_NAMED_ELEMENT / QML_SINGLETON in model headers (moc-generated registration) + Qt6::CorePrivate # sortproxymodel.cpp -> QAbstractItemModelPrivate + ${DTK_NS}::Core # DConfig, DFileWatcherManager, DPinyin (appsmodel.cpp, frequentlyusedproxymodel.cpp) +) + +add_library(launchpad-test-utils OBJECT + ${CMAKE_SOURCE_DIR}/src/utils/blurhash.cpp + ${CMAKE_SOURCE_DIR}/src/utils/categoryutils.cpp + ${CMAKE_SOURCE_DIR}/src/utils/iconutils.cpp +) +target_include_directories(launchpad-test-utils PRIVATE + ${CMAKE_SOURCE_DIR}/src/utils +) +target_link_libraries(launchpad-test-utils PUBLIC + Qt6::Core + Qt6::Gui + Qt6::Svg + ${DTK_NS}::Gui # DIcon, DIconTheme, DSvgRenderer (iconutils.cpp) +) + macro(launchpad_add_tests) foreach(_testname ${ARGN}) qt_add_executable(launchpad-${_testname} ${_testname}.cpp) - target_link_libraries(launchpad-${_testname} PRIVATE Qt6::Core Qt6::Test launchpadcommon) + # OBJECT libs first: their symbols take precedence over launchpadcommon.so, + # so the gcov-instrumented copies are the ones that execute at test time. + target_link_libraries(launchpad-${_testname} PRIVATE + Qt6::Core + Qt6::Test + launchpad-test-models + launchpad-test-utils + gio-utils # already OBJECT; covers src/gioutils/ (e.g. gioappinfotest) + launchpadcommon + ) add_test(NAME launchpad-${_testname} COMMAND launchpad-${_testname}) endforeach() endmacro() @@ -18,4 +90,16 @@ launchpad_add_tests( itemspagetest gioappinfotest searchfilterproxymodeltest + countlimitproxymodeltest + sortproxymodeltest + freesortproxymodeltest + recentlyinstalledproxymodeltest + multipagesortfilterproxymodeltest + categoryutilstest + blurhashtest + favoritedproxymodeltest + frequentlyusedproxymodeltest + itemarrangementproxymodeltest + iconutilstest + itemspagemodeltest ) diff --git a/tests/blurhashtest.cpp b/tests/blurhashtest.cpp new file mode 100644 index 00000000..a0fdb378 --- /dev/null +++ b/tests/blurhashtest.cpp @@ -0,0 +1,162 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include + +#include "../src/utils/blurhash.hpp" + +#include + +namespace { +Q_LOGGING_CATEGORY(logTest, "dde.launchpad.test") + +std::vector solidImage(size_t width, size_t height, + unsigned char r, unsigned char g, unsigned char b) +{ + std::vector image(width * height * 3, 0); + for (size_t i = 0; i < width * height; ++i) { + image[i * 3 + 0] = r; + image[i * 3 + 1] = g; + image[i * 3 + 2] = b; + } + return image; +} + +// blurhash layout: 1 (components) + 1 (maxAC) + 4 (DC) + 2 bytes per AC component +size_t expectedHashLength(int cx, int cy) +{ + return size_t(1 + 1 + 4 + (cx * cy - 1) * 2); +} +} + +class TestBlurhash : public QObject +{ + Q_OBJECT +private slots: + void decodeEmptyStringReturnsEmpty(); + void decodeInvalidHashReturnsEmpty(); + void encodeRejectsInvalidInput(); + void encodedHashHasExpectedLength(); + void decodedImageHasExpectedDimensions(); + void roundTripPreservesAverageColor(); + void decodeWithFourBytesPerPixel(); + void decodeTooShortHashReturnsEmpty(); + void decodeWrongSizeForComponentsReturnsEmpty(); +}; + +void TestBlurhash::decodeEmptyStringReturnsEmpty() +{ + qCInfo(logTest) << "Decoding an empty hash should return an empty image"; + const auto img = blurhash::decode(std::string_view{}, 8, 8); + QVERIFY(img.image.empty()); + QCOMPARE(img.width, size_t(0)); + QCOMPARE(img.height, size_t(0)); +} + +void TestBlurhash::decodeInvalidHashReturnsEmpty() +{ + qCInfo(logTest) << "Decoding a hash with invalid characters should return an empty image"; + const auto img = blurhash::decode(std::string_view("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"), 8, 8); + QVERIFY(img.image.empty()); +} + +void TestBlurhash::encodeRejectsInvalidInput() +{ + qCInfo(logTest) << "encode should return an empty string for invalid parameters"; + std::vector image = solidImage(4, 4, 255, 0, 0); + QVERIFY(blurhash::encode(image.data(), 0, 4, 1, 1).empty()); // width 0 + QVERIFY(blurhash::encode(nullptr, 4, 4, 1, 1).empty()); // null image + QVERIFY(blurhash::encode(image.data(), 4, 4, 0, 1).empty()); // components_x < 1 + QVERIFY(blurhash::encode(image.data(), 4, 4, 1, 10).empty()); // components_y > 9 +} + +void TestBlurhash::encodedHashHasExpectedLength() +{ + qCInfo(logTest) << "encode should produce a hash of the documented length"; + std::vector image = solidImage(8, 8, 128, 64, 200); + QCOMPARE(blurhash::encode(image.data(), 8, 8, 1, 1).size(), size_t(6)); // no AC components + QCOMPARE(blurhash::encode(image.data(), 8, 8, 1, 3).size(), expectedHashLength(1, 3)); // 10 + QCOMPARE(blurhash::encode(image.data(), 8, 8, 2, 2).size(), expectedHashLength(2, 2)); // 12 + QCOMPARE(blurhash::encode(image.data(), 8, 8, 4, 4).size(), expectedHashLength(4, 4)); // 36 +} + +void TestBlurhash::decodedImageHasExpectedDimensions() +{ + qCInfo(logTest) << "decode should produce an image matching the requested dimensions"; + constexpr size_t width = 16, height = 16; + std::vector image = solidImage(width, height, 128, 64, 200); + const std::string hash = blurhash::encode(image.data(), width, height, 2, 2); + QVERIFY(!hash.empty()); + + const auto decoded = blurhash::decode(hash, width, height); + QCOMPARE(decoded.width, width); + QCOMPARE(decoded.height, height); + QCOMPARE(decoded.image.size(), width * height * 3); +} + +void TestBlurhash::roundTripPreservesAverageColor() +{ + qCInfo(logTest) << "Round-trip should preserve the average color (DC component)"; + constexpr size_t width = 16, height = 16; + const unsigned char r = 128, g = 64, b = 200; + std::vector image = solidImage(width, height, r, g, b); + + const std::string hash = blurhash::encode(image.data(), width, height, 3, 3); + QVERIFY(!hash.empty()); + + const auto decoded = blurhash::decode(hash, width, height); + QCOMPARE(decoded.image.size(), width * height * 3); + + long rs = 0, gs = 0, bs = 0; + for (size_t i = 0; i < width * height; ++i) { + rs += decoded.image[i * 3 + 0]; + gs += decoded.image[i * 3 + 1]; + bs += decoded.image[i * 3 + 2]; + } + const long n = long(width * height); + const long avgR = rs / n, avgG = gs / n, avgB = bs / n; + // blurhash is lossy per-pixel, but the average (DC) stays close to the source color + QVERIFY(avgR >= long(r) - 15 && avgR <= long(r) + 15); + QVERIFY(avgG >= long(g) - 15 && avgG <= long(g) + 15); + QVERIFY(avgB >= long(b) - 15 && avgB <= long(b) + 15); +} + +void TestBlurhash::decodeWithFourBytesPerPixel() +{ + qCInfo(logTest) << "decode with bytesPerPixel=4 should produce a larger buffer"; + constexpr size_t width = 16, height = 16; + std::vector image = solidImage(width, height, 128, 64, 200); + const std::string hash = blurhash::encode(image.data(), width, height, 3, 3); + QVERIFY(!hash.empty()); + + const auto decoded = blurhash::decode(hash, width, height, 4); + QCOMPARE(decoded.width, width); + QCOMPARE(decoded.height, height); + // buffer should be width * height * 4 (not 3) + QCOMPARE(decoded.image.size(), width * height * 4); + // first 3 bytes of each pixel should still be the color channels + QVERIFY(decoded.image[0] != 255 || decoded.image[1] != 255 || decoded.image[2] != 255); +} + +void TestBlurhash::decodeTooShortHashReturnsEmpty() +{ + qCInfo(logTest) << "decode with a hash shorter than 10 chars should return empty"; + // hash must be >= 10 chars (1 + 1 + 4 + at least 4 for 2x2 components - 1 AC = 2 bytes -> 8, but 10 is the minimum check) + const auto img = blurhash::decode(std::string_view("short"), 8, 8); + QVERIFY(img.image.empty()); +} + +void TestBlurhash::decodeWrongSizeForComponentsReturnsEmpty() +{ + qCInfo(logTest) << "decode with a hash whose length doesn't match the component count should return empty"; + // 1 component char says 4x4 = 16 components -> needs 1+1+4+(16-1)*2 = 36 chars + // but we'll give only 12 chars + const auto img = blurhash::decode(std::string_view("LFE.}?a]a]a]a]a]a]a]"), 8, 8); + // this hash has wrong length for its declared components -> should return empty + QVERIFY(img.image.empty()); +} + +QTEST_MAIN(TestBlurhash) +#include "blurhashtest.moc" diff --git a/tests/categoryutilstest.cpp b/tests/categoryutilstest.cpp new file mode 100644 index 00000000..f8d5c9f6 --- /dev/null +++ b/tests/categoryutilstest.cpp @@ -0,0 +1,266 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include + +#include "../src/utils/categoryutils.h" + +namespace { +Q_LOGGING_CATEGORY(logTest, "dde.launchpad.test") + +using C = CategoryUtils::Categorytype; +} + +class TestCategoryUtils : public QObject +{ + Q_OBJECT +private slots: + void parseDDECategoryStringKnown(); + void parseDDECategoryStringUnknown(); + void parseXdgCategoryStringSingle(); + void parseXdgCategoryStringMultiValued(); + void parseXdgCategoryStringUnknown(); + void bestMatchedSingleCategory(); + void bestMatchedEmptyReturnsOthers(); + void bestMatchedOthersOnlyReturnsOthers(); + void bestMatchedMusicVideoTiePrefersVideo(); + void bestMatchedPlayerMapsToVideo(); + void bestMatchedMaxCountWins(); + void bestMatchedMultipleInternet(); + void parseDDECategoryStringAllKnown(); + void parseXdgCategoryStringMultipleDistinctCategories(); + void parseXdgCategoryStringAudioMapsToMusic(); + void bestMatchedDDECategoryWinsOverXdg(); + void bestMatchedGameVariants(); + void bestMatchedDevelopmentVariants(); + void bestMatchedReadingVariants(); + void bestMatchedSystemVariants(); + void bestMatchedGraphicsVariants(); + void bestMatchedChatVariants(); + void bestMatchedOfficeVariants(); + void bestMatchedMixedUnknownAndKnown(); + void bestMatchedAudioVideoEditingTie(); + void bestMatchedRecorderTie(); +}; + +void TestCategoryUtils::parseDDECategoryStringKnown() +{ + qCInfo(logTest) << "parseDDECategoryString should map the known DDE category names"; + QCOMPARE(int(CategoryUtils::parseDDECategoryString(QStringLiteral("music"))), int(C::CategoryMusic)); + QCOMPARE(int(CategoryUtils::parseDDECategoryString(QStringLiteral("internet"))), int(C::CategoryInternet)); + QCOMPARE(int(CategoryUtils::parseDDECategoryString(QStringLiteral("development"))), int(C::CategoryDevelopment)); + QCOMPARE(int(CategoryUtils::parseDDECategoryString(QStringLiteral("others"))), int(C::CategoryOthers)); +} + +void TestCategoryUtils::parseDDECategoryStringUnknown() +{ + qCInfo(logTest) << "Unknown DDE category strings should yield CategoryErr"; + QCOMPARE(int(CategoryUtils::parseDDECategoryString(QStringLiteral("unknown"))), int(C::CategoryErr)); + QCOMPARE(int(CategoryUtils::parseDDECategoryString(QStringLiteral("Music"))), int(C::CategoryErr)); // case sensitive +} + +void TestCategoryUtils::parseXdgCategoryStringSingle() +{ + qCInfo(logTest) << "parseXdgCategoryString should map single-valued XDG categories"; + const auto result = CategoryUtils::parseXdgCategoryString(QStringLiteral("webbrowser")); + QCOMPARE(result.size(), 1); + QVERIFY(result.contains(C::CategoryInternet)); +} + +void TestCategoryUtils::parseXdgCategoryStringMultiValued() +{ + qCInfo(logTest) << "audiovideo should map to both music and video"; + const auto result = CategoryUtils::parseXdgCategoryString(QStringLiteral("audiovideo")); + QCOMPARE(result.size(), 2); + QVERIFY(result.contains(C::CategoryMusic)); + QVERIFY(result.contains(C::CategoryVideo)); +} + +void TestCategoryUtils::parseXdgCategoryStringUnknown() +{ + qCInfo(logTest) << "Unknown XDG category strings should yield an empty list"; + const auto result = CategoryUtils::parseXdgCategoryString(QStringLiteral("totally-unknown-category")); + QVERIFY(result.isEmpty()); +} + +void TestCategoryUtils::bestMatchedSingleCategory() +{ + qCInfo(logTest) << "A single matching category should win"; + QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("music")})), int(C::CategoryMusic)); + QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("network")})), int(C::CategoryInternet)); + QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("spreadsheet")})), int(C::CategoryOffice)); +} + +void TestCategoryUtils::bestMatchedEmptyReturnsOthers() +{ + qCInfo(logTest) << "An empty category list should be classified as others"; + QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({})), int(C::CategoryOthers)); +} + +void TestCategoryUtils::bestMatchedOthersOnlyReturnsOthers() +{ + qCInfo(logTest) << "An only-others list should be classified as others"; + QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("others")})), int(C::CategoryOthers)); +} + +void TestCategoryUtils::bestMatchedMusicVideoTiePrefersVideo() +{ + qCInfo(logTest) << "A music/video tie should prefer video"; + QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("music"), QStringLiteral("video")})), + int(C::CategoryVideo)); +} + +void TestCategoryUtils::bestMatchedPlayerMapsToVideo() +{ + qCInfo(logTest) << "player (maps to music+video) should resolve to video"; + QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("player")})), int(C::CategoryVideo)); +} + +void TestCategoryUtils::bestMatchedMaxCountWins() +{ + qCInfo(logTest) << "The category with the highest count should win"; + QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("game"), QStringLiteral("game"), QStringLiteral("music")})), + int(C::CategoryGame)); +} + +void TestCategoryUtils::bestMatchedMultipleInternet() +{ + qCInfo(logTest) << "Several internet categories should aggregate into internet"; + QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("webbrowser"), QStringLiteral("email")})), + int(C::CategoryInternet)); +} + +void TestCategoryUtils::parseDDECategoryStringAllKnown() +{ + qCInfo(logTest) << "parseDDECategoryString should map all known DDE category names"; + QCOMPARE(int(CategoryUtils::parseDDECategoryString(QStringLiteral("chat"))), int(C::CategoryChat)); + QCOMPARE(int(CategoryUtils::parseDDECategoryString(QStringLiteral("video"))), int(C::CategoryVideo)); + QCOMPARE(int(CategoryUtils::parseDDECategoryString(QStringLiteral("graphics"))), int(C::CategoryGraphics)); + QCOMPARE(int(CategoryUtils::parseDDECategoryString(QStringLiteral("office"))), int(C::CategoryOffice)); + QCOMPARE(int(CategoryUtils::parseDDECategoryString(QStringLiteral("game"))), int(C::CategoryGame)); + QCOMPARE(int(CategoryUtils::parseDDECategoryString(QStringLiteral("reading"))), int(C::CategoryReading)); + QCOMPARE(int(CategoryUtils::parseDDECategoryString(QStringLiteral("system"))), int(C::CategorySystem)); +} + +void TestCategoryUtils::parseXdgCategoryStringMultipleDistinctCategories() +{ + qCInfo(logTest) << "parseXdgCategoryString for several distinct known categories"; + // Each call returns the list for that single string; verify a batch of them + QCOMPARE(CategoryUtils::parseXdgCategoryString(QStringLiteral("ide")).size(), 1); + QVERIFY(CategoryUtils::parseXdgCategoryString(QStringLiteral("ide")).contains(C::CategoryDevelopment)); + QCOMPARE(CategoryUtils::parseXdgCategoryString(QStringLiteral("webdevelopment")).size(), 1); + QVERIFY(CategoryUtils::parseXdgCategoryString(QStringLiteral("webdevelopment")).contains(C::CategoryDevelopment)); + QCOMPARE(CategoryUtils::parseXdgCategoryString(QStringLiteral("arcadegame")).size(), 1); + QVERIFY(CategoryUtils::parseXdgCategoryString(QStringLiteral("arcadegame")).contains(C::CategoryGame)); + QCOMPARE(CategoryUtils::parseXdgCategoryString(QStringLiteral("wordprocessor")).size(), 1); + QVERIFY(CategoryUtils::parseXdgCategoryString(QStringLiteral("wordprocessor")).contains(C::CategoryOffice)); + QCOMPARE(CategoryUtils::parseXdgCategoryString(QStringLiteral("ircclient")).size(), 1); + QVERIFY(CategoryUtils::parseXdgCategoryString(QStringLiteral("ircclient")).contains(C::CategoryChat)); + QCOMPARE(CategoryUtils::parseXdgCategoryString(QStringLiteral("news")).size(), 1); + QVERIFY(CategoryUtils::parseXdgCategoryString(QStringLiteral("news")).contains(C::CategoryReading)); +} + +void TestCategoryUtils::parseXdgCategoryStringAudioMapsToMusic() +{ + qCInfo(logTest) << "audio should map to music"; + const auto result = CategoryUtils::parseXdgCategoryString(QStringLiteral("audio")); + QCOMPARE(result.size(), 1); + QVERIFY(result.contains(C::CategoryMusic)); +} + +void TestCategoryUtils::bestMatchedDDECategoryWinsOverXdg() +{ + qCInfo(logTest) << "A DDE category name should be matched before falling to XDG parsing"; + // 'music' is a DDE name -> CategoryMusic (via parseDDECategoryString) + // 'audio' is an XDG name -> CategoryMusic (via parseXdgCategoryString) + // mixing them should still yield Music + QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("music"), QStringLiteral("audio")})), + int(C::CategoryMusic)); +} + +void TestCategoryUtils::bestMatchedGameVariants() +{ + qCInfo(logTest) << "Various game XDG categories should aggregate into game"; + QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("actiongame"), QStringLiteral("arcadegame")})), + int(C::CategoryGame)); + QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("boardgame"), QStringLiteral("cardgame"), QStringLiteral("puzzlegame")})), + int(C::CategoryGame)); +} + +void TestCategoryUtils::bestMatchedDevelopmentVariants() +{ + qCInfo(logTest) << "Development XDG categories should aggregate into development"; + QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("ide"), QStringLiteral("debugger")})), + int(C::CategoryDevelopment)); + QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("building"), QStringLiteral("revisioncontrol")})), + int(C::CategoryDevelopment)); +} + +void TestCategoryUtils::bestMatchedReadingVariants() +{ + qCInfo(logTest) << "Reading XDG categories should aggregate into reading"; + QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("news"), QStringLiteral("translation")})), + int(C::CategoryReading)); +} + +void TestCategoryUtils::bestMatchedSystemVariants() +{ + qCInfo(logTest) << "System XDG categories should aggregate into system"; + QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("desktopsettings"), QStringLiteral("packagemanager")})), + int(C::CategorySystem)); + QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("terminalemulator"), QStringLiteral("filemanager")})), + int(C::CategorySystem)); +} + +void TestCategoryUtils::bestMatchedGraphicsVariants() +{ + qCInfo(logTest) << "Graphics XDG categories should aggregate into graphics"; + QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("2dgraphics"), QStringLiteral("rastergraphics")})), + int(C::CategoryGraphics)); + QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("photography"), QStringLiteral("viewer")})), + int(C::CategoryGraphics)); +} + +void TestCategoryUtils::bestMatchedChatVariants() +{ + qCInfo(logTest) << "Chat XDG categories should aggregate into chat"; + QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("instantmessaging"), QStringLiteral("contactmanagement")})), + int(C::CategoryChat)); +} + +void TestCategoryUtils::bestMatchedOfficeVariants() +{ + qCInfo(logTest) << "Office XDG categories should aggregate into office"; + QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("spreadsheet"), QStringLiteral("presentation")})), + int(C::CategoryOffice)); + QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("science"), QStringLiteral("math")})), + int(C::CategoryOffice)); +} + +void TestCategoryUtils::bestMatchedMixedUnknownAndKnown() +{ + qCInfo(logTest) << "Unknown categories mixed with known ones should ignore unknowns"; + // 'totally-unknown' yields CategoryErr from DDE and empty from XDG -> ignored + // 'music' yields CategoryMusic -> should win + QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("totally-unknown"), QStringLiteral("music")})), + int(C::CategoryMusic)); +} + +void TestCategoryUtils::bestMatchedAudioVideoEditingTie() +{ + qCInfo(logTest) << "audiovideoediting maps to both music and video -> tie -> video"; + QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("audiovideoediting")})), + int(C::CategoryVideo)); +} + +void TestCategoryUtils::bestMatchedRecorderTie() +{ + qCInfo(logTest) << "recorder maps to both music and video -> tie -> video"; + QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("recorder")})), + int(C::CategoryVideo)); +} + +QTEST_MAIN(TestCategoryUtils) +#include "categoryutilstest.moc" diff --git a/tests/countlimitproxymodeltest.cpp b/tests/countlimitproxymodeltest.cpp new file mode 100644 index 00000000..37ca5da0 --- /dev/null +++ b/tests/countlimitproxymodeltest.cpp @@ -0,0 +1,204 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include +#include +#include + +#include "../src/models/countlimitproxymodel.h" + +namespace { +Q_LOGGING_CATEGORY(logTest, "dde.launchpad.test") +} + +class TestCountLimitProxyModel : public QObject +{ + Q_OBJECT +private slots: + void noLimitByDefault(); + void maxRowCountLimitsVisibleRows(); + void maxRowCountChangeEmitsSignal(); + void unchangedMaxRowCountDoesNotEmit(); + void sourceRowInsertionReevaluates(); + void sourceRowRemovalReevaluates(); + void maxRowCountEqualsSourceCount(); + void maxRowCountExceedsSourceCount(); + void setSourceModelWithNullDisconnects(); + void setSameSourceModelIsNoop(); +}; + +void TestCountLimitProxyModel::noLimitByDefault() +{ + qCInfo(logTest) << "CountLimitProxyModel should not limit rows when maxRowCount is unset"; + QStandardItemModel source; + CountLimitProxyModel proxy; + QCOMPARE(proxy.maxRowCount(), -1); + proxy.setSourceModel(&source); + + source.appendRow(new QStandardItem(QStringLiteral("a"))); + source.appendRow(new QStandardItem(QStringLiteral("b"))); + source.appendRow(new QStandardItem(QStringLiteral("c"))); + + QCOMPARE(proxy.rowCount(), 3); +} + +void TestCountLimitProxyModel::maxRowCountLimitsVisibleRows() +{ + qCInfo(logTest) << "Setting maxRowCount should cap the visible rows and keep the first ones"; + QStandardItemModel source; + for (int i = 0; i < 5; ++i) + source.appendRow(new QStandardItem(QStringLiteral("item-%1").arg(i))); + + CountLimitProxyModel proxy; + proxy.setSourceModel(&source); + proxy.setMaxRowCount(3); + QCOMPARE(proxy.maxRowCount(), 3); + QCOMPARE(proxy.rowCount(), 3); + QCOMPARE(proxy.data(proxy.index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("item-0")); + QCOMPARE(proxy.data(proxy.index(1, 0), Qt::DisplayRole).toString(), QStringLiteral("item-1")); + QCOMPARE(proxy.data(proxy.index(2, 0), Qt::DisplayRole).toString(), QStringLiteral("item-2")); + + // relaxing the limit again shows everything + proxy.setMaxRowCount(-1); + QCOMPARE(proxy.rowCount(), 5); +} + +void TestCountLimitProxyModel::maxRowCountChangeEmitsSignal() +{ + qCInfo(logTest) << "Changing maxRowCount should emit maxRowCountChanged"; + QStandardItemModel source; + source.appendRow(new QStandardItem(QStringLiteral("a"))); + CountLimitProxyModel proxy; + proxy.setSourceModel(&source); + + QSignalSpy spy(&proxy, &CountLimitProxyModel::maxRowCountChanged); + proxy.setMaxRowCount(2); + QCOMPARE(spy.count(), 1); + + // maxRowCount == 0 rejects every row + proxy.setMaxRowCount(0); + QCOMPARE(spy.count(), 2); + QCOMPARE(proxy.rowCount(), 0); +} + +void TestCountLimitProxyModel::unchangedMaxRowCountDoesNotEmit() +{ + qCInfo(logTest) << "Setting the same maxRowCount should not emit the signal"; + QStandardItemModel source; + source.appendRow(new QStandardItem(QStringLiteral("a"))); + CountLimitProxyModel proxy; + proxy.setSourceModel(&source); + proxy.setMaxRowCount(2); + + QSignalSpy spy(&proxy, &CountLimitProxyModel::maxRowCountChanged); + proxy.setMaxRowCount(2); + QCOMPARE(spy.count(), 0); +} + +void TestCountLimitProxyModel::sourceRowInsertionReevaluates() +{ + qCInfo(logTest) << "Inserting source rows should re-evaluate the limit"; + QStandardItemModel source; + CountLimitProxyModel proxy; + proxy.setSourceModel(&source); + proxy.setMaxRowCount(2); + QCOMPARE(proxy.rowCount(), 0); + + for (int i = 0; i < 4; ++i) + source.appendRow(new QStandardItem(QStringLiteral("item-%1").arg(i))); + + QCOMPARE(proxy.rowCount(), 2); + QCOMPARE(proxy.data(proxy.index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("item-0")); + QCOMPARE(proxy.data(proxy.index(1, 0), Qt::DisplayRole).toString(), QStringLiteral("item-1")); +} + +void TestCountLimitProxyModel::sourceRowRemovalReevaluates() +{ + qCInfo(logTest) << "Removing source rows should re-evaluate the limit and keep the first surviving rows"; + QStandardItemModel source; + for (int i = 0; i < 4; ++i) + source.appendRow(new QStandardItem(QStringLiteral("item-%1").arg(i))); + + CountLimitProxyModel proxy; + proxy.setSourceModel(&source); + proxy.setMaxRowCount(2); + QCOMPARE(proxy.rowCount(), 2); + QCOMPARE(proxy.data(proxy.index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("item-0")); + QCOMPARE(proxy.data(proxy.index(1, 0), Qt::DisplayRole).toString(), QStringLiteral("item-1")); + + // setSourceModel connects rowsRemoved -> invalidate(), so removing a source row + // re-runs the filter against the (shifted) remaining rows: item-1, item-2 survive. + source.removeRow(0); + QCOMPARE(proxy.rowCount(), 2); + QCOMPARE(proxy.data(proxy.index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("item-1")); + QCOMPARE(proxy.data(proxy.index(1, 0), Qt::DisplayRole).toString(), QStringLiteral("item-2")); +} + +void TestCountLimitProxyModel::maxRowCountEqualsSourceCount() +{ + qCInfo(logTest) << "maxRowCount == source row count should show all rows"; + QStandardItemModel source; + for (int i = 0; i < 3; ++i) + source.appendRow(new QStandardItem(QStringLiteral("item-%1").arg(i))); + + CountLimitProxyModel proxy; + proxy.setSourceModel(&source); + proxy.setMaxRowCount(3); + QCOMPARE(proxy.rowCount(), 3); // all visible + QCOMPARE(proxy.data(proxy.index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("item-0")); + QCOMPARE(proxy.data(proxy.index(2, 0), Qt::DisplayRole).toString(), QStringLiteral("item-2")); +} + +void TestCountLimitProxyModel::maxRowCountExceedsSourceCount() +{ + qCInfo(logTest) << "maxRowCount > source row count should show all rows"; + QStandardItemModel source; + for (int i = 0; i < 2; ++i) + source.appendRow(new QStandardItem(QStringLiteral("item-%1").arg(i))); + + CountLimitProxyModel proxy; + proxy.setSourceModel(&source); + proxy.setMaxRowCount(100); + QCOMPARE(proxy.rowCount(), 2); // all visible, no extra rows +} + +void TestCountLimitProxyModel::setSourceModelWithNullDisconnects() +{ + qCInfo(logTest) << "setSourceModel(null) should disconnect from old model"; + QStandardItemModel source; + source.appendRow(new QStandardItem(QStringLiteral("a"))); + + CountLimitProxyModel proxy; + proxy.setSourceModel(&source); + proxy.setMaxRowCount(1); + QCOMPARE(proxy.rowCount(), 1); + + // set null source model + proxy.setSourceModel(nullptr); + QCOMPARE(proxy.rowCount(), 0); + + // old model changes should not affect proxy anymore + source.appendRow(new QStandardItem(QStringLiteral("b"))); + QCOMPARE(proxy.rowCount(), 0); +} + +void TestCountLimitProxyModel::setSameSourceModelIsNoop() +{ + qCInfo(logTest) << "setSourceModel with the same model should be a noop"; + QStandardItemModel source; + source.appendRow(new QStandardItem(QStringLiteral("a"))); + + CountLimitProxyModel proxy; + proxy.setSourceModel(&source); + QCOMPARE(proxy.rowCount(), 1); + + // set same model again + proxy.setSourceModel(&source); + QCOMPARE(proxy.rowCount(), 1); +} + +QTEST_MAIN(TestCountLimitProxyModel) +#include "countlimitproxymodeltest.moc" diff --git a/tests/favoritedproxymodeltest.cpp b/tests/favoritedproxymodeltest.cpp new file mode 100644 index 00000000..00cf17d4 --- /dev/null +++ b/tests/favoritedproxymodeltest.cpp @@ -0,0 +1,243 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../src/models/favoritedproxymodel.h" +#include "../src/models/appsmodel.h" + +namespace { +Q_LOGGING_CATEGORY(logTest, "dde.launchpad.test") + +enum SourceRoles { + SrcDesktopIdRole = Qt::UserRole + 1, + SrcNameRole, + SrcIconNameRole, + SrcNoDisplayRole, + SrcDDECategoryRole, + SrcInstalledTimeRole, + SrcLastLaunchedTimeRole, + SrcLaunchedTimesRole, + SrcAutoStartRole, + SrcCategoriesRole, + SrcVendorRole, + SrcGenericNameRole, +}; + +QStandardItem *makeApp(const QString &desktopId, const QString &name) +{ + auto item = new QStandardItem; + item->setData(desktopId, SrcDesktopIdRole); + item->setData(name, SrcNameRole); + item->setData(QStringLiteral("application-default-icon"), SrcIconNameRole); + item->setData(false, SrcNoDisplayRole); + item->setData(0, SrcDDECategoryRole); + return item; +} +} + +class TestFavoritedProxyModel : public QObject +{ + Q_OBJECT +private slots: + void initTestCase(); + void cleanupTestCase(); + void existsReturnsTrueForAddedFavorite(); + void addAndRemoveFavorite(); + void addDuplicateIsNoop(); + void removeNonexistentIsNoop(); + void pinToTopMovesToFront(); + void filterAcceptsOnlyFavorited(); + void lessThanOrdersByFavoriteListPosition(); + +private: + QStandardItemModel m_sourceModel; + // unique prefix so we can safely remove leftovers even if a prior test aborted + static inline const QString prefix = QStringLiteral("test-"); + void removeAllTestFavorites(); +}; + +void TestFavoritedProxyModel::removeAllTestFavorites() +{ + auto &proxy = FavoritedProxyModel::instance(); + // remove all test-managed IDs (best-effort; removeFavorite is noop if absent) + for (const QString &id : { + prefix + "exists.desktop", prefix + "add-remove.desktop", prefix + "dup.desktop", + prefix + "fav-1.desktop", prefix + "fav-2.desktop", prefix + "fav-3.desktop", + QStringLiteral("app-a.desktop"), QStringLiteral("app-b.desktop"), + QStringLiteral("app-c.desktop"), QStringLiteral("app-d.desktop") }) { + proxy.removeFavorite(id); + } +} + +void TestFavoritedProxyModel::initTestCase() +{ + m_sourceModel.setItemRoleNames({ + { SrcDesktopIdRole, QByteArrayLiteral("desktopId") }, + { SrcNameRole, QByteArrayLiteral("name") }, + { SrcIconNameRole, QByteArrayLiteral("iconName") }, + { SrcNoDisplayRole, QByteArrayLiteral("noDisplay") }, + { SrcDDECategoryRole, QByteArrayLiteral("ddeCategory") }, + { SrcInstalledTimeRole, QByteArrayLiteral("installedTime") }, + { SrcLastLaunchedTimeRole, QByteArrayLiteral("lastLaunchedTime") }, + { SrcLaunchedTimesRole, QByteArrayLiteral("launchedTimes") }, + { SrcAutoStartRole, QByteArrayLiteral("autoStart") }, + { SrcCategoriesRole, QByteArrayLiteral("categories") }, + { SrcVendorRole, QByteArrayLiteral("vendor") }, + { SrcGenericNameRole, QByteArrayLiteral("genericName") }, + }); + m_sourceModel.appendRow(makeApp("app-a.desktop", "App A")); + m_sourceModel.appendRow(makeApp("app-b.desktop", "App B")); + m_sourceModel.appendRow(makeApp("app-c.desktop", "App C")); + m_sourceModel.appendRow(makeApp("app-d.desktop", "App D")); + + AppsModel::instance().setSourceModel(&m_sourceModel); + AppsModel::instance().setReady(true); + + // Accessing the singleton triggers construction + load() + setSourceModel. + FavoritedProxyModel::instance(); + + // clean any leftovers from a prior aborted run + removeAllTestFavorites(); +} + +void TestFavoritedProxyModel::cleanupTestCase() +{ + // best-effort cleanup of in-memory state + removeAllTestFavorites(); + // delete the favorited.ini written by save() to prevent cross-run disk pollution + const QString basePath = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation); + const QString iniPath = QDir(basePath).absoluteFilePath("favorited.ini"); + QFile::remove(iniPath); +} + +void TestFavoritedProxyModel::existsReturnsTrueForAddedFavorite() +{ + auto &proxy = FavoritedProxyModel::instance(); + const QString id = prefix + QStringLiteral("exists.desktop"); + QVERIFY(!proxy.exists(id)); + proxy.addFavorite(id); + QVERIFY(proxy.exists(id)); + proxy.removeFavorite(id); + QVERIFY(!proxy.exists(id)); +} + +void TestFavoritedProxyModel::addAndRemoveFavorite() +{ + auto &proxy = FavoritedProxyModel::instance(); + const QString id = prefix + QStringLiteral("add-remove.desktop"); + + proxy.addFavorite(id); + QVERIFY(proxy.exists(id)); + + proxy.removeFavorite(id); + QVERIFY(!proxy.exists(id)); +} + +void TestFavoritedProxyModel::addDuplicateIsNoop() +{ + auto &proxy = FavoritedProxyModel::instance(); + const QString id = prefix + QStringLiteral("dup.desktop"); + + proxy.addFavorite(id); + QVERIFY(proxy.exists(id)); + + // adding again should not crash or duplicate + proxy.addFavorite(id); + QVERIFY(proxy.exists(id)); + + // only one removal needed + proxy.removeFavorite(id); + QVERIFY(!proxy.exists(id)); +} + +void TestFavoritedProxyModel::removeNonexistentIsNoop() +{ + auto &proxy = FavoritedProxyModel::instance(); + // removing a non-existent favorite should not crash + proxy.removeFavorite(prefix + QStringLiteral("nonexistent-id.desktop")); +} + +void TestFavoritedProxyModel::pinToTopMovesToFront() +{ + auto &proxy = FavoritedProxyModel::instance(); + const QString id1 = prefix + QStringLiteral("fav-1.desktop"); + const QString id2 = prefix + QStringLiteral("fav-2.desktop"); + const QString id3 = prefix + QStringLiteral("fav-3.desktop"); + + proxy.addFavorite(id1); + proxy.addFavorite(id2); + proxy.addFavorite(id3); + + // pin fav-3 to top + proxy.pinToTop(id3); + QVERIFY(proxy.exists(id3)); + + // pin a non-existent id should be a noop + proxy.pinToTop(prefix + QStringLiteral("nonexistent.desktop")); + + // cleanup + proxy.removeFavorite(id1); + proxy.removeFavorite(id2); + proxy.removeFavorite(id3); +} + +void TestFavoritedProxyModel::filterAcceptsOnlyFavorited() +{ + auto &proxy = FavoritedProxyModel::instance(); + + // add favorites that match source model items (app-a, app-c) + proxy.addFavorite("app-a.desktop"); + proxy.addFavorite("app-c.desktop"); + + // proxy should show exactly the 2 favorited items (out of 4 source rows). + // predefined favorites (deepin-editor etc.) don't match the test source, so rowCount == 2. + QCOMPARE(proxy.rowCount(), 2); + + // unconditionally verify: every proxy row's desktopId is a favorited item + for (int i = 0; i < proxy.rowCount(); ++i) { + QString id = proxy.data(proxy.index(i, 0), AppsModel::DesktopIdRole).toString(); + QVERIFY(proxy.exists(id)); + } + + // cleanup + proxy.removeFavorite("app-a.desktop"); + proxy.removeFavorite("app-c.desktop"); + QCOMPARE(proxy.rowCount(), 0); +} + +void TestFavoritedProxyModel::lessThanOrdersByFavoriteListPosition() +{ + auto &proxy = FavoritedProxyModel::instance(); + + proxy.addFavorite("app-b.desktop"); + proxy.addFavorite("app-a.desktop"); + proxy.addFavorite("app-d.desktop"); + + // pin app-d to top so the order is: app-d, app-b, app-a + proxy.pinToTop("app-d.desktop"); + + // proxy should show exactly 3 favorited items + QCOMPARE(proxy.rowCount(), 3); + + // after sort, app-d should come before app-b which comes before app-a + QCOMPARE(proxy.data(proxy.index(0, 0), AppsModel::DesktopIdRole).toString(), QStringLiteral("app-d.desktop")); + QCOMPARE(proxy.data(proxy.index(1, 0), AppsModel::DesktopIdRole).toString(), QStringLiteral("app-b.desktop")); + QCOMPARE(proxy.data(proxy.index(2, 0), AppsModel::DesktopIdRole).toString(), QStringLiteral("app-a.desktop")); + + // cleanup + proxy.removeFavorite("app-b.desktop"); + proxy.removeFavorite("app-a.desktop"); + proxy.removeFavorite("app-d.desktop"); +} + +QTEST_MAIN(TestFavoritedProxyModel) +#include "favoritedproxymodeltest.moc" diff --git a/tests/freesortproxymodeltest.cpp b/tests/freesortproxymodeltest.cpp new file mode 100644 index 00000000..0677195b --- /dev/null +++ b/tests/freesortproxymodeltest.cpp @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include +#include + +#include "../src/models/freesortproxymodel.h" +#include "../src/models/itemarrangementproxymodel.h" + +namespace { +Q_LOGGING_CATEGORY(logTest, "dde.launchpad.test") +} + +class TestFreeSortProxyModel : public QObject +{ + Q_OBJECT +private slots: + void sortByPageThenIndex(); + void descendingOrder(); + void missingRolesDefaultToZero(); +}; + +static QStandardItem *makeArrangedItem(const QString &name, int page, int indexInPage) +{ + auto item = new QStandardItem(name); + item->setData(page, ItemArrangementProxyModel::PageRole); + item->setData(indexInPage, ItemArrangementProxyModel::IndexInPageRole); + return item; +} + +void TestFreeSortProxyModel::sortByPageThenIndex() +{ + qCInfo(logTest) << "FreeSortProxyModel should sort by page then by index-in-page"; + QStandardItemModel source; + // intentionally appended out of order so the proxy actually has to sort + source.appendRow(makeArrangedItem(QStringLiteral("A"), 1, 0)); + source.appendRow(makeArrangedItem(QStringLiteral("B"), 0, 1)); + source.appendRow(makeArrangedItem(QStringLiteral("C"), 0, 0)); + source.appendRow(makeArrangedItem(QStringLiteral("D"), 1, 1)); + + FreeSortProxyModel proxy; + proxy.setSourceModel(&source); + proxy.sort(0, Qt::AscendingOrder); + + QCOMPARE(proxy.rowCount(), 4); + // ascending: C(0,0), B(0,1), A(1,0), D(1,1) + QCOMPARE(proxy.data(proxy.index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("C")); + QCOMPARE(proxy.data(proxy.index(1, 0), Qt::DisplayRole).toString(), QStringLiteral("B")); + QCOMPARE(proxy.data(proxy.index(2, 0), Qt::DisplayRole).toString(), QStringLiteral("A")); + QCOMPARE(proxy.data(proxy.index(3, 0), Qt::DisplayRole).toString(), QStringLiteral("D")); +} + +void TestFreeSortProxyModel::descendingOrder() +{ + qCInfo(logTest) << "FreeSortProxyModel should honor descending sort order"; + QStandardItemModel source; + source.appendRow(makeArrangedItem(QStringLiteral("A"), 1, 0)); + source.appendRow(makeArrangedItem(QStringLiteral("B"), 0, 1)); + source.appendRow(makeArrangedItem(QStringLiteral("C"), 0, 0)); + source.appendRow(makeArrangedItem(QStringLiteral("D"), 1, 1)); + + FreeSortProxyModel proxy; + proxy.setSourceModel(&source); + proxy.sort(0, Qt::DescendingOrder); + + // descending: D(1,1), A(1,0), B(0,1), C(0,0) + QCOMPARE(proxy.data(proxy.index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("D")); + QCOMPARE(proxy.data(proxy.index(1, 0), Qt::DisplayRole).toString(), QStringLiteral("A")); + QCOMPARE(proxy.data(proxy.index(2, 0), Qt::DisplayRole).toString(), QStringLiteral("B")); + QCOMPARE(proxy.data(proxy.index(3, 0), Qt::DisplayRole).toString(), QStringLiteral("C")); +} + +void TestFreeSortProxyModel::missingRolesDefaultToZero() +{ + qCInfo(logTest) << "Items without PageRole/IndexInPageRole should default to 0"; + QStandardItemModel source; + // items without PageRole or IndexInPageRole data -> toInt() returns 0 for both + source.appendRow(new QStandardItem(QStringLiteral("X"))); + source.appendRow(new QStandardItem(QStringLiteral("Y"))); + source.appendRow(new QStandardItem(QStringLiteral("Z"))); + + FreeSortProxyModel proxy; + proxy.setSourceModel(&source); + proxy.sort(0, Qt::AscendingOrder); + + // all items have page=0, index=0 -> equal keys -> stable source order + QCOMPARE(proxy.rowCount(), 3); + QCOMPARE(proxy.data(proxy.index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("X")); + QCOMPARE(proxy.data(proxy.index(1, 0), Qt::DisplayRole).toString(), QStringLiteral("Y")); + QCOMPARE(proxy.data(proxy.index(2, 0), Qt::DisplayRole).toString(), QStringLiteral("Z")); +} + +QTEST_MAIN(TestFreeSortProxyModel) +#include "freesortproxymodeltest.moc" diff --git a/tests/frequentlyusedproxymodeltest.cpp b/tests/frequentlyusedproxymodeltest.cpp new file mode 100644 index 00000000..ce87ee39 --- /dev/null +++ b/tests/frequentlyusedproxymodeltest.cpp @@ -0,0 +1,208 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include +#include +#include + +#include "../src/models/frequentlyusedproxymodel.h" + +namespace { +Q_LOGGING_CATEGORY(logTest, "dde.launchpad.test") + +constexpr int NameRole = Qt::UserRole + 1; +constexpr int DesktopIdRole = Qt::UserRole + 2; +constexpr int LaunchedTimesRole = Qt::UserRole + 3; +constexpr int LastLaunchedTimeRole = Qt::UserRole + 4; +} + +class TestFrequentlyUsedProxyModel : public QObject +{ + Q_OBJECT +private slots: + void filterAcceptsAllWhenNoRecentlyInstalled(); + void filterRejectsItemsInRecentlyInstalled(); + void lessThanByLaunchedTimes(); + void lessThanByLastLaunchedTimeWhenTimesEqual(); + void lessThanFallsToFrequentlyUsedWhenTimesZero(); + void setRecentlyInstalledModelEmitsSignal(); + void setSameRecentlyInstalledModelIsNoop(); + void componentCompleteSortsDescending(); + void classBeginIsNoop(); +}; + +static QStandardItem *makeApp(const QString &name, const QString &desktopId, + qint64 launchedTimes, qint64 lastLaunchedTime) +{ + auto item = new QStandardItem; + item->setData(name, NameRole); + item->setData(desktopId, DesktopIdRole); + item->setData(launchedTimes, LaunchedTimesRole); + item->setData(lastLaunchedTime, LastLaunchedTimeRole); + return item; +} + +static void populate(QStandardItemModel &source) +{ + // high usage, low usage, zero usage + source.appendRow(makeApp("A", "a.desktop", 100, 1000)); + source.appendRow(makeApp("B", "b.desktop", 50, 500)); + source.appendRow(makeApp("C", "c.desktop", 0, 0)); +} + +static void configureRoles(FrequentlyUsedProxyModel &proxy) +{ + QVERIFY(proxy.setProperty("desktopIdRole", DesktopIdRole)); + QCOMPARE(proxy.property("desktopIdRole").toInt(), DesktopIdRole); + QVERIFY(proxy.setProperty("launchedTimesRole", LaunchedTimesRole)); + QCOMPARE(proxy.property("launchedTimesRole").toInt(), LaunchedTimesRole); + QVERIFY(proxy.setProperty("lastLaunchedTimeRole", LastLaunchedTimeRole)); + QCOMPARE(proxy.property("lastLaunchedTimeRole").toInt(), LastLaunchedTimeRole); +} + +void TestFrequentlyUsedProxyModel::filterAcceptsAllWhenNoRecentlyInstalled() +{ + QStandardItemModel source; + populate(source); + + FrequentlyUsedProxyModel proxy; + configureRoles(proxy); + proxy.setSourceModel(&source); + + // no recentlyInstalledModel set -> all rows accepted + QCOMPARE(proxy.rowCount(), 3); +} + +void TestFrequentlyUsedProxyModel::filterRejectsItemsInRecentlyInstalled() +{ + QStandardItemModel source; + populate(source); + + // recentlyInstalledModel contains "b.desktop" + QStandardItemModel recentModel; + recentModel.setItemRoleNames({{ DesktopIdRole, QByteArrayLiteral("desktopId") }}); + auto recentItem = new QStandardItem; + recentItem->setData(QStringLiteral("b.desktop"), DesktopIdRole); + recentModel.appendRow(recentItem); + + FrequentlyUsedProxyModel proxy; + configureRoles(proxy); + proxy.setSourceModel(&source); + proxy.setRecentlyInstalledModel(&recentModel); + + // "b.desktop" is in recentlyInstalled -> filtered out + QCOMPARE(proxy.rowCount(), 2); + QCOMPARE(proxy.data(proxy.index(0, 0), DesktopIdRole).toString(), QStringLiteral("a.desktop")); + QCOMPARE(proxy.data(proxy.index(1, 0), DesktopIdRole).toString(), QStringLiteral("c.desktop")); +} + +void TestFrequentlyUsedProxyModel::lessThanByLaunchedTimes() +{ + QStandardItemModel source; + populate(source); + + FrequentlyUsedProxyModel proxy; + configureRoles(proxy); + proxy.setSourceModel(&source); + proxy.sort(0, Qt::DescendingOrder); + + // descending by launched times: A(100), B(50), C(0) + QCOMPARE(proxy.data(proxy.index(0, 0), DesktopIdRole).toString(), QStringLiteral("a.desktop")); + QCOMPARE(proxy.data(proxy.index(1, 0), DesktopIdRole).toString(), QStringLiteral("b.desktop")); + QCOMPARE(proxy.data(proxy.index(2, 0), DesktopIdRole).toString(), QStringLiteral("c.desktop")); +} + +void TestFrequentlyUsedProxyModel::lessThanByLastLaunchedTimeWhenTimesEqual() +{ + QStandardItemModel source; + // two apps with same launched times but different lastLaunchedTime + source.appendRow(makeApp("X", "x.desktop", 50, 200)); + source.appendRow(makeApp("Y", "y.desktop", 50, 100)); + + FrequentlyUsedProxyModel proxy; + configureRoles(proxy); + proxy.setSourceModel(&source); + proxy.sort(0, Qt::DescendingOrder); + + // same launchedTimes(50) -> compare lastLaunchedTime: X(200) > Y(100) + QCOMPARE(proxy.data(proxy.index(0, 0), DesktopIdRole).toString(), QStringLiteral("x.desktop")); + QCOMPARE(proxy.data(proxy.index(1, 0), DesktopIdRole).toString(), QStringLiteral("y.desktop")); +} + +void TestFrequentlyUsedProxyModel::lessThanFallsToFrequentlyUsedWhenTimesZero() +{ + QStandardItemModel source; + // two apps with zero launched times -> falls to lessThenByFrequentlyUsed + source.appendRow(makeApp("P", "p.desktop", 0, 0)); + source.appendRow(makeApp("Q", "q.desktop", 0, 0)); + + FrequentlyUsedProxyModel proxy; + configureRoles(proxy); + proxy.setSourceModel(&source); + proxy.sort(0, Qt::DescendingOrder); + + // both have 0 launched times and 0 lastLaunchedTime -> falls to frequentlyUsedAppIdList + // (from DConfig, likely empty -> both indexOf return -1 -> equal -> stable source order) + QCOMPARE(proxy.rowCount(), 2); + // stable source order: P (row 0) before Q (row 1) + QCOMPARE(proxy.data(proxy.index(0, 0), DesktopIdRole).toString(), QStringLiteral("p.desktop")); + QCOMPARE(proxy.data(proxy.index(1, 0), DesktopIdRole).toString(), QStringLiteral("q.desktop")); +} + +void TestFrequentlyUsedProxyModel::setRecentlyInstalledModelEmitsSignal() +{ + QStandardItemModel source; + populate(source); + + FrequentlyUsedProxyModel proxy; + configureRoles(proxy); + proxy.setSourceModel(&source); + + QStandardItemModel recentModel; + QSignalSpy spy(&proxy, &FrequentlyUsedProxyModel::recentlyInstalledModelChanged); + proxy.setRecentlyInstalledModel(&recentModel); + QCOMPARE(spy.count(), 1); + QCOMPARE(proxy.recentlyInstalledModel(), &recentModel); +} + +void TestFrequentlyUsedProxyModel::setSameRecentlyInstalledModelIsNoop() +{ + QStandardItemModel source; + populate(source); + + QStandardItemModel recentModel; + + FrequentlyUsedProxyModel proxy; + configureRoles(proxy); + proxy.setSourceModel(&source); + proxy.setRecentlyInstalledModel(&recentModel); + + QSignalSpy spy(&proxy, &FrequentlyUsedProxyModel::recentlyInstalledModelChanged); + proxy.setRecentlyInstalledModel(&recentModel); // same model + QCOMPARE(spy.count(), 0); +} + +void TestFrequentlyUsedProxyModel::componentCompleteSortsDescending() +{ + QStandardItemModel source; + populate(source); + + FrequentlyUsedProxyModel proxy; + configureRoles(proxy); + proxy.setSourceModel(&source); + proxy.componentComplete(); // sort(0, Qt::DescendingOrder) + + QCOMPARE(proxy.data(proxy.index(0, 0), DesktopIdRole).toString(), QStringLiteral("a.desktop")); +} + +void TestFrequentlyUsedProxyModel::classBeginIsNoop() +{ + FrequentlyUsedProxyModel proxy; + proxy.classBegin(); // just logs, no crash +} + +QTEST_MAIN(TestFrequentlyUsedProxyModel) +#include "frequentlyusedproxymodeltest.moc" diff --git a/tests/iconutilstest.cpp b/tests/iconutilstest.cpp new file mode 100644 index 00000000..06453f61 --- /dev/null +++ b/tests/iconutilstest.cpp @@ -0,0 +1,126 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include + +#include "../src/utils/iconutils.h" + +#include + +namespace { +Q_LOGGING_CATEGORY(logTest, "dde.launchpad.test") +} + +class TestIconUtils : public QObject +{ + Q_OBJECT +private slots: + void perfectIconSizeRoundsUp(); + void perfectIconSizeReturnsFirstForSmallInput(); + void perfectIconSizeReturnsLastForLargeInput(); + void perfectIconSizeReverseRoundsDown(); + void perfectIconSizeReverseReturnsLastForLargeInput(); + void perfectIconSizeReverseReturnsFirstForSmallInput(); + void getFolderPerfectIconCellReturnsValidPair(); + void getFolderPerfectIconCellWithDifferentSizes(); + void loadSvgReturnsEmptyForNonexistentFile(); + void loadSvgIntOverloadDelegatesToQSize(); +}; + +void TestIconUtils::perfectIconSizeRoundsUp() +{ + // sizes array: { 16, 18, 24, 32, 64, 96, 128, 256 } + QCOMPARE(IconUtils::perfectIconSize(17), 18); + QCOMPARE(IconUtils::perfectIconSize(20), 24); + QCOMPARE(IconUtils::perfectIconSize(25), 32); + QCOMPARE(IconUtils::perfectIconSize(50), 64); + QCOMPARE(IconUtils::perfectIconSize(100), 128); + QCOMPARE(IconUtils::perfectIconSize(200), 256); +} + +void TestIconUtils::perfectIconSizeReturnsFirstForSmallInput() +{ + // 15 < 16 -> lower_bound returns begin -> 16 + QCOMPARE(IconUtils::perfectIconSize(15), 16); + QCOMPARE(IconUtils::perfectIconSize(1), 16); + QCOMPARE(IconUtils::perfectIconSize(0), 16); +} + +void TestIconUtils::perfectIconSizeReturnsLastForLargeInput() +{ + // 300 > 256 -> lower_bound returns end -> returns sizes[0] = 16 (fallback) + QCOMPARE(IconUtils::perfectIconSize(300), 16); + QCOMPARE(IconUtils::perfectIconSize(1000), 16); +} + +void TestIconUtils::perfectIconSizeReverseRoundsDown() +{ + // reverse: finds the largest size <= input + QCOMPARE(IconUtils::perfectIconSizeReverse(17), 16); + QCOMPARE(IconUtils::perfectIconSizeReverse(20), 18); + QCOMPARE(IconUtils::perfectIconSizeReverse(30), 24); + QCOMPARE(IconUtils::perfectIconSizeReverse(50), 32); + QCOMPARE(IconUtils::perfectIconSizeReverse(100), 96); + QCOMPARE(IconUtils::perfectIconSizeReverse(200), 128); + QCOMPARE(IconUtils::perfectIconSizeReverse(300), 256); +} + +void TestIconUtils::perfectIconSizeReverseReturnsLastForLargeInput() +{ + // 1000 > 256 -> returns 256 (largest in array) + QCOMPARE(IconUtils::perfectIconSizeReverse(1000), 256); +} + +void TestIconUtils::perfectIconSizeReverseReturnsFirstForSmallInput() +{ + // 10 < 16 -> returns sizes[0] = 16 (fallback) + QCOMPARE(IconUtils::perfectIconSizeReverse(10), 16); + QCOMPARE(IconUtils::perfectIconSizeReverse(1), 16); +} + +void TestIconUtils::getFolderPerfectIconCellReturnsValidPair() +{ + auto result = IconUtils::getFolderPerfectIconCell(256, 4); + QVERIFY(result.first > 0); + QVERIFY(result.second > 0); + // iconSize should be a valid perfect icon size + QVERIFY(result.first == 16 || result.first == 18 || result.first == 24 || + result.first == 32 || result.first == 64 || result.first == 96 || + result.first == 128 || result.first == 256); +} + +void TestIconUtils::getFolderPerfectIconCellWithDifferentSizes() +{ + auto result96 = IconUtils::getFolderPerfectIconCell(96, 3); + QVERIFY(result96.first > 0); + QVERIFY(result96.second > 0); + + auto result128 = IconUtils::getFolderPerfectIconCell(128, 4); + QVERIFY(result128.first > 0); + QVERIFY(result128.second > 0); + + auto result192 = IconUtils::getFolderPerfectIconCell(192, 3); + QVERIFY(result192.first > 0); + QVERIFY(result192.second > 0); +} + +void TestIconUtils::loadSvgReturnsEmptyForNonexistentFile() +{ + // loadSvg with a file that doesn't exist should return an empty QPixmap + // (exercises the QFileInfo::exists -> false -> return QPixmap() path) + const QPixmap result = IconUtils::loadSvg(QStringLiteral("/nonexistent/file.svg"), QSize(32, 32)); + QVERIFY(result.isNull()); +} + +void TestIconUtils::loadSvgIntOverloadDelegatesToQSize() +{ + // The int overload should delegate to the QSize overload; with a nonexistent + // file it returns an empty QPixmap (exercises the wrapper line + delegation). + const QPixmap result = IconUtils::loadSvg(QStringLiteral("/nonexistent/file.svg"), 32); + QVERIFY(result.isNull()); +} + +QTEST_MAIN(TestIconUtils) +#include "iconutilstest.moc" diff --git a/tests/itemarrangementproxymodeltest.cpp b/tests/itemarrangementproxymodeltest.cpp new file mode 100644 index 00000000..749efed0 --- /dev/null +++ b/tests/itemarrangementproxymodeltest.cpp @@ -0,0 +1,303 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../src/models/itemarrangementproxymodel.h" +#include "../src/models/appsmodel.h" + +namespace { +Q_LOGGING_CATEGORY(logTest, "dde.launchpad.test") + +enum SourceRoles { + SrcDesktopIdRole = Qt::UserRole + 1, + SrcNameRole, + SrcIconNameRole, + SrcNoDisplayRole, + SrcDDECategoryRole, + SrcInstalledTimeRole, + SrcLastLaunchedTimeRole, + SrcLaunchedTimesRole, + SrcAutoStartRole, + SrcCategoriesRole, + SrcVendorRole, + SrcGenericNameRole, +}; + +QStandardItem *makeApp(const QString &desktopId, const QString &name, int category = 0) +{ + auto item = new QStandardItem; + item->setData(desktopId, SrcDesktopIdRole); + item->setData(name, SrcNameRole); + item->setData(QStringLiteral("application-default-icon"), SrcIconNameRole); + item->setData(false, SrcNoDisplayRole); + item->setData(category, SrcDDECategoryRole); + return item; +} +} + +class TestItemArrangementProxyModel : public QObject +{ + Q_OBJECT +private slots: + void initTestCase(); + void cleanupTestCase(); + void pageCountIsPositiveAfterSetup(); + void findItemReturnsValidPositionForExistingItem(); + void findItemReturnsInvalidForNonexistent(); + void dataReturnsPageAndIndexForAppRow(); + void dataReturnsAppItemType(); + void dataReturnsEmptyIconsNameForApp(); + void roleNamesContainsExtendedRoles(); + void allArrangedItemsContainsAllApps(); + void bringToFrontMovesItemToFirstPosition(); + void bringToFrontOnNonexistentIsNoop(); + void bringToFrontOnFirstItemIsNoop(); + void commitDndOperationWithSameIdIsNoop(); + void creatEmptyPageIncreasesPageCount(); + void removeEmptyPageRemovesEmptyPages(); + +private: + QStandardItemModel m_sourceModel; + int findRowForDesktopId(const QString &desktopId); +}; + +int TestItemArrangementProxyModel::findRowForDesktopId(const QString &desktopId) +{ + auto &model = ItemArrangementProxyModel::instance(); + for (int i = 0; i < model.rowCount(); ++i) { + if (model.data(model.index(i, 0), AppsModel::DesktopIdRole).toString() == desktopId) + return i; + } + return -1; +} + +void TestItemArrangementProxyModel::initTestCase() +{ + m_sourceModel.setItemRoleNames({ + { SrcDesktopIdRole, QByteArrayLiteral("desktopId") }, + { SrcNameRole, QByteArrayLiteral("name") }, + { SrcIconNameRole, QByteArrayLiteral("iconName") }, + { SrcNoDisplayRole, QByteArrayLiteral("noDisplay") }, + { SrcDDECategoryRole, QByteArrayLiteral("ddeCategory") }, + { SrcInstalledTimeRole, QByteArrayLiteral("installedTime") }, + { SrcLastLaunchedTimeRole, QByteArrayLiteral("lastLaunchedTime") }, + { SrcLaunchedTimesRole, QByteArrayLiteral("launchedTimes") }, + { SrcAutoStartRole, QByteArrayLiteral("autoStart") }, + { SrcCategoriesRole, QByteArrayLiteral("categories") }, + { SrcVendorRole, QByteArrayLiteral("vendor") }, + { SrcGenericNameRole, QByteArrayLiteral("genericName") }, + }); + m_sourceModel.appendRow(makeApp("arrange-a.desktop", "App A", 1)); + m_sourceModel.appendRow(makeApp("arrange-b.desktop", "App B", 2)); + m_sourceModel.appendRow(makeApp("arrange-c.desktop", "App C", 3)); + m_sourceModel.appendRow(makeApp("arrange-d.desktop", "App D", 4)); + m_sourceModel.appendRow(makeApp("arrange-e.desktop", "App E", 5)); + + AppsModel::instance().setSourceModel(&m_sourceModel); + AppsModel::instance().setReady(true); + + // Accessing the singleton triggers construction + onSourceModelChanged, + // which adds all AppsModel items to topLevel pages. + ItemArrangementProxyModel::instance(); +} + +void TestItemArrangementProxyModel::pageCountIsPositiveAfterSetup() +{ + QVERIFY(ItemArrangementProxyModel::instance().pageCount(0) > 0); +} + +void TestItemArrangementProxyModel::findItemReturnsValidPositionForExistingItem() +{ + auto &model = ItemArrangementProxyModel::instance(); + int row = findRowForDesktopId("arrange-a.desktop"); + QVERIFY(row >= 0); + + QModelIndex idx = model.index(row, 0); + int folder = model.data(idx, ItemArrangementProxyModel::FolderIdNumberRole).toInt(); + int page = model.data(idx, ItemArrangementProxyModel::PageRole).toInt(); + int indexInPage = model.data(idx, ItemArrangementProxyModel::IndexInPageRole).toInt(); + + // should be in top-level folder (0) + QCOMPARE(folder, 0); + QVERIFY(page >= 0); + QVERIFY(indexInPage >= 0); +} + +void TestItemArrangementProxyModel::findItemReturnsInvalidForNonexistent() +{ + auto &model = ItemArrangementProxyModel::instance(); + int row = findRowForDesktopId("nonexistent.desktop"); + QCOMPARE(row, -1); +} + +void TestItemArrangementProxyModel::dataReturnsPageAndIndexForAppRow() +{ + auto &model = ItemArrangementProxyModel::instance(); + int row = findRowForDesktopId("arrange-b.desktop"); + QVERIFY(row >= 0); + + QModelIndex idx = model.index(row, 0); + QVERIFY(model.data(idx, ItemArrangementProxyModel::PageRole).isValid()); + QVERIFY(model.data(idx, ItemArrangementProxyModel::IndexInPageRole).isValid()); + QVERIFY(model.data(idx, ItemArrangementProxyModel::FolderIdNumberRole).isValid()); +} + +void TestItemArrangementProxyModel::dataReturnsAppItemType() +{ + auto &model = ItemArrangementProxyModel::instance(); + int row = findRowForDesktopId("arrange-c.desktop"); + QVERIFY(row >= 0); + + QModelIndex idx = model.index(row, 0); + QCOMPARE(model.data(idx, ItemArrangementProxyModel::ItemTypeRole).toInt(), + ItemArrangementProxyModel::AppItemType); +} + +void TestItemArrangementProxyModel::dataReturnsEmptyIconsNameForApp() +{ + auto &model = ItemArrangementProxyModel::instance(); + int row = findRowForDesktopId("arrange-d.desktop"); + QVERIFY(row >= 0); + + QModelIndex idx = model.index(row, 0); + QVERIFY(!model.data(idx, ItemArrangementProxyModel::IconsNameRole).isValid()); +} + +void TestItemArrangementProxyModel::roleNamesContainsExtendedRoles() +{ + auto names = ItemArrangementProxyModel::instance().roleNames(); + // Source roleNames() only inserts IconsNameRole and ItemTypeRole (see + // itemarrangementproxymodel.cpp roleNames()); PageRole/IndexInPageRole/ + // FolderIdNumberRole are NOT in roleNames() — they are extended roles served + // via data() but not declared in the role-name hash. + QVERIFY(names.contains(ItemArrangementProxyModel::IconsNameRole)); + QVERIFY(names.contains(ItemArrangementProxyModel::ItemTypeRole)); + // the three page-arrangement roles are data-only, not in roleNames() + QVERIFY(!names.contains(ItemArrangementProxyModel::PageRole)); + QVERIFY(!names.contains(ItemArrangementProxyModel::IndexInPageRole)); + QVERIFY(!names.contains(ItemArrangementProxyModel::FolderIdNumberRole)); +} + +void TestItemArrangementProxyModel::allArrangedItemsContainsAllApps() +{ + // allArrangedItems is private, but itemsPage() is public and returns m_topLevel + auto all = ItemArrangementProxyModel::instance().itemsPage()->allArrangedItems(); + QVERIFY(all.contains("arrange-a.desktop")); + QVERIFY(all.contains("arrange-b.desktop")); + QVERIFY(all.contains("arrange-c.desktop")); + QVERIFY(all.contains("arrange-d.desktop")); + QVERIFY(all.contains("arrange-e.desktop")); +} + +void TestItemArrangementProxyModel::bringToFrontMovesItemToFirstPosition() +{ + auto &model = ItemArrangementProxyModel::instance(); + + // find the row for arrange-e (should NOT be at page 0, index 0 initially) + int row = findRowForDesktopId("arrange-e.desktop"); + QVERIFY(row >= 0); + + QSignalSpy spy(&model, &ItemArrangementProxyModel::itemBroughtToFront); + model.bringToFront("arrange-e.desktop"); + + // signal should have fired (item was moved) + QVERIFY(spy.count() >= 1); + + // after bringToFront, arrange-e should be at page 0, index 0 + row = findRowForDesktopId("arrange-e.desktop"); + QVERIFY(row >= 0); + QModelIndex idx = model.index(row, 0); + QCOMPARE(model.data(idx, ItemArrangementProxyModel::PageRole).toInt(), 0); + QCOMPARE(model.data(idx, ItemArrangementProxyModel::IndexInPageRole).toInt(), 0); +} + +void TestItemArrangementProxyModel::bringToFrontOnNonexistentIsNoop() +{ + auto &model = ItemArrangementProxyModel::instance(); + QSignalSpy spy(&model, &ItemArrangementProxyModel::itemBroughtToFront); + model.bringToFront("nonexistent-id.desktop"); + QCOMPARE(spy.count(), 0); // no signal emitted +} + +void TestItemArrangementProxyModel::bringToFrontOnFirstItemIsNoop() +{ + auto &model = ItemArrangementProxyModel::instance(); + + // self-establish: bring arrange-b to front first so it's at (0, 0) + int row = findRowForDesktopId("arrange-b.desktop"); + QVERIFY(row >= 0); + model.bringToFront("arrange-b.desktop"); + + // verify it's now at page 0, index 0 + row = findRowForDesktopId("arrange-b.desktop"); + QVERIFY(row >= 0); + QModelIndex idx = model.index(row, 0); + QCOMPARE(model.data(idx, ItemArrangementProxyModel::PageRole).toInt(), 0); + QCOMPARE(model.data(idx, ItemArrangementProxyModel::IndexInPageRole).toInt(), 0); + + // bringToFront on an item already at (0, 0) should be noop + QSignalSpy spy(&model, &ItemArrangementProxyModel::itemBroughtToFront); + model.bringToFront("arrange-b.desktop"); + QCOMPARE(spy.count(), 0); +} + +void TestItemArrangementProxyModel::commitDndOperationWithSameIdIsNoop() +{ + auto &model = ItemArrangementProxyModel::instance(); + // dragging onto itself should return early + model.commitDndOperation("arrange-a.desktop", "arrange-a.desktop", + ItemArrangementProxyModel::DndJoin); + // no crash, no assertion failure +} + +void TestItemArrangementProxyModel::creatEmptyPageIncreasesPageCount() +{ + auto &model = ItemArrangementProxyModel::instance(); + int before = model.pageCount(0); + int newPageIndex = model.creatEmptyPage(0); + QVERIFY(newPageIndex >= 0); + QCOMPARE(model.pageCount(0), before + 1); + + // cleanup: remove the empty page we just created so it doesn't leak into + // subsequent tests (removeEmptyPages removes ALL empty pages). + model.removeEmptyPage(); + QCOMPARE(model.pageCount(0), before); +} + +void TestItemArrangementProxyModel::removeEmptyPageRemovesEmptyPages() +{ + auto &model = ItemArrangementProxyModel::instance(); + // self-establish: record the baseline (real pages with apps, no empty pages) + int baseline = model.pageCount(0); + // ensure there are no pre-existing empty pages from prior tests + model.removeEmptyPage(); + QCOMPARE(model.pageCount(0), baseline); + + // create an empty page -> count increases by 1 + model.creatEmptyPage(0); + QCOMPARE(model.pageCount(0), baseline + 1); + + // remove empty pages -> the page we just created should be gone + model.removeEmptyPage(); + QCOMPARE(model.pageCount(0), baseline); +} + +void TestItemArrangementProxyModel::cleanupTestCase() +{ + // delete the item-arrangement.ini written by saveItemArrangementToUserData() + const QString basePath = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation); + const QString iniPath = QDir(basePath).absoluteFilePath("deepin/dde-launchpad/item-arrangement.ini"); + QFile::remove(iniPath); +} + +QTEST_MAIN(TestItemArrangementProxyModel) +#include "itemarrangementproxymodeltest.moc" diff --git a/tests/itemspagemodeltest.cpp b/tests/itemspagemodeltest.cpp new file mode 100644 index 00000000..c161472d --- /dev/null +++ b/tests/itemspagemodeltest.cpp @@ -0,0 +1,192 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include +#include +#include + +#include "../src/models/itemspagemodel.h" +#include "../src/models/itemarrangementproxymodel.h" +#include "../src/models/appsmodel.h" + +namespace { +Q_LOGGING_CATEGORY(logTest, "dde.launchpad.test") + +enum SourceRoles { + SrcDesktopIdRole = Qt::UserRole + 1, + SrcNameRole, + SrcIconNameRole, + SrcNoDisplayRole, + SrcDDECategoryRole, + SrcInstalledTimeRole, + SrcLastLaunchedTimeRole, + SrcLaunchedTimesRole, + SrcAutoStartRole, + SrcCategoriesRole, + SrcVendorRole, + SrcGenericNameRole, +}; + +QStandardItem *makeApp(const QString &desktopId, const QString &name) +{ + auto item = new QStandardItem; + item->setData(desktopId, SrcDesktopIdRole); + item->setData(name, SrcNameRole); + item->setData(QStringLiteral("application-default-icon"), SrcIconNameRole); + item->setData(false, SrcNoDisplayRole); + item->setData(0, SrcDDECategoryRole); + return item; +} +} + +class TestItemsPageModel : public QObject +{ + Q_OBJECT +private slots: + void initTestCase(); + void rowCountIsZeroWithoutSourceModel(); + void dataReturnsEmptyVariant(); + void setSourceModelWithNullIsNoop(); + void setSameSourceModelIsNoop(); + void setSourceModelConnectsToItemsPage(); + void rowCountReflectsItemsPageCount(); + void sigPageAddedInsertsRows(); + void sigPageRemovedRemovesRows(); + void sourceModelGetterReturnsSetModel(); + +private: + QStandardItemModel m_sourceModel; +}; + +void TestItemsPageModel::initTestCase() +{ + m_sourceModel.setItemRoleNames({ + { SrcDesktopIdRole, QByteArrayLiteral("desktopId") }, + { SrcNameRole, QByteArrayLiteral("name") }, + { SrcIconNameRole, QByteArrayLiteral("iconName") }, + { SrcNoDisplayRole, QByteArrayLiteral("noDisplay") }, + { SrcDDECategoryRole, QByteArrayLiteral("ddeCategory") }, + { SrcInstalledTimeRole, QByteArrayLiteral("installedTime") }, + { SrcLastLaunchedTimeRole, QByteArrayLiteral("lastLaunchedTime") }, + { SrcLaunchedTimesRole, QByteArrayLiteral("launchedTimes") }, + { SrcAutoStartRole, QByteArrayLiteral("autoStart") }, + { SrcCategoriesRole, QByteArrayLiteral("categories") }, + { SrcVendorRole, QByteArrayLiteral("vendor") }, + { SrcGenericNameRole, QByteArrayLiteral("genericName") }, + }); + m_sourceModel.appendRow(makeApp("page-a.desktop", "Page A")); + m_sourceModel.appendRow(makeApp("page-b.desktop", "Page B")); + m_sourceModel.appendRow(makeApp("page-c.desktop", "Page C")); + + AppsModel::instance().setSourceModel(&m_sourceModel); + AppsModel::instance().setReady(true); + + // Accessing the singleton triggers construction + onSourceModelChanged, + // which adds all AppsModel items to topLevel pages. + ItemArrangementProxyModel::instance(); +} + +void TestItemsPageModel::rowCountIsZeroWithoutSourceModel() +{ + ItemsPageModel model; + QCOMPARE(model.rowCount(), 0); +} + +void TestItemsPageModel::dataReturnsEmptyVariant() +{ + ItemsPageModel model; + // data() always returns empty QVariant (per source) + QVERIFY(!model.data(model.index(0, 0)).isValid()); +} + +void TestItemsPageModel::setSourceModelWithNullIsNoop() +{ + ItemsPageModel model; + QSignalSpy spy(&model, &ItemsPageModel::sourceModelChanged); + model.setSourceModel(nullptr); + QCOMPARE(spy.count(), 0); // null model -> skip, no signal + QCOMPARE(model.rowCount(), 0); +} + +void TestItemsPageModel::setSameSourceModelIsNoop() +{ + ItemsPageModel model; + auto &iapm = ItemArrangementProxyModel::instance(); + + model.setSourceModel(&iapm); + QSignalSpy spy(&model, &ItemsPageModel::sourceModelChanged); + model.setSourceModel(&iapm); // same model -> skip + QCOMPARE(spy.count(), 0); +} + +void TestItemsPageModel::setSourceModelConnectsToItemsPage() +{ + ItemsPageModel model; + auto &iapm = ItemArrangementProxyModel::instance(); + + QSignalSpy spy(&model, &ItemsPageModel::sourceModelChanged); + model.setSourceModel(&iapm); + QCOMPARE(spy.count(), 1); + QCOMPARE(model.sourceModel(), &iapm); +} + +void TestItemsPageModel::rowCountReflectsItemsPageCount() +{ + ItemsPageModel model; + auto &iapm = ItemArrangementProxyModel::instance(); + model.setSourceModel(&iapm); + + // rowCount should match the topLevel pageCount (apps added in initTestCase) + QCOMPARE(model.rowCount(), iapm.itemsPage()->pageCount()); + QVERIFY(model.rowCount() > 0); +} + +void TestItemsPageModel::sigPageAddedInsertsRows() +{ + ItemsPageModel model; + auto &iapm = ItemArrangementProxyModel::instance(); + model.setSourceModel(&iapm); + + int before = model.rowCount(); + // create an empty page -> sigPageAdded -> beginInsertRows/endInsertRows + QSignalSpy insertSpy(&model, &QAbstractItemModel::rowsInserted); + iapm.creatEmptyPage(0); + QVERIFY(insertSpy.count() >= 1); + QCOMPARE(model.rowCount(), before + 1); + + // cleanup + iapm.removeEmptyPage(); + QCOMPARE(model.rowCount(), before); +} + +void TestItemsPageModel::sigPageRemovedRemovesRows() +{ + ItemsPageModel model; + auto &iapm = ItemArrangementProxyModel::instance(); + model.setSourceModel(&iapm); + + int before = model.rowCount(); + // create an empty page first + iapm.creatEmptyPage(0); + QCOMPARE(model.rowCount(), before + 1); + + // remove it -> sigPageRemoved -> beginRemoveRows/endRemoveRows + QSignalSpy removeSpy(&model, &QAbstractItemModel::rowsRemoved); + iapm.removeEmptyPage(); + QVERIFY(removeSpy.count() >= 1); + QCOMPARE(model.rowCount(), before); +} + +void TestItemsPageModel::sourceModelGetterReturnsSetModel() +{ + ItemsPageModel model; + auto &iapm = ItemArrangementProxyModel::instance(); + model.setSourceModel(&iapm); + QCOMPARE(model.sourceModel(), &iapm); +} + +QTEST_MAIN(TestItemsPageModel) +#include "itemspagemodeltest.moc" diff --git a/tests/itemspagetest.cpp b/tests/itemspagetest.cpp index 481d2e5a..87ff5271 100644 --- a/tests/itemspagetest.cpp +++ b/tests/itemspagetest.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2024 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later @@ -18,6 +18,20 @@ private slots: void insertAndRemove(); void autoRemoveEmptyPage(); void dragItemToFolder(); + void findItemReturnsValidPosition(); + void findItemReturnsInvalidForNonexistent(); + void containsReturnsCorrect(); + void nameSetterAndGetter(); + void itemCountTotalAndPerPage(); + void firstNItemsReturnsUpToN(); + void allArrangedItemsReturnsAll(); + void removeItemsNotInRemovesAbsent(); + void removeEmptyPagesRemovesAllEmpty(); + void appendItemCreatesNewPageWhenFull(); + void moveItemPositionSamePageAdjacentAppendIsNoop(); + void moveItemPositionCrossPage(); + void removeItemNonExistentIsNoop(); + void insertItemToPageAutoSelectsPage(); }; void TestItemsPage::insertAndRemove() @@ -93,5 +107,163 @@ void TestItemsPage::dragItemToFolder() qCInfo(logTest) << "Verified page 0 contains items [1,2,3], dragItemToFolder test completed"; } +void TestItemsPage::findItemReturnsValidPosition() +{ + ItemsPage ip(3); + ip.appendPage({"a", "b", "c"}); + ip.appendPage({"d"}); + + auto [page, index] = ip.findItem("b"); + QCOMPARE(page, 0); + QCOMPARE(index, 1); + + auto [page2, index2] = ip.findItem("d"); + QCOMPARE(page2, 1); + QCOMPARE(index2, 0); +} + +void TestItemsPage::findItemReturnsInvalidForNonexistent() +{ + ItemsPage ip(3); + ip.appendPage({"a", "b"}); + + auto [page, index] = ip.findItem("nonexistent"); + QCOMPARE(page, -1); + QCOMPARE(index, -1); +} + +void TestItemsPage::containsReturnsCorrect() +{ + ItemsPage ip(3); + ip.appendPage({"a", "b"}); + + QVERIFY(ip.contains("a")); + QVERIFY(ip.contains("b")); + QVERIFY(!ip.contains("c")); +} + +void TestItemsPage::nameSetterAndGetter() +{ + ItemsPage ip(3); + QVERIFY(ip.name().isEmpty()); + ip.setName(QStringLiteral("MyFolder")); + QCOMPARE(ip.name(), QStringLiteral("MyFolder")); + ip.setName(QStringLiteral("Renamed")); + QCOMPARE(ip.name(), QStringLiteral("Renamed")); +} + +void TestItemsPage::itemCountTotalAndPerPage() +{ + ItemsPage ip(3); + ip.appendPage({"a", "b", "c"}); + ip.appendPage({"d", "e"}); + + QCOMPARE(ip.itemCount(), 5); + QCOMPARE(ip.itemCount(0), 3); + QCOMPARE(ip.itemCount(1), 2); +} + +void TestItemsPage::firstNItemsReturnsUpToN() +{ + ItemsPage ip(3); + ip.appendPage({"a", "b"}); + ip.appendPage({"c", "d", "e"}); + + QCOMPARE(ip.firstNItems(3), QStringList({"a", "b", "c"})); + QCOMPARE(ip.firstNItems(1), QStringList({"a"})); + QCOMPARE(ip.firstNItems(5), QStringList({"a", "b", "c", "d", "e"})); +} + +void TestItemsPage::allArrangedItemsReturnsAll() +{ + ItemsPage ip(3); + ip.appendPage({"a", "b"}); + ip.appendPage({"c"}); + + QCOMPARE(ip.allArrangedItems(), QStringList({"a", "b", "c"})); +} + +void TestItemsPage::removeItemsNotInRemovesAbsent() +{ + ItemsPage ip(3); + ip.appendPage({"a", "b", "c"}); + ip.appendPage({"d"}); + + QSet keep = {"a", "c", "d"}; + ip.removeItemsNotIn(keep); + + QCOMPARE(ip.allArrangedItems(), QStringList({"a", "c", "d"})); +} + +void TestItemsPage::removeEmptyPagesRemovesAllEmpty() +{ + ItemsPage ip(3); + ip.appendPage({"a"}); + ip.appendEmptyPage(); + ip.appendEmptyPage(); + QCOMPARE(ip.pageCount(), 3); + + ip.removeEmptyPages(); + QCOMPARE(ip.pageCount(), 1); + QCOMPARE(ip.items(0), QStringList({"a"})); +} + +void TestItemsPage::appendItemCreatesNewPageWhenFull() +{ + ItemsPage ip(2); + ip.appendPage({"a", "b"}); // page 0 full + QCOMPARE(ip.pageCount(), 1); + + ip.appendItem("c"); // should create page 1 + QCOMPARE(ip.pageCount(), 2); + QCOMPARE(ip.items(1), QStringList({"c"})); +} + +void TestItemsPage::moveItemPositionSamePageAdjacentAppendIsNoop() +{ + ItemsPage ip(3); + ip.appendPage({"a", "b", "c"}); + + // move from page 0, index 1 to page 0, index 0 with append=true + // since fromIndex(1) > toIndex(0) and append=true, and they're adjacent (1 == 0+1), + // the source does nothing. + ip.moveItemPosition(0, 1, 0, 0, true); + QCOMPARE(ip.items(0), QStringList({"a", "b", "c"})); +} + +void TestItemsPage::moveItemPositionCrossPage() +{ + ItemsPage ip(3); + ip.appendPage({"a", "b", "c"}); + ip.appendPage({"d", "e"}); + + // move "b" from page 0, index 1 to page 1, index 0 + ip.moveItemPosition(0, 1, 1, 0, false); + QCOMPARE(ip.items(0), QStringList({"a", "c"})); + QCOMPARE(ip.items(1), QStringList({"b", "d", "e"})); +} + +void TestItemsPage::removeItemNonExistentIsNoop() +{ + ItemsPage ip(3); + ip.appendPage({"a", "b"}); + QCOMPARE(ip.pageCount(), 1); + + ip.removeItem("nonexistent"); + QCOMPARE(ip.pageCount(), 1); + QCOMPARE(ip.items(0), QStringList({"a", "b"})); +} + +void TestItemsPage::insertItemToPageAutoSelectsPage() +{ + ItemsPage ip(3); + ip.appendPage({"a", "b", "c"}); // page 0 full + ip.appendPage({"d"}); + + // insertItemToPage with page=-1 should auto-select the last page with space + ip.insertItemToPage("e", -1); + QVERIFY(ip.contains("e")); +} + QTEST_MAIN(TestItemsPage) #include "itemspagetest.moc" diff --git a/tests/multipagesortfilterproxymodeltest.cpp b/tests/multipagesortfilterproxymodeltest.cpp new file mode 100644 index 00000000..48f7fe96 --- /dev/null +++ b/tests/multipagesortfilterproxymodeltest.cpp @@ -0,0 +1,187 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include +#include + +#include "../src/models/multipagesortfilterproxymodel.h" +#include "../src/models/itemarrangementproxymodel.h" + +namespace { +Q_LOGGING_CATEGORY(logTest, "dde.launchpad.test") + +constexpr int NameRole = Qt::UserRole + 10; +} + +class TestMultipageSortFilterProxyModel : public QObject +{ + Q_OBJECT +private slots: + void filtersByFolderAndPage(); + void noMatchYieldsEmpty(); + void filterOnlyModeChangesSorting(); + void filterOnlyModeToggleDoesNotAutoResort(); +}; + +// build the source in the order: Z, Y, X, W (so the proxy actually has to sort) +static QStandardItem *makeArrangedItem(const QString &name, int folderId, int page, int indexInPage) +{ + auto item = new QStandardItem; + item->setData(name, NameRole); + item->setData(folderId, ItemArrangementProxyModel::FolderIdNumberRole); + item->setData(page, ItemArrangementProxyModel::PageRole); + item->setData(indexInPage, ItemArrangementProxyModel::IndexInPageRole); + return item; +} + +static void populateSource(QStandardItemModel &source) +{ + source.appendRow(makeArrangedItem(QStringLiteral("Z"), 0, 1, 0)); // source row 0 + source.appendRow(makeArrangedItem(QStringLiteral("Y"), 0, 0, 1)); // source row 1 + source.appendRow(makeArrangedItem(QStringLiteral("X"), 0, 0, 0)); // source row 2 + source.appendRow(makeArrangedItem(QStringLiteral("W"), 1, 0, 0)); // source row 3 +} + +void TestMultipageSortFilterProxyModel::filtersByFolderAndPage() +{ + qCInfo(logTest) << "Filter by folderId and pageId, sorted by page then index-in-page"; + QStandardItemModel source; + populateSource(source); + + MultipageSortFilterProxyModel proxy; + // folderId / pageId members are not initialized by the constructor, set them up front. + proxy.setProperty("folderId", 0); + QCOMPARE(proxy.property("folderId").toInt(), 0); + proxy.setProperty("pageId", -1); + QCOMPARE(proxy.property("pageId").toInt(), -1); + proxy.setProperty("filterOnlyMode", false); + QCOMPARE(proxy.property("filterOnlyMode").toBool(), false); + proxy.setModel(&source); + + // folder 0, all pages -> X(0,0,0), Y(0,0,1), Z(0,1,0) + QCOMPARE(proxy.rowCount(), 3); + QCOMPARE(proxy.data(proxy.index(0, 0), NameRole).toString(), QStringLiteral("X")); + QCOMPARE(proxy.data(proxy.index(1, 0), NameRole).toString(), QStringLiteral("Y")); + QCOMPARE(proxy.data(proxy.index(2, 0), NameRole).toString(), QStringLiteral("Z")); + + // restrict to page 0 -> X, Y + proxy.setProperty("pageId", 0); + QCOMPARE(proxy.property("pageId").toInt(), 0); + QCOMPARE(proxy.rowCount(), 2); + QCOMPARE(proxy.data(proxy.index(0, 0), NameRole).toString(), QStringLiteral("X")); + QCOMPARE(proxy.data(proxy.index(1, 0), NameRole).toString(), QStringLiteral("Y")); + + // restrict to page 1 -> Z + proxy.setProperty("pageId", 1); + QCOMPARE(proxy.property("pageId").toInt(), 1); + QCOMPARE(proxy.rowCount(), 1); + QCOMPARE(proxy.data(proxy.index(0, 0), NameRole).toString(), QStringLiteral("Z")); + + // switch to folder 1, all pages -> W + proxy.setProperty("folderId", 1); + QCOMPARE(proxy.property("folderId").toInt(), 1); + proxy.setProperty("pageId", -1); + QCOMPARE(proxy.property("pageId").toInt(), -1); + QCOMPARE(proxy.rowCount(), 1); + QCOMPARE(proxy.data(proxy.index(0, 0), NameRole).toString(), QStringLiteral("W")); +} + +void TestMultipageSortFilterProxyModel::noMatchYieldsEmpty() +{ + qCInfo(logTest) << "A folderId matching no source row should yield an empty proxy"; + QStandardItemModel source; + populateSource(source); + + MultipageSortFilterProxyModel proxy; + proxy.setProperty("folderId", 999); + QCOMPARE(proxy.property("folderId").toInt(), 999); + proxy.setProperty("pageId", -1); + QCOMPARE(proxy.property("pageId").toInt(), -1); + proxy.setModel(&source); + QCOMPARE(proxy.rowCount(), 0); +} + +void TestMultipageSortFilterProxyModel::filterOnlyModeChangesSorting() +{ + qCInfo(logTest) << "filterOnlyMode=true, after a forced re-sort, should fall back to sortRole-only ordering"; + QStandardItemModel source; + populateSource(source); + + MultipageSortFilterProxyModel proxy; + proxy.setProperty("folderId", 0); + QCOMPARE(proxy.property("folderId").toInt(), 0); + proxy.setProperty("pageId", -1); + QCOMPARE(proxy.property("pageId").toInt(), -1); + proxy.setProperty("filterOnlyMode", false); + QCOMPARE(proxy.property("filterOnlyMode").toBool(), false); + proxy.setModel(&source); + // default ordering (filterOnlyMode=false) by page then index-in-page -> X, Y, Z + QCOMPARE(proxy.data(proxy.index(0, 0), NameRole).toString(), QStringLiteral("X")); + QCOMPARE(proxy.data(proxy.index(1, 0), NameRole).toString(), QStringLiteral("Y")); + QCOMPARE(proxy.data(proxy.index(2, 0), NameRole).toString(), QStringLiteral("Z")); + + // Switching filterOnlyMode updates the member (read back to prove it took effect)... + proxy.setProperty("filterOnlyMode", true); + QCOMPARE(proxy.property("filterOnlyMode").toBool(), true); + // ...but onFilterOnlyModeChanged is not wired to any re-sort/invalidate in the source, + // so the order does NOT change yet (see filterOnlyModeToggleDoesNotAutoResort). + QCOMPARE(proxy.data(proxy.index(0, 0), NameRole).toString(), QStringLiteral("X")); + QCOMPARE(proxy.data(proxy.index(1, 0), NameRole).toString(), QStringLiteral("Y")); + QCOMPARE(proxy.data(proxy.index(2, 0), NameRole).toString(), QStringLiteral("Z")); + + // Force a re-evaluation: invalidate() re-runs the sort with the now-active + // filterOnlyMode==true lessThan (QSortFilterProxyModel::lessThan by sortRole, + // which is FolderIdNumberRole == 0 for all rows) -> stable source order Z, Y, X. + // (sort(0) alone would short-circuit: column/order equal to the sort(0) done in setModel.) + proxy.invalidate(); + QCOMPARE(proxy.rowCount(), 3); + QCOMPARE(proxy.data(proxy.index(0, 0), NameRole).toString(), QStringLiteral("Z")); + QCOMPARE(proxy.data(proxy.index(1, 0), NameRole).toString(), QStringLiteral("Y")); + QCOMPARE(proxy.data(proxy.index(2, 0), NameRole).toString(), QStringLiteral("X")); +} + +void TestMultipageSortFilterProxyModel::filterOnlyModeToggleDoesNotAutoResort() +{ + qCInfo(logTest) << "Toggling filterOnlyMode alone must not re-sort (signal is not wired in source)"; + QStandardItemModel source; + populateSource(source); + + MultipageSortFilterProxyModel proxy; + proxy.setProperty("folderId", 0); + QCOMPARE(proxy.property("folderId").toInt(), 0); + proxy.setProperty("pageId", -1); + QCOMPARE(proxy.property("pageId").toInt(), -1); + proxy.setProperty("filterOnlyMode", false); + QCOMPARE(proxy.property("filterOnlyMode").toBool(), false); + proxy.setModel(&source); + // filterOnlyMode=false ordering: X, Y, Z + QCOMPARE(proxy.data(proxy.index(0, 0), NameRole).toString(), QStringLiteral("X")); + QCOMPARE(proxy.data(proxy.index(2, 0), NameRole).toString(), QStringLiteral("Z")); + + // Toggling filterOnlyMode emits onFilterOnlyModeChanged, but the source never + // connects it to invalidate()/invalidateFilter(). The member is updated (read + // back below), yet no re-sort/re-filter is triggered, so the visible order is + // unchanged. + proxy.setProperty("filterOnlyMode", true); + QCOMPARE(proxy.property("filterOnlyMode").toBool(), true); + proxy.sort(0); // short-circuits: same column/order as the sort(0) inside setModel + QCOMPARE(proxy.data(proxy.index(0, 0), NameRole).toString(), QStringLiteral("X")); + QCOMPARE(proxy.data(proxy.index(1, 0), NameRole).toString(), QStringLiteral("Y")); + QCOMPARE(proxy.data(proxy.index(2, 0), NameRole).toString(), QStringLiteral("Z")); + + // Even flipping the sort order does NOT yield source order here: with + // filterOnlyMode==true the sortRole (FolderIdNumberRole) is identical for every + // row, so the stable sort preserves the *current* proxy mapping (X, Y, Z) + // rather than re-deriving source order. Only invalidate() re-builds from the + // source (see filterOnlyModeChangesSorting for that case). + proxy.sort(0, Qt::DescendingOrder); + QCOMPARE(proxy.data(proxy.index(0, 0), NameRole).toString(), QStringLiteral("X")); + QCOMPARE(proxy.data(proxy.index(1, 0), NameRole).toString(), QStringLiteral("Y")); + QCOMPARE(proxy.data(proxy.index(2, 0), NameRole).toString(), QStringLiteral("Z")); +} + +QTEST_MAIN(TestMultipageSortFilterProxyModel) +#include "multipagesortfilterproxymodeltest.moc" diff --git a/tests/recentlyinstalledproxymodeltest.cpp b/tests/recentlyinstalledproxymodeltest.cpp new file mode 100644 index 00000000..e273f13c --- /dev/null +++ b/tests/recentlyinstalledproxymodeltest.cpp @@ -0,0 +1,108 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include +#include + +#include "../src/models/recentlyinstalledproxymodel.h" + +namespace { +Q_LOGGING_CATEGORY(logTest, "dde.launchpad.test") + +constexpr int NameRole = Qt::UserRole + 1; +constexpr int InstalledTimeRole = Qt::UserRole + 2; +constexpr int LastLaunchedTimeRole = Qt::UserRole + 3; +} + +class TestRecentlyInstalledProxyModel : public QObject +{ + Q_OBJECT +private slots: + void filtersAndSortsByInstalledTime(); + void ascendingOrder(); + void componentCompleteSortsDescending(); +}; + +static QStandardItem *makeApp(const QString &name, qint64 installed, qint64 launched) +{ + auto item = new QStandardItem; + item->setData(name, NameRole); + item->setData(installed, InstalledTimeRole); + item->setData(launched, LastLaunchedTimeRole); + return item; +} + +static void populateSource(QStandardItemModel &source) +{ + source.appendRow(makeApp(QStringLiteral("A"), 100, 0)); // installed, never launched -> kept + source.appendRow(makeApp(QStringLiteral("B"), 200, 0)); // installed, never launched -> kept + source.appendRow(makeApp(QStringLiteral("C"), 300, 50)); // already launched -> filtered out + source.appendRow(makeApp(QStringLiteral("D"), 0, 0)); // not installed -> filtered out + source.appendRow(makeApp(QStringLiteral("E"), 0, 10)); // launched but not installed -> filtered out +} + +// the role members are intentionally not initialized by the constructor, +// so they must be set before any filtering/sorting happens. The readback +// QCOMPAREs guard against a silent drift of the Q_PROPERTY name (a wrong +// name would make property() return an invalid variant -> toInt()==0). +static void configureProxy(RecentlyInstalledProxyModel &proxy, QStandardItemModel &source) +{ + proxy.setProperty("installedTimeRole", InstalledTimeRole); + QCOMPARE(proxy.property("installedTimeRole").toInt(), InstalledTimeRole); + proxy.setProperty("lastLaunchedTimeRole", LastLaunchedTimeRole); + QCOMPARE(proxy.property("lastLaunchedTimeRole").toInt(), LastLaunchedTimeRole); + proxy.setSourceModel(&source); +} + +void TestRecentlyInstalledProxyModel::filtersAndSortsByInstalledTime() +{ + qCInfo(logTest) << "Only never-launched, installed apps should be kept, newest first"; + QStandardItemModel source; + populateSource(source); + + RecentlyInstalledProxyModel proxy; + configureProxy(proxy, source); + proxy.sort(0, Qt::DescendingOrder); + + QCOMPARE(proxy.rowCount(), 2); + QCOMPARE(proxy.data(proxy.index(0, 0), NameRole).toString(), QStringLiteral("B")); // installed=200 + QCOMPARE(proxy.data(proxy.index(1, 0), NameRole).toString(), QStringLiteral("A")); // installed=100 +} + +void TestRecentlyInstalledProxyModel::ascendingOrder() +{ + qCInfo(logTest) << "Ascending sort should order by installed time ascending"; + QStandardItemModel source; + populateSource(source); + + RecentlyInstalledProxyModel proxy; + configureProxy(proxy, source); + proxy.sort(0, Qt::AscendingOrder); + + QCOMPARE(proxy.rowCount(), 2); + QCOMPARE(proxy.data(proxy.index(0, 0), NameRole).toString(), QStringLiteral("A")); // installed=100 + QCOMPARE(proxy.data(proxy.index(1, 0), NameRole).toString(), QStringLiteral("B")); // installed=200 +} + +void TestRecentlyInstalledProxyModel::componentCompleteSortsDescending() +{ + qCInfo(logTest) << "componentComplete() should sort by installed time descending (newest first)"; + QStandardItemModel source; + populateSource(source); + + RecentlyInstalledProxyModel proxy; + configureProxy(proxy, source); + // componentComplete() is what QML calls once the declarative object is ready; + // it issues sort(0, Qt::DescendingOrder), i.e. newest installed first. + proxy.componentComplete(); + + QCOMPARE(proxy.rowCount(), 2); + QCOMPARE(proxy.data(proxy.index(0, 0), NameRole).toString(), QStringLiteral("B")); // installed=200 + QCOMPARE(proxy.data(proxy.index(1, 0), NameRole).toString(), QStringLiteral("A")); // installed=100 +} + +QTEST_MAIN(TestRecentlyInstalledProxyModel) +#include "recentlyinstalledproxymodeltest.moc" diff --git a/tests/searchfilterproxymodeltest.cpp b/tests/searchfilterproxymodeltest.cpp index 185c7aaa..b0e823ba 100644 --- a/tests/searchfilterproxymodeltest.cpp +++ b/tests/searchfilterproxymodeltest.cpp @@ -49,6 +49,9 @@ private slots: void testSpecialCharacters(); void testSharedModelAdapter(); void testCategorySortSignalOrder(); + void testCategorizedSortAlphabetarySections(); + void testCategorizedSortDDECategorySections(); + void testCategorizedSortRoleNameAndCategoryType(); private: void setupTestData(); @@ -432,5 +435,64 @@ void TestSearchFilterProxyModel::testCategorySortSignalOrder() QStringLiteral("category") })); } +void TestSearchFilterProxyModel::testCategorizedSortAlphabetarySections() +{ + // Exercise alphabetarySections(): collect uppercased first chars of transliterated names. + auto &model = CategorizedSortProxyModel::instance(); + model.setCategoryType(CategorizedSortProxyModel::Alphabetary); + + const auto sections = model.alphabetarySections(); + // The test data contains English apps (Calculator, Editor, Browser, etc.) + // and Chinese apps (音乐, etc.). Each transliterated name starts with a + // letter; the set should be non-empty and sorted (with &/# at front if present). + QVERIFY(!sections.isEmpty()); + // Verify the returned list is sorted by the custom comparator (& < # < letters) + for (int i = 1; i < sections.size(); ++i) { + const QString &prev = sections[i - 1]; + const QString &curr = sections[i]; + // customLessThan: & < #, then normal < + if (prev == "&" && curr == "#") + continue; // & before # is valid + QVERIFY(prev != "#" || curr != "&"); // # before & would be wrong order + QVERIFY(curr >= prev || (prev == "&" && curr == "#")); + } +} + +void TestSearchFilterProxyModel::testCategorizedSortDDECategorySections() +{ + // Exercise DDECategorySections(): collect distinct DDECategory values. + auto &model = CategorizedSortProxyModel::instance(); + model.setCategoryType(CategorizedSortProxyModel::DDECategory); + + const auto sections = model.DDECategorySections(); + // The test data has items with DDECategoryRole set (via SourceDDECategoryRole % 11) + // so the section list should be non-empty and sorted ascending. + QVERIFY(!sections.isEmpty()); + // Verify sorted ascending + for (int i = 1; i < sections.size(); ++i) + QVERIFY(sections[i] >= sections[i - 1]); +} + +void TestSearchFilterProxyModel::testCategorizedSortRoleNameAndCategoryType() +{ + // Exercise sortRoleName() and categoryType() getter — all 3 branches. + auto &model = CategorizedSortProxyModel::instance(); + + // Set to DDECategory and verify getter returns DDECategory + model.setCategoryType(CategorizedSortProxyModel::DDECategory); + QCOMPARE(int(model.categoryType()), int(CategorizedSortProxyModel::DDECategory)); + // sortRoleName should return the name of the DDECategoryRole + QCOMPARE(model.sortRoleName(), QStringLiteral("category")); + + // Set to Alphabetary and verify getter returns Alphabetary + model.setCategoryType(CategorizedSortProxyModel::Alphabetary); + QCOMPARE(int(model.categoryType()), int(CategorizedSortProxyModel::Alphabetary)); + QCOMPARE(model.sortRoleName(), QStringLiteral("transliterated")); + + // FreeCategory branch: when categoryType is FreeCategory, isFreeSort=true + model.setCategoryType(CategorizedSortProxyModel::FreeCategory); + QCOMPARE(int(model.categoryType()), int(CategorizedSortProxyModel::FreeCategory)); +} + QTEST_MAIN(TestSearchFilterProxyModel) #include "searchfilterproxymodeltest.moc" diff --git a/tests/sortproxymodeltest.cpp b/tests/sortproxymodeltest.cpp new file mode 100644 index 00000000..8dc99443 --- /dev/null +++ b/tests/sortproxymodeltest.cpp @@ -0,0 +1,488 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include +#include +#include + +#include "../src/models/sortproxymodel.h" + +namespace { +Q_LOGGING_CATEGORY(logTest, "dde.launchpad.test") + +// A minimal list model that allows changing data WITHOUT auto-emitting dataChanged, +// so we can control exactly which roles trigger handleDataChanged. This isolates +// the empty-roles path of SortProxyModel::handleDataChanged from the sort-role path. +class ControllableStringListModel : public QAbstractListModel +{ +public: + explicit ControllableStringListModel(const QStringList &items, QObject *parent = nullptr) + : QAbstractListModel(parent), m_items(items) {} + int rowCount(const QModelIndex & = QModelIndex()) const override { return m_items.size(); } + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override + { + if (!index.isValid() || role != Qt::DisplayRole) + return {}; + return m_items.value(index.row()); + } + void setDataSilent(int row, const QString &value) { m_items[row] = value; } + void emitDataChanged(int row, const QVector &roles = {}) + { + const QModelIndex idx = index(row, 0); + emit dataChanged(idx, idx, roles); + } +private: + QStringList m_items; +}; +} + +class TestSortProxyModel : public QObject +{ + Q_OBJECT +private slots: + void ascendingAndDescendingOrder(); + void mappingBetweenSourceAndProxy(); + void caseSensitivity(); + void sortByCustomRole(); + void rowsInsertedKeepsOrder(); + void rowsRemovedKeepsOrder(); + void dataChangedReorders(); + void replaceSourceModelResets(); + void sortColumnMinusOneRestoresNaturalOrder(); + void setSortColumnToSameIsNoop(); + void indexWithNoSourceModel(); + void indexWithOutOfRangeColumn(); + void dataWithInvalidProxyIndex(); + void mapToSourceWithInvalidIndex(); + void mapFromSourceWithInvalidIndex(); + void mapFromSourceWithParentReturnsEmpty(); + void handleModelReset(); + void removeMultipleConsecutiveRows(); + void dataChangedWithEmptyRolesReorders(); + void dataChangedWithNonSortRoleDoesNotReorder(); + void columnCountWithAndWithoutSource(); + void sortColumnAndSortOrderGetters(); +}; + +void TestSortProxyModel::ascendingAndDescendingOrder() +{ + qCInfo(logTest) << "SortProxyModel should sort ascending and descending by the display role"; + QStandardItemModel source; + source.appendRow(new QStandardItem(QStringLiteral("cherry"))); + source.appendRow(new QStandardItem(QStringLiteral("apple"))); + source.appendRow(new QStandardItem(QStringLiteral("banana"))); + + SortProxyModel proxy; + proxy.setSourceModel(&source); + proxy.sort(0, Qt::AscendingOrder); + + QCOMPARE(proxy.rowCount(), 3); + QCOMPARE(proxy.data(proxy.index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("apple")); + QCOMPARE(proxy.data(proxy.index(1, 0), Qt::DisplayRole).toString(), QStringLiteral("banana")); + QCOMPARE(proxy.data(proxy.index(2, 0), Qt::DisplayRole).toString(), QStringLiteral("cherry")); + + proxy.sort(0, Qt::DescendingOrder); + QCOMPARE(proxy.data(proxy.index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("cherry")); + QCOMPARE(proxy.data(proxy.index(1, 0), Qt::DisplayRole).toString(), QStringLiteral("banana")); + QCOMPARE(proxy.data(proxy.index(2, 0), Qt::DisplayRole).toString(), QStringLiteral("apple")); +} + +void TestSortProxyModel::mappingBetweenSourceAndProxy() +{ + qCInfo(logTest) << "mapFromSource/mapToSource should be consistent with the sort order"; + QStandardItemModel source; + source.appendRow(new QStandardItem(QStringLiteral("cherry"))); // source row 0 + source.appendRow(new QStandardItem(QStringLiteral("apple"))); // source row 1 + source.appendRow(new QStandardItem(QStringLiteral("banana"))); // source row 2 + + SortProxyModel proxy; + proxy.setSourceModel(&source); + proxy.sort(0, Qt::AscendingOrder); + // expected proxy order: apple(1), banana(2), cherry(0) + + QCOMPARE(proxy.mapFromSource(source.index(0, 0)).row(), 2); // cherry -> proxy 2 + QCOMPARE(proxy.mapFromSource(source.index(1, 0)).row(), 0); // apple -> proxy 0 + + QCOMPARE(proxy.mapToSource(proxy.index(0, 0)).row(), 1); // proxy 0 -> apple + QCOMPARE(proxy.mapToSource(proxy.index(2, 0)).row(), 0); // proxy 2 -> cherry +} + +void TestSortProxyModel::caseSensitivity() +{ + qCInfo(logTest) << "SortProxyModel should honor sortCaseSensitivity"; + QStandardItemModel source; + source.appendRow(new QStandardItem(QStringLiteral("apple"))); + source.appendRow(new QStandardItem(QStringLiteral("Banana"))); + source.appendRow(new QStandardItem(QStringLiteral("cherry"))); + + SortProxyModel proxy; + proxy.setSourceModel(&source); + QCOMPARE(proxy.sortCaseSensitivity(), Qt::CaseSensitive); + proxy.sort(0, Qt::AscendingOrder); + // case sensitive: 'B'(66) sorts before 'a'(97) + QCOMPARE(proxy.data(proxy.index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("Banana")); + + proxy.setSortCaseSensitivity(Qt::CaseInsensitive); + QCOMPARE(proxy.sortCaseSensitivity(), Qt::CaseInsensitive); + QCOMPARE(proxy.data(proxy.index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("apple")); + QCOMPARE(proxy.data(proxy.index(1, 0), Qt::DisplayRole).toString(), QStringLiteral("Banana")); +} + +void TestSortProxyModel::sortByCustomRole() +{ + qCInfo(logTest) << "SortProxyModel should sort by a custom sortRole"; + constexpr int WeightRole = Qt::UserRole + 1; + QStandardItemModel source; + source.setItemRoleNames({{Qt::DisplayRole, QByteArrayLiteral("display")}, + {WeightRole, QByteArrayLiteral("weight")}}); + + auto makeItem = [](const QString &name, int weight) { + auto item = new QStandardItem(name); + item->setData(weight, WeightRole); + return item; + }; + source.appendRow(makeItem(QStringLiteral("heavy"), 30)); + source.appendRow(makeItem(QStringLiteral("light"), 5)); + source.appendRow(makeItem(QStringLiteral("medium"), 20)); + + SortProxyModel proxy; + proxy.setSourceModel(&source); + proxy.setSortRole(WeightRole); + proxy.sort(0, Qt::AscendingOrder); + QCOMPARE(proxy.sortRole(), WeightRole); + QCOMPARE(proxy.data(proxy.index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("light")); + QCOMPARE(proxy.data(proxy.index(1, 0), Qt::DisplayRole).toString(), QStringLiteral("medium")); + QCOMPARE(proxy.data(proxy.index(2, 0), Qt::DisplayRole).toString(), QStringLiteral("heavy")); +} + +void TestSortProxyModel::rowsInsertedKeepsOrder() +{ + qCInfo(logTest) << "Inserted source rows should appear at their sorted position"; + QStandardItemModel source; + source.appendRow(new QStandardItem(QStringLiteral("apple"))); + source.appendRow(new QStandardItem(QStringLiteral("cherry"))); + + SortProxyModel proxy; + proxy.setSourceModel(&source); + proxy.sort(0, Qt::AscendingOrder); + QCOMPARE(proxy.rowCount(), 2); + + QSignalSpy insertedSpy(&proxy, &QAbstractItemModel::rowsInserted); + source.appendRow(new QStandardItem(QStringLiteral("banana"))); + QVERIFY(insertedSpy.count() >= 1); + QCOMPARE(proxy.rowCount(), 3); + QCOMPARE(proxy.data(proxy.index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("apple")); + QCOMPARE(proxy.data(proxy.index(1, 0), Qt::DisplayRole).toString(), QStringLiteral("banana")); + QCOMPARE(proxy.data(proxy.index(2, 0), Qt::DisplayRole).toString(), QStringLiteral("cherry")); +} + +void TestSortProxyModel::rowsRemovedKeepsOrder() +{ + qCInfo(logTest) << "Removed source rows should disappear while keeping the rest sorted"; + QStandardItemModel source; + source.appendRow(new QStandardItem(QStringLiteral("apple"))); + source.appendRow(new QStandardItem(QStringLiteral("banana"))); + source.appendRow(new QStandardItem(QStringLiteral("cherry"))); + + SortProxyModel proxy; + proxy.setSourceModel(&source); + proxy.sort(0, Qt::AscendingOrder); + + QSignalSpy removedSpy(&proxy, &QAbstractItemModel::rowsRemoved); + source.removeRow(1); // remove "banana" + QVERIFY(removedSpy.count() >= 1); + QCOMPARE(proxy.rowCount(), 2); + QCOMPARE(proxy.data(proxy.index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("apple")); + QCOMPARE(proxy.data(proxy.index(1, 0), Qt::DisplayRole).toString(), QStringLiteral("cherry")); +} + +void TestSortProxyModel::dataChangedReorders() +{ + qCInfo(logTest) << "Changing source data should reorder the proxy"; + QStandardItemModel source; + source.appendRow(new QStandardItem(QStringLiteral("apple"))); + source.appendRow(new QStandardItem(QStringLiteral("banana"))); + source.appendRow(new QStandardItem(QStringLiteral("cherry"))); + + SortProxyModel proxy; + proxy.setSourceModel(&source); + proxy.sort(0, Qt::AscendingOrder); + // order: apple, banana, cherry + + source.item(1)->setText(QStringLiteral("zzz")); + // expected order after re-sort: apple, cherry, zzz + QCOMPARE(proxy.data(proxy.index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("apple")); + QCOMPARE(proxy.data(proxy.index(1, 0), Qt::DisplayRole).toString(), QStringLiteral("cherry")); + QCOMPARE(proxy.data(proxy.index(2, 0), Qt::DisplayRole).toString(), QStringLiteral("zzz")); +} + +void TestSortProxyModel::replaceSourceModelResets() +{ + qCInfo(logTest) << "Replacing the source model should reset to the new rows"; + QStandardItemModel sourceA; + sourceA.appendRow(new QStandardItem(QStringLiteral("zeta"))); + sourceA.appendRow(new QStandardItem(QStringLiteral("alpha"))); + + SortProxyModel proxy; + proxy.setSourceModel(&sourceA); + proxy.sort(0, Qt::AscendingOrder); + QCOMPARE(proxy.rowCount(), 2); + QCOMPARE(proxy.data(proxy.index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("alpha")); + + QStandardItemModel sourceB; + sourceB.appendRow(new QStandardItem(QStringLiteral("delta"))); + sourceB.appendRow(new QStandardItem(QStringLiteral("bravo"))); + sourceB.appendRow(new QStandardItem(QStringLiteral("charlie"))); + proxy.setSourceModel(&sourceB); + proxy.sort(0, Qt::AscendingOrder); + QCOMPARE(proxy.rowCount(), 3); + QCOMPARE(proxy.data(proxy.index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("bravo")); + QCOMPARE(proxy.data(proxy.index(1, 0), Qt::DisplayRole).toString(), QStringLiteral("charlie")); + QCOMPARE(proxy.data(proxy.index(2, 0), Qt::DisplayRole).toString(), QStringLiteral("delta")); +} + +void TestSortProxyModel::sortColumnMinusOneRestoresNaturalOrder() +{ + qCInfo(logTest) << "sort(-1) should disable sorting and restore natural source order"; + QStandardItemModel source; + source.appendRow(new QStandardItem(QStringLiteral("cherry"))); // source row 0 + source.appendRow(new QStandardItem(QStringLiteral("apple"))); // source row 1 + source.appendRow(new QStandardItem(QStringLiteral("banana"))); // source row 2 + + SortProxyModel proxy; + proxy.setSourceModel(&source); + proxy.sort(0, Qt::AscendingOrder); + // ascending: apple(1), banana(2), cherry(0) + QCOMPARE(proxy.data(proxy.index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("apple")); + QCOMPARE(proxy.data(proxy.index(1, 0), Qt::DisplayRole).toString(), QStringLiteral("banana")); + QCOMPARE(proxy.data(proxy.index(2, 0), Qt::DisplayRole).toString(), QStringLiteral("cherry")); + + // column -1 disables sorting: reorder() falls back to identity (natural source order) + proxy.sort(-1); + QCOMPARE(proxy.data(proxy.index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("cherry")); + QCOMPARE(proxy.data(proxy.index(1, 0), Qt::DisplayRole).toString(), QStringLiteral("apple")); + QCOMPARE(proxy.data(proxy.index(2, 0), Qt::DisplayRole).toString(), QStringLiteral("banana")); +} + +void TestSortProxyModel::setSortColumnToSameIsNoop() +{ + qCInfo(logTest) << "setSortColumn to the same column should be a noop (no signal, no reorder)"; + QStandardItemModel source; + source.appendRow(new QStandardItem(QStringLiteral("cherry"))); + source.appendRow(new QStandardItem(QStringLiteral("apple"))); + source.appendRow(new QStandardItem(QStringLiteral("banana"))); + + SortProxyModel proxy; + proxy.setSourceModel(&source); + proxy.sort(0, Qt::AscendingOrder); + QCOMPARE(proxy.sortColumn(), 0); + QCOMPARE(proxy.sortOrder(), Qt::AscendingOrder); + + // setSortColumn to the same column -> no signal emitted + QSignalSpy spy(&proxy, &SortProxyModel::sortColumnChanged); + proxy.setSortColumn(0); + QCOMPARE(spy.count(), 0); + + // order unchanged + QCOMPARE(proxy.rowCount(), 3); + QCOMPARE(proxy.data(proxy.index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("apple")); +} + +void TestSortProxyModel::indexWithNoSourceModel() +{ + qCInfo(logTest) << "index() with no source model should return invalid"; + SortProxyModel proxy; + QVERIFY(!proxy.index(0, 0).isValid()); + QCOMPARE(proxy.rowCount(), 0); +} + +void TestSortProxyModel::indexWithOutOfRangeColumn() +{ + qCInfo(logTest) << "index() with out-of-range column should return invalid"; + QStandardItemModel source; + source.appendRow(new QStandardItem(QStringLiteral("a"))); + + SortProxyModel proxy; + proxy.setSourceModel(&source); + QVERIFY(!proxy.index(0, 5).isValid()); // column 5 out of range + QVERIFY(!proxy.index(0, -1).isValid()); // negative column +} + +void TestSortProxyModel::dataWithInvalidProxyIndex() +{ + qCInfo(logTest) << "data() with invalid proxy index should return empty QVariant"; + QStandardItemModel source; + source.appendRow(new QStandardItem(QStringLiteral("a"))); + + SortProxyModel proxy; + proxy.setSourceModel(&source); + QVERIFY(!proxy.data(QModelIndex(), Qt::DisplayRole).isValid()); +} + +void TestSortProxyModel::mapToSourceWithInvalidIndex() +{ + qCInfo(logTest) << "mapToSource() with invalid index should return invalid"; + QStandardItemModel source; + source.appendRow(new QStandardItem(QStringLiteral("a"))); + + SortProxyModel proxy; + proxy.setSourceModel(&source); + QVERIFY(!proxy.mapToSource(QModelIndex()).isValid()); +} + +void TestSortProxyModel::mapFromSourceWithInvalidIndex() +{ + qCInfo(logTest) << "mapFromSource() with invalid index should return invalid"; + QStandardItemModel source; + source.appendRow(new QStandardItem(QStringLiteral("a"))); + + SortProxyModel proxy; + proxy.setSourceModel(&source); + QVERIFY(!proxy.mapFromSource(QModelIndex()).isValid()); +} + +void TestSortProxyModel::mapFromSourceWithParentReturnsEmpty() +{ + qCInfo(logTest) << "mapFromSource() with a parent index should return empty (flat model)"; + QStandardItemModel source; + source.appendRow(new QStandardItem(QStringLiteral("a"))); + + SortProxyModel proxy; + proxy.setSourceModel(&source); + // a child index (has parent) should return empty + QModelIndex childIdx = source.index(0, 0, source.index(0, 0)); + if (childIdx.isValid()) { + QVERIFY(!proxy.mapFromSource(childIdx).isValid()); + } +} + +void TestSortProxyModel::handleModelReset() +{ + qCInfo(logTest) << "source model reset should reset the proxy"; + QStandardItemModel source; + source.appendRow(new QStandardItem(QStringLiteral("cherry"))); + source.appendRow(new QStandardItem(QStringLiteral("apple"))); + + SortProxyModel proxy; + proxy.setSourceModel(&source); + proxy.sort(0, Qt::AscendingOrder); + QCOMPARE(proxy.rowCount(), 2); + + // reset source model + source.clear(); + QCOMPARE(proxy.rowCount(), 0); + + // add items again + source.appendRow(new QStandardItem(QStringLiteral("banana"))); + source.appendRow(new QStandardItem(QStringLiteral("apple"))); + QCOMPARE(proxy.rowCount(), 2); + QCOMPARE(proxy.data(proxy.index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("apple")); +} + +void TestSortProxyModel::removeMultipleConsecutiveRows() +{ + qCInfo(logTest) << "removing multiple consecutive source rows should keep proxy sorted"; + QStandardItemModel source; + source.appendRow(new QStandardItem(QStringLiteral("a"))); + source.appendRow(new QStandardItem(QStringLiteral("b"))); + source.appendRow(new QStandardItem(QStringLiteral("c"))); + source.appendRow(new QStandardItem(QStringLiteral("d"))); + source.appendRow(new QStandardItem(QStringLiteral("e"))); + + SortProxyModel proxy; + proxy.setSourceModel(&source); + proxy.sort(0, Qt::AscendingOrder); + QCOMPARE(proxy.rowCount(), 5); + + // remove rows 1 and 2 (b and c) in one operation + source.removeRows(1, 2); + QCOMPARE(proxy.rowCount(), 3); + QCOMPARE(proxy.data(proxy.index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("a")); + QCOMPARE(proxy.data(proxy.index(1, 0), Qt::DisplayRole).toString(), QStringLiteral("d")); + QCOMPARE(proxy.data(proxy.index(2, 0), Qt::DisplayRole).toString(), QStringLiteral("e")); +} + +void TestSortProxyModel::dataChangedWithEmptyRolesReorders() +{ + qCInfo(logTest) << "dataChanged with empty roles should trigger reorder (isolated from sort-role path)"; + // Use ControllableStringListModel to change data WITHOUT auto-emitting dataChanged, + // then emit dataChanged with empty roles to trigger the empty-roles reorder path. + ControllableStringListModel source({QStringLiteral("apple"), + QStringLiteral("banana"), + QStringLiteral("cherry")}); + + SortProxyModel proxy; + proxy.setSourceModel(&source); + proxy.sort(0, Qt::AscendingOrder); + // order: apple(0), banana(1), cherry(2) + QCOMPARE(proxy.data(proxy.index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("apple")); + QCOMPARE(proxy.data(proxy.index(1, 0), Qt::DisplayRole).toString(), QStringLiteral("banana")); + QCOMPARE(proxy.data(proxy.index(2, 0), Qt::DisplayRole).toString(), QStringLiteral("cherry")); + + // silently change banana to zzz (no dataChanged emitted -> no auto-reorder) + source.setDataSilent(1, QStringLiteral("zzz")); + + // now emit dataChanged with empty roles -> handleDataChanged sees roles.isEmpty() -> reorder + source.emitDataChanged(1, {}); + + // expected order after reorder: apple(0), cherry(2), zzz(1) + QCOMPARE(proxy.data(proxy.index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("apple")); + QCOMPARE(proxy.data(proxy.index(1, 0), Qt::DisplayRole).toString(), QStringLiteral("cherry")); + QCOMPARE(proxy.data(proxy.index(2, 0), Qt::DisplayRole).toString(), QStringLiteral("zzz")); +} + +void TestSortProxyModel::dataChangedWithNonSortRoleDoesNotReorder() +{ + qCInfo(logTest) << "dataChanged with non-sort role should not reorder"; + constexpr int UserRole1 = Qt::UserRole + 1; + QStandardItemModel source; + source.appendRow(new QStandardItem(QStringLiteral("apple"))); + source.appendRow(new QStandardItem(QStringLiteral("banana"))); + + SortProxyModel proxy; + proxy.setSourceModel(&source); + proxy.sort(0, Qt::AscendingOrder); + // order: apple, banana + + // change a non-sort role on apple -> should NOT reorder + source.item(0)->setData(42, UserRole1); + emit source.dataChanged(source.index(0, 0), source.index(0, 0), { UserRole1 }); + + // order unchanged + QCOMPARE(proxy.data(proxy.index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("apple")); + QCOMPARE(proxy.data(proxy.index(1, 0), Qt::DisplayRole).toString(), QStringLiteral("banana")); +} + +void TestSortProxyModel::columnCountWithAndWithoutSource() +{ + qCInfo(logTest) << "columnCount should return source column count or 0"; + SortProxyModel proxy; + QCOMPARE(proxy.columnCount(), 0); + + QStandardItemModel source; + source.appendRow(new QStandardItem(QStringLiteral("a"))); + proxy.setSourceModel(&source); + QCOMPARE(proxy.columnCount(), 1); +} + +void TestSortProxyModel::sortColumnAndSortOrderGetters() +{ + qCInfo(logTest) << "sortColumn and sortOrder getters should return current values"; + QStandardItemModel source; + source.appendRow(new QStandardItem(QStringLiteral("a"))); + + SortProxyModel proxy; + proxy.setSourceModel(&source); + QCOMPARE(proxy.sortColumn(), -1); // not yet sorted + QCOMPARE(proxy.sortOrder(), Qt::AscendingOrder); // default + + proxy.sort(0, Qt::DescendingOrder); + QCOMPARE(proxy.sortColumn(), 0); + QCOMPARE(proxy.sortOrder(), Qt::DescendingOrder); +} + +QTEST_MAIN(TestSortProxyModel) +#include "sortproxymodeltest.moc" From 0a3396ff91f5e1fce9713ea9d73cb64a9ad226a8 Mon Sep 17 00:00:00 2001 From: Fangxun Zhao Date: Wed, 9 Sep 2026 16:43:30 +0800 Subject: [PATCH 2/2] test: unify test gate to BUILD_TESTING and add coverage target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Rename the test gate option from BUILD_TEST to BUILD_TESTING 2. Enable coverage instrumentation for test targets by default 3. Add coverage target that runs ctest and generates the lcov report Log: Unify the test gate to BUILD_TESTING and add a coverage target test: 统一测试门控为 BUILD_TESTING 并新增 coverage 目标 1. 将测试门控选项由 BUILD_TEST 重命名为 BUILD_TESTING 2. 默认开启测试目标的覆盖率插桩 3. 新增 coverage 目标,执行 ctest 并生成 lcov 报告 Log: 统一测试门控为 BUILD_TESTING,并新增 coverage 目标 --- CMakeLists.txt | 4 +- tests/CMakeLists.txt | 46 ++++++++++++ tests/blurhashtest.cpp | 13 +--- tests/categoryutilstest.cpp | 30 +------- tests/countlimitproxymodeltest.cpp | 14 +--- tests/favoritedproxymodeltest.cpp | 6 +- tests/freesortproxymodeltest.cpp | 9 +-- tests/frequentlyusedproxymodeltest.cpp | 4 +- tests/gioappinfotest.cpp | 10 +-- tests/iconutilstest.cpp | 4 +- tests/itemarrangementproxymodeltest.cpp | 6 +- tests/itemspagemodeltest.cpp | 8 +-- tests/itemspagetest.cpp | 24 ------- tests/multipagesortfilterproxymodeltest.cpp | 10 +-- tests/recentlyinstalledproxymodeltest.cpp | 7 +- tests/searchfilterproxymodeltest.cpp | 79 +++------------------ tests/sortproxymodeltest.cpp | 26 +------ 17 files changed, 75 insertions(+), 225 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 07393160..d1bef50b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ cmake_minimum_required(VERSION 3.10) project(dde-launchpad VERSION 0.7.0) -option(BUILD_TEST "Whether or not to build the tests" OFF) +option(BUILD_TESTING "Whether or not to build the tests" OFF) option(CMAKE_EXPORT_COMPILE_COMMANDS "clangd support" ON) set(CMAKE_CXX_STANDARD 17) # blurhash requires 17, otherwish we can still use 14 @@ -65,7 +65,7 @@ add_subdirectory(src/treelandintegration) add_subdirectory(src/models) add_subdirectory(qml/windowed) -if(BUILD_TEST) +if(BUILD_TESTING) enable_testing() add_subdirectory(tests) endif() diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e18ab360..6b9c2047 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -8,6 +8,15 @@ find_package(Qt6Core CONFIG REQUIRED Private) # sortproxymodel.cpp uses QAb qt_standard_project_setup() +# Coverage instrumentation (on by default so `coverage` produces a report). +# Only the test-directory targets (OBJECT libs + test executables) are affected; +# the production `launchpadcommon` shared library is left untouched. +option(ENABLE_COVERAGE "Enable coverage instrumentation for tests" ON) +if(ENABLE_COVERAGE) + add_compile_options(--coverage) + add_link_options(--coverage) +endif() + # --------------------------------------------------------------------------- # Tested-source OBJECT libraries # @@ -72,6 +81,14 @@ target_link_libraries(launchpad-test-utils PUBLIC macro(launchpad_add_tests) foreach(_testname ${ARGN}) qt_add_executable(launchpad-${_testname} ${_testname}.cpp) + # Test sources include the headers under test by bare name (e.g. + # `#include "categoryutils.h"`), matching how src/ includes its own + # headers — no `../src/...` relative paths. + target_include_directories(launchpad-${_testname} PRIVATE + ${CMAKE_SOURCE_DIR}/src/models + ${CMAKE_SOURCE_DIR}/src/utils + ${CMAKE_SOURCE_DIR}/src/gioutils + ) # OBJECT libs first: their symbols take precedence over launchpadcommon.so, # so the gcov-instrumented copies are the ones that execute at test time. target_link_libraries(launchpad-${_testname} PRIVATE @@ -83,6 +100,7 @@ macro(launchpad_add_tests) launchpadcommon ) add_test(NAME launchpad-${_testname} COMMAND launchpad-${_testname}) + list(APPEND LAUNCHPAD_TEST_TARGETS launchpad-${_testname}) endforeach() endmacro() @@ -103,3 +121,31 @@ launchpad_add_tests( iconutilstest itemspagemodeltest ) + +# ---- coverage: execute all tests and generate a coverage report ---- +# Produces: ${CMAKE_BINARY_DIR}/coverage_report/index.html +find_program(LCOV_BIN lcov) +find_program(GENHTML_BIN genhtml) +set(_run_test_coverage "") +if(LCOV_BIN AND GENHTML_BIN) + set(_run_test_coverage + COMMAND ${LCOV_BIN} --capture --directory ${CMAKE_BINARY_DIR} + --rc lcov_branch_coverage=1 --output-file ${CMAKE_BINARY_DIR}/coverage.info + COMMAND ${LCOV_BIN} --extract ${CMAKE_BINARY_DIR}/coverage.info + --rc lcov_branch_coverage=1 + "${CMAKE_SOURCE_DIR}/src/models/*" + "${CMAKE_SOURCE_DIR}/src/utils/*" + "${CMAKE_SOURCE_DIR}/src/gioutils/*" + --output-file ${CMAKE_BINARY_DIR}/coverage_filtered.info + COMMAND ${GENHTML_BIN} ${CMAKE_BINARY_DIR}/coverage_filtered.info + --branch-coverage --output-directory ${CMAKE_BINARY_DIR}/coverage_report + ) +endif() +add_custom_target(coverage + COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure + ${_run_test_coverage} + DEPENDS ${LAUNCHPAD_TEST_TARGETS} + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + COMMENT "Run all tests and generate coverage report" + VERBATIM +) diff --git a/tests/blurhashtest.cpp b/tests/blurhashtest.cpp index a0fdb378..cc75d893 100644 --- a/tests/blurhashtest.cpp +++ b/tests/blurhashtest.cpp @@ -3,14 +3,12 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include -#include -#include "../src/utils/blurhash.hpp" +#include "blurhash.hpp" #include namespace { -Q_LOGGING_CATEGORY(logTest, "dde.launchpad.test") std::vector solidImage(size_t width, size_t height, unsigned char r, unsigned char g, unsigned char b) @@ -48,7 +46,6 @@ private slots: void TestBlurhash::decodeEmptyStringReturnsEmpty() { - qCInfo(logTest) << "Decoding an empty hash should return an empty image"; const auto img = blurhash::decode(std::string_view{}, 8, 8); QVERIFY(img.image.empty()); QCOMPARE(img.width, size_t(0)); @@ -57,14 +54,12 @@ void TestBlurhash::decodeEmptyStringReturnsEmpty() void TestBlurhash::decodeInvalidHashReturnsEmpty() { - qCInfo(logTest) << "Decoding a hash with invalid characters should return an empty image"; const auto img = blurhash::decode(std::string_view("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"), 8, 8); QVERIFY(img.image.empty()); } void TestBlurhash::encodeRejectsInvalidInput() { - qCInfo(logTest) << "encode should return an empty string for invalid parameters"; std::vector image = solidImage(4, 4, 255, 0, 0); QVERIFY(blurhash::encode(image.data(), 0, 4, 1, 1).empty()); // width 0 QVERIFY(blurhash::encode(nullptr, 4, 4, 1, 1).empty()); // null image @@ -74,7 +69,6 @@ void TestBlurhash::encodeRejectsInvalidInput() void TestBlurhash::encodedHashHasExpectedLength() { - qCInfo(logTest) << "encode should produce a hash of the documented length"; std::vector image = solidImage(8, 8, 128, 64, 200); QCOMPARE(blurhash::encode(image.data(), 8, 8, 1, 1).size(), size_t(6)); // no AC components QCOMPARE(blurhash::encode(image.data(), 8, 8, 1, 3).size(), expectedHashLength(1, 3)); // 10 @@ -84,7 +78,6 @@ void TestBlurhash::encodedHashHasExpectedLength() void TestBlurhash::decodedImageHasExpectedDimensions() { - qCInfo(logTest) << "decode should produce an image matching the requested dimensions"; constexpr size_t width = 16, height = 16; std::vector image = solidImage(width, height, 128, 64, 200); const std::string hash = blurhash::encode(image.data(), width, height, 2, 2); @@ -98,7 +91,6 @@ void TestBlurhash::decodedImageHasExpectedDimensions() void TestBlurhash::roundTripPreservesAverageColor() { - qCInfo(logTest) << "Round-trip should preserve the average color (DC component)"; constexpr size_t width = 16, height = 16; const unsigned char r = 128, g = 64, b = 200; std::vector image = solidImage(width, height, r, g, b); @@ -125,7 +117,6 @@ void TestBlurhash::roundTripPreservesAverageColor() void TestBlurhash::decodeWithFourBytesPerPixel() { - qCInfo(logTest) << "decode with bytesPerPixel=4 should produce a larger buffer"; constexpr size_t width = 16, height = 16; std::vector image = solidImage(width, height, 128, 64, 200); const std::string hash = blurhash::encode(image.data(), width, height, 3, 3); @@ -142,7 +133,6 @@ void TestBlurhash::decodeWithFourBytesPerPixel() void TestBlurhash::decodeTooShortHashReturnsEmpty() { - qCInfo(logTest) << "decode with a hash shorter than 10 chars should return empty"; // hash must be >= 10 chars (1 + 1 + 4 + at least 4 for 2x2 components - 1 AC = 2 bytes -> 8, but 10 is the minimum check) const auto img = blurhash::decode(std::string_view("short"), 8, 8); QVERIFY(img.image.empty()); @@ -150,7 +140,6 @@ void TestBlurhash::decodeTooShortHashReturnsEmpty() void TestBlurhash::decodeWrongSizeForComponentsReturnsEmpty() { - qCInfo(logTest) << "decode with a hash whose length doesn't match the component count should return empty"; // 1 component char says 4x4 = 16 components -> needs 1+1+4+(16-1)*2 = 36 chars // but we'll give only 12 chars const auto img = blurhash::decode(std::string_view("LFE.}?a]a]a]a]a]a]a]"), 8, 8); diff --git a/tests/categoryutilstest.cpp b/tests/categoryutilstest.cpp index f8d5c9f6..b25ab81d 100644 --- a/tests/categoryutilstest.cpp +++ b/tests/categoryutilstest.cpp @@ -3,12 +3,10 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include -#include -#include "../src/utils/categoryutils.h" +#include "categoryutils.h" namespace { -Q_LOGGING_CATEGORY(logTest, "dde.launchpad.test") using C = CategoryUtils::Categorytype; } @@ -47,7 +45,6 @@ private slots: void TestCategoryUtils::parseDDECategoryStringKnown() { - qCInfo(logTest) << "parseDDECategoryString should map the known DDE category names"; QCOMPARE(int(CategoryUtils::parseDDECategoryString(QStringLiteral("music"))), int(C::CategoryMusic)); QCOMPARE(int(CategoryUtils::parseDDECategoryString(QStringLiteral("internet"))), int(C::CategoryInternet)); QCOMPARE(int(CategoryUtils::parseDDECategoryString(QStringLiteral("development"))), int(C::CategoryDevelopment)); @@ -56,14 +53,12 @@ void TestCategoryUtils::parseDDECategoryStringKnown() void TestCategoryUtils::parseDDECategoryStringUnknown() { - qCInfo(logTest) << "Unknown DDE category strings should yield CategoryErr"; QCOMPARE(int(CategoryUtils::parseDDECategoryString(QStringLiteral("unknown"))), int(C::CategoryErr)); QCOMPARE(int(CategoryUtils::parseDDECategoryString(QStringLiteral("Music"))), int(C::CategoryErr)); // case sensitive } void TestCategoryUtils::parseXdgCategoryStringSingle() { - qCInfo(logTest) << "parseXdgCategoryString should map single-valued XDG categories"; const auto result = CategoryUtils::parseXdgCategoryString(QStringLiteral("webbrowser")); QCOMPARE(result.size(), 1); QVERIFY(result.contains(C::CategoryInternet)); @@ -71,7 +66,6 @@ void TestCategoryUtils::parseXdgCategoryStringSingle() void TestCategoryUtils::parseXdgCategoryStringMultiValued() { - qCInfo(logTest) << "audiovideo should map to both music and video"; const auto result = CategoryUtils::parseXdgCategoryString(QStringLiteral("audiovideo")); QCOMPARE(result.size(), 2); QVERIFY(result.contains(C::CategoryMusic)); @@ -80,14 +74,12 @@ void TestCategoryUtils::parseXdgCategoryStringMultiValued() void TestCategoryUtils::parseXdgCategoryStringUnknown() { - qCInfo(logTest) << "Unknown XDG category strings should yield an empty list"; const auto result = CategoryUtils::parseXdgCategoryString(QStringLiteral("totally-unknown-category")); QVERIFY(result.isEmpty()); } void TestCategoryUtils::bestMatchedSingleCategory() { - qCInfo(logTest) << "A single matching category should win"; QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("music")})), int(C::CategoryMusic)); QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("network")})), int(C::CategoryInternet)); QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("spreadsheet")})), int(C::CategoryOffice)); @@ -95,46 +87,39 @@ void TestCategoryUtils::bestMatchedSingleCategory() void TestCategoryUtils::bestMatchedEmptyReturnsOthers() { - qCInfo(logTest) << "An empty category list should be classified as others"; QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({})), int(C::CategoryOthers)); } void TestCategoryUtils::bestMatchedOthersOnlyReturnsOthers() { - qCInfo(logTest) << "An only-others list should be classified as others"; QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("others")})), int(C::CategoryOthers)); } void TestCategoryUtils::bestMatchedMusicVideoTiePrefersVideo() { - qCInfo(logTest) << "A music/video tie should prefer video"; QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("music"), QStringLiteral("video")})), int(C::CategoryVideo)); } void TestCategoryUtils::bestMatchedPlayerMapsToVideo() { - qCInfo(logTest) << "player (maps to music+video) should resolve to video"; QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("player")})), int(C::CategoryVideo)); } void TestCategoryUtils::bestMatchedMaxCountWins() { - qCInfo(logTest) << "The category with the highest count should win"; QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("game"), QStringLiteral("game"), QStringLiteral("music")})), int(C::CategoryGame)); } void TestCategoryUtils::bestMatchedMultipleInternet() { - qCInfo(logTest) << "Several internet categories should aggregate into internet"; QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("webbrowser"), QStringLiteral("email")})), int(C::CategoryInternet)); } void TestCategoryUtils::parseDDECategoryStringAllKnown() { - qCInfo(logTest) << "parseDDECategoryString should map all known DDE category names"; QCOMPARE(int(CategoryUtils::parseDDECategoryString(QStringLiteral("chat"))), int(C::CategoryChat)); QCOMPARE(int(CategoryUtils::parseDDECategoryString(QStringLiteral("video"))), int(C::CategoryVideo)); QCOMPARE(int(CategoryUtils::parseDDECategoryString(QStringLiteral("graphics"))), int(C::CategoryGraphics)); @@ -146,7 +131,6 @@ void TestCategoryUtils::parseDDECategoryStringAllKnown() void TestCategoryUtils::parseXdgCategoryStringMultipleDistinctCategories() { - qCInfo(logTest) << "parseXdgCategoryString for several distinct known categories"; // Each call returns the list for that single string; verify a batch of them QCOMPARE(CategoryUtils::parseXdgCategoryString(QStringLiteral("ide")).size(), 1); QVERIFY(CategoryUtils::parseXdgCategoryString(QStringLiteral("ide")).contains(C::CategoryDevelopment)); @@ -164,7 +148,6 @@ void TestCategoryUtils::parseXdgCategoryStringMultipleDistinctCategories() void TestCategoryUtils::parseXdgCategoryStringAudioMapsToMusic() { - qCInfo(logTest) << "audio should map to music"; const auto result = CategoryUtils::parseXdgCategoryString(QStringLiteral("audio")); QCOMPARE(result.size(), 1); QVERIFY(result.contains(C::CategoryMusic)); @@ -172,7 +155,6 @@ void TestCategoryUtils::parseXdgCategoryStringAudioMapsToMusic() void TestCategoryUtils::bestMatchedDDECategoryWinsOverXdg() { - qCInfo(logTest) << "A DDE category name should be matched before falling to XDG parsing"; // 'music' is a DDE name -> CategoryMusic (via parseDDECategoryString) // 'audio' is an XDG name -> CategoryMusic (via parseXdgCategoryString) // mixing them should still yield Music @@ -182,7 +164,6 @@ void TestCategoryUtils::bestMatchedDDECategoryWinsOverXdg() void TestCategoryUtils::bestMatchedGameVariants() { - qCInfo(logTest) << "Various game XDG categories should aggregate into game"; QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("actiongame"), QStringLiteral("arcadegame")})), int(C::CategoryGame)); QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("boardgame"), QStringLiteral("cardgame"), QStringLiteral("puzzlegame")})), @@ -191,7 +172,6 @@ void TestCategoryUtils::bestMatchedGameVariants() void TestCategoryUtils::bestMatchedDevelopmentVariants() { - qCInfo(logTest) << "Development XDG categories should aggregate into development"; QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("ide"), QStringLiteral("debugger")})), int(C::CategoryDevelopment)); QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("building"), QStringLiteral("revisioncontrol")})), @@ -200,14 +180,12 @@ void TestCategoryUtils::bestMatchedDevelopmentVariants() void TestCategoryUtils::bestMatchedReadingVariants() { - qCInfo(logTest) << "Reading XDG categories should aggregate into reading"; QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("news"), QStringLiteral("translation")})), int(C::CategoryReading)); } void TestCategoryUtils::bestMatchedSystemVariants() { - qCInfo(logTest) << "System XDG categories should aggregate into system"; QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("desktopsettings"), QStringLiteral("packagemanager")})), int(C::CategorySystem)); QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("terminalemulator"), QStringLiteral("filemanager")})), @@ -216,7 +194,6 @@ void TestCategoryUtils::bestMatchedSystemVariants() void TestCategoryUtils::bestMatchedGraphicsVariants() { - qCInfo(logTest) << "Graphics XDG categories should aggregate into graphics"; QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("2dgraphics"), QStringLiteral("rastergraphics")})), int(C::CategoryGraphics)); QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("photography"), QStringLiteral("viewer")})), @@ -225,14 +202,12 @@ void TestCategoryUtils::bestMatchedGraphicsVariants() void TestCategoryUtils::bestMatchedChatVariants() { - qCInfo(logTest) << "Chat XDG categories should aggregate into chat"; QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("instantmessaging"), QStringLiteral("contactmanagement")})), int(C::CategoryChat)); } void TestCategoryUtils::bestMatchedOfficeVariants() { - qCInfo(logTest) << "Office XDG categories should aggregate into office"; QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("spreadsheet"), QStringLiteral("presentation")})), int(C::CategoryOffice)); QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("science"), QStringLiteral("math")})), @@ -241,7 +216,6 @@ void TestCategoryUtils::bestMatchedOfficeVariants() void TestCategoryUtils::bestMatchedMixedUnknownAndKnown() { - qCInfo(logTest) << "Unknown categories mixed with known ones should ignore unknowns"; // 'totally-unknown' yields CategoryErr from DDE and empty from XDG -> ignored // 'music' yields CategoryMusic -> should win QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("totally-unknown"), QStringLiteral("music")})), @@ -250,14 +224,12 @@ void TestCategoryUtils::bestMatchedMixedUnknownAndKnown() void TestCategoryUtils::bestMatchedAudioVideoEditingTie() { - qCInfo(logTest) << "audiovideoediting maps to both music and video -> tie -> video"; QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("audiovideoediting")})), int(C::CategoryVideo)); } void TestCategoryUtils::bestMatchedRecorderTie() { - qCInfo(logTest) << "recorder maps to both music and video -> tie -> video"; QCOMPARE(int(CategoryUtils::parseBestMatchedCategory({QStringLiteral("recorder")})), int(C::CategoryVideo)); } diff --git a/tests/countlimitproxymodeltest.cpp b/tests/countlimitproxymodeltest.cpp index 37ca5da0..8c1fed41 100644 --- a/tests/countlimitproxymodeltest.cpp +++ b/tests/countlimitproxymodeltest.cpp @@ -6,12 +6,10 @@ #include #include #include -#include -#include "../src/models/countlimitproxymodel.h" +#include "countlimitproxymodel.h" namespace { -Q_LOGGING_CATEGORY(logTest, "dde.launchpad.test") } class TestCountLimitProxyModel : public QObject @@ -32,7 +30,6 @@ private slots: void TestCountLimitProxyModel::noLimitByDefault() { - qCInfo(logTest) << "CountLimitProxyModel should not limit rows when maxRowCount is unset"; QStandardItemModel source; CountLimitProxyModel proxy; QCOMPARE(proxy.maxRowCount(), -1); @@ -47,7 +44,6 @@ void TestCountLimitProxyModel::noLimitByDefault() void TestCountLimitProxyModel::maxRowCountLimitsVisibleRows() { - qCInfo(logTest) << "Setting maxRowCount should cap the visible rows and keep the first ones"; QStandardItemModel source; for (int i = 0; i < 5; ++i) source.appendRow(new QStandardItem(QStringLiteral("item-%1").arg(i))); @@ -68,7 +64,6 @@ void TestCountLimitProxyModel::maxRowCountLimitsVisibleRows() void TestCountLimitProxyModel::maxRowCountChangeEmitsSignal() { - qCInfo(logTest) << "Changing maxRowCount should emit maxRowCountChanged"; QStandardItemModel source; source.appendRow(new QStandardItem(QStringLiteral("a"))); CountLimitProxyModel proxy; @@ -86,7 +81,6 @@ void TestCountLimitProxyModel::maxRowCountChangeEmitsSignal() void TestCountLimitProxyModel::unchangedMaxRowCountDoesNotEmit() { - qCInfo(logTest) << "Setting the same maxRowCount should not emit the signal"; QStandardItemModel source; source.appendRow(new QStandardItem(QStringLiteral("a"))); CountLimitProxyModel proxy; @@ -100,7 +94,6 @@ void TestCountLimitProxyModel::unchangedMaxRowCountDoesNotEmit() void TestCountLimitProxyModel::sourceRowInsertionReevaluates() { - qCInfo(logTest) << "Inserting source rows should re-evaluate the limit"; QStandardItemModel source; CountLimitProxyModel proxy; proxy.setSourceModel(&source); @@ -117,7 +110,6 @@ void TestCountLimitProxyModel::sourceRowInsertionReevaluates() void TestCountLimitProxyModel::sourceRowRemovalReevaluates() { - qCInfo(logTest) << "Removing source rows should re-evaluate the limit and keep the first surviving rows"; QStandardItemModel source; for (int i = 0; i < 4; ++i) source.appendRow(new QStandardItem(QStringLiteral("item-%1").arg(i))); @@ -139,7 +131,6 @@ void TestCountLimitProxyModel::sourceRowRemovalReevaluates() void TestCountLimitProxyModel::maxRowCountEqualsSourceCount() { - qCInfo(logTest) << "maxRowCount == source row count should show all rows"; QStandardItemModel source; for (int i = 0; i < 3; ++i) source.appendRow(new QStandardItem(QStringLiteral("item-%1").arg(i))); @@ -154,7 +145,6 @@ void TestCountLimitProxyModel::maxRowCountEqualsSourceCount() void TestCountLimitProxyModel::maxRowCountExceedsSourceCount() { - qCInfo(logTest) << "maxRowCount > source row count should show all rows"; QStandardItemModel source; for (int i = 0; i < 2; ++i) source.appendRow(new QStandardItem(QStringLiteral("item-%1").arg(i))); @@ -167,7 +157,6 @@ void TestCountLimitProxyModel::maxRowCountExceedsSourceCount() void TestCountLimitProxyModel::setSourceModelWithNullDisconnects() { - qCInfo(logTest) << "setSourceModel(null) should disconnect from old model"; QStandardItemModel source; source.appendRow(new QStandardItem(QStringLiteral("a"))); @@ -187,7 +176,6 @@ void TestCountLimitProxyModel::setSourceModelWithNullDisconnects() void TestCountLimitProxyModel::setSameSourceModelIsNoop() { - qCInfo(logTest) << "setSourceModel with the same model should be a noop"; QStandardItemModel source; source.appendRow(new QStandardItem(QStringLiteral("a"))); diff --git a/tests/favoritedproxymodeltest.cpp b/tests/favoritedproxymodeltest.cpp index 00cf17d4..9867505b 100644 --- a/tests/favoritedproxymodeltest.cpp +++ b/tests/favoritedproxymodeltest.cpp @@ -6,16 +6,14 @@ #include #include #include -#include #include #include #include -#include "../src/models/favoritedproxymodel.h" -#include "../src/models/appsmodel.h" +#include "favoritedproxymodel.h" +#include "appsmodel.h" namespace { -Q_LOGGING_CATEGORY(logTest, "dde.launchpad.test") enum SourceRoles { SrcDesktopIdRole = Qt::UserRole + 1, diff --git a/tests/freesortproxymodeltest.cpp b/tests/freesortproxymodeltest.cpp index 0677195b..f9ede41a 100644 --- a/tests/freesortproxymodeltest.cpp +++ b/tests/freesortproxymodeltest.cpp @@ -5,13 +5,11 @@ #include #include #include -#include -#include "../src/models/freesortproxymodel.h" -#include "../src/models/itemarrangementproxymodel.h" +#include "freesortproxymodel.h" +#include "itemarrangementproxymodel.h" namespace { -Q_LOGGING_CATEGORY(logTest, "dde.launchpad.test") } class TestFreeSortProxyModel : public QObject @@ -33,7 +31,6 @@ static QStandardItem *makeArrangedItem(const QString &name, int page, int indexI void TestFreeSortProxyModel::sortByPageThenIndex() { - qCInfo(logTest) << "FreeSortProxyModel should sort by page then by index-in-page"; QStandardItemModel source; // intentionally appended out of order so the proxy actually has to sort source.appendRow(makeArrangedItem(QStringLiteral("A"), 1, 0)); @@ -55,7 +52,6 @@ void TestFreeSortProxyModel::sortByPageThenIndex() void TestFreeSortProxyModel::descendingOrder() { - qCInfo(logTest) << "FreeSortProxyModel should honor descending sort order"; QStandardItemModel source; source.appendRow(makeArrangedItem(QStringLiteral("A"), 1, 0)); source.appendRow(makeArrangedItem(QStringLiteral("B"), 0, 1)); @@ -75,7 +71,6 @@ void TestFreeSortProxyModel::descendingOrder() void TestFreeSortProxyModel::missingRolesDefaultToZero() { - qCInfo(logTest) << "Items without PageRole/IndexInPageRole should default to 0"; QStandardItemModel source; // items without PageRole or IndexInPageRole data -> toInt() returns 0 for both source.appendRow(new QStandardItem(QStringLiteral("X"))); diff --git a/tests/frequentlyusedproxymodeltest.cpp b/tests/frequentlyusedproxymodeltest.cpp index ce87ee39..e017d0b3 100644 --- a/tests/frequentlyusedproxymodeltest.cpp +++ b/tests/frequentlyusedproxymodeltest.cpp @@ -6,12 +6,10 @@ #include #include #include -#include -#include "../src/models/frequentlyusedproxymodel.h" +#include "frequentlyusedproxymodel.h" namespace { -Q_LOGGING_CATEGORY(logTest, "dde.launchpad.test") constexpr int NameRole = Qt::UserRole + 1; constexpr int DesktopIdRole = Qt::UserRole + 2; diff --git a/tests/gioappinfotest.cpp b/tests/gioappinfotest.cpp index 3e7d7489..3c31cb2b 100644 --- a/tests/gioappinfotest.cpp +++ b/tests/gioappinfotest.cpp @@ -1,14 +1,12 @@ -// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2024 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later #include -#include #include "appinfo.h" namespace { -Q_LOGGING_CATEGORY(logTest, "dde.launchpad.test") } class TestGioAppInfo: public QObject @@ -17,11 +15,9 @@ class TestGioAppInfo: public QObject public: TestGioAppInfo() { - qCDebug(logTest) << "Constructing TestGioAppInfo test object"; } ~TestGioAppInfo() { - qCDebug(logTest) << "Destroying TestGioAppInfo test object"; } private slots: @@ -30,15 +26,11 @@ private slots: void TestGioAppInfo::dfmInfo() { - qCInfo(logTest) << "Testing AppInfo::fullPathByDesktopId with dde-file-manager.desktop"; QString path = AppInfo::fullPathByDesktopId("dde-file-manager.desktop"); - qCDebug(logTest) << "Retrieved path:" << path; QVERIFY(QFile::exists(path)); - qCDebug(logTest) << "Verified that file exists at path:" << path; QVERIFY(!path.isEmpty()); - qCInfo(logTest) << "Verified path is not empty, test passed"; } QTEST_MAIN(TestGioAppInfo) diff --git a/tests/iconutilstest.cpp b/tests/iconutilstest.cpp index 06453f61..3684953c 100644 --- a/tests/iconutilstest.cpp +++ b/tests/iconutilstest.cpp @@ -3,14 +3,12 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include -#include -#include "../src/utils/iconutils.h" +#include "iconutils.h" #include namespace { -Q_LOGGING_CATEGORY(logTest, "dde.launchpad.test") } class TestIconUtils : public QObject diff --git a/tests/itemarrangementproxymodeltest.cpp b/tests/itemarrangementproxymodeltest.cpp index 749efed0..4b29a355 100644 --- a/tests/itemarrangementproxymodeltest.cpp +++ b/tests/itemarrangementproxymodeltest.cpp @@ -6,16 +6,14 @@ #include #include #include -#include #include #include #include -#include "../src/models/itemarrangementproxymodel.h" -#include "../src/models/appsmodel.h" +#include "itemarrangementproxymodel.h" +#include "appsmodel.h" namespace { -Q_LOGGING_CATEGORY(logTest, "dde.launchpad.test") enum SourceRoles { SrcDesktopIdRole = Qt::UserRole + 1, diff --git a/tests/itemspagemodeltest.cpp b/tests/itemspagemodeltest.cpp index c161472d..3987c32f 100644 --- a/tests/itemspagemodeltest.cpp +++ b/tests/itemspagemodeltest.cpp @@ -6,14 +6,12 @@ #include #include #include -#include -#include "../src/models/itemspagemodel.h" -#include "../src/models/itemarrangementproxymodel.h" -#include "../src/models/appsmodel.h" +#include "itemspagemodel.h" +#include "itemarrangementproxymodel.h" +#include "appsmodel.h" namespace { -Q_LOGGING_CATEGORY(logTest, "dde.launchpad.test") enum SourceRoles { SrcDesktopIdRole = Qt::UserRole + 1, diff --git a/tests/itemspagetest.cpp b/tests/itemspagetest.cpp index 87ff5271..42620be8 100644 --- a/tests/itemspagetest.cpp +++ b/tests/itemspagetest.cpp @@ -3,12 +3,10 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include -#include #include "itemspage.h" namespace { -Q_LOGGING_CATEGORY(logTest, "dde.launchpad.test") } class TestItemsPage: public QObject @@ -36,75 +34,53 @@ private slots: void TestItemsPage::insertAndRemove() { - qCInfo(logTest) << "Testing insertAndRemove functionality"; ItemsPage ip(3); - qCDebug(logTest) << "Created ItemsPage with max 3 items per page"; ip.appendPage({"a", "b", "c"}); - qCDebug(logTest) << "Appended first page with items a,b,c"; QVERIFY(ip.pageCount() == 1); ip.appendPage({"e"}); - qCDebug(logTest) << "Appended second page with item e"; QVERIFY(ip.pageCount() == 2); ip.insertItem("d", 1); - qCDebug(logTest) << "Inserted item d to page 1"; QCOMPARE(ip.items(0), QStringList({"a", "b", "c"})); QCOMPARE(ip.items(1), QStringList({"d", "e"})); ip.insertItem("^", 0, 1); - qCDebug(logTest) << "Inserted item ^ at page 0, position 1"; QCOMPARE(ip.items(0), QStringList({"a", "^", "b"})); QCOMPARE(ip.items(1), QStringList({"c", "d", "e"})); ip.removeItem("d"); - qCDebug(logTest) << "Removed item d"; QCOMPARE(ip.items(1), QStringList({"c", "e"})); - qCInfo(logTest) << "insertAndRemove test completed successfully"; } void TestItemsPage::autoRemoveEmptyPage() { - qCInfo(logTest) << "Testing autoRemoveEmptyPage functionality"; ItemsPage ip(4); - qCDebug(logTest) << "Created ItemsPage with max 4 items per page"; ip.appendPage({"1", "2", "3"}); ip.appendPage({"4"}); ip.appendPage({"5", "6", "7"}); - qCDebug(logTest) << "Created 3 pages with items: [1,2,3], [4], [5,6,7]"; ip.moveItemPosition(1, 0, 2, 1, true); - qCDebug(logTest) << "Moved item from page 1, pos 0 to page 2, pos 1"; QVERIFY(ip.pageCount() == 2); - qCDebug(logTest) << "Verified page count is 2 after move"; ip.appendPage({"8"}); - qCDebug(logTest) << "Appended page with item 8"; ip.moveItemPosition(2, 0, 0, 1, true); - qCDebug(logTest) << "Moved item from page 2, pos 0 to page 0, pos 1"; QVERIFY(ip.pageCount() == 2); - qCInfo(logTest) << "autoRemoveEmptyPage test completed successfully"; } void TestItemsPage::dragItemToFolder() { - qCInfo(logTest) << "Testing dragItemToFolder functionality"; ItemsPage dstFolder(3); - qCDebug(logTest) << "Created destination folder with max 3 items per page"; dstFolder.appendPage({"1", "2"}); - qCDebug(logTest) << "Added initial page with items 1,2"; dstFolder.insertItemToPage("3", -1); - qCDebug(logTest) << "Inserted item 3 to auto page (-1)"; QVERIFY(dstFolder.pageCount() == 1); - qCDebug(logTest) << "Verified page count is still 1"; QVERIFY(dstFolder.items(0) == QStringList({"1", "2", "3"})); - qCInfo(logTest) << "Verified page 0 contains items [1,2,3], dragItemToFolder test completed"; } void TestItemsPage::findItemReturnsValidPosition() diff --git a/tests/multipagesortfilterproxymodeltest.cpp b/tests/multipagesortfilterproxymodeltest.cpp index 48f7fe96..b9cce81d 100644 --- a/tests/multipagesortfilterproxymodeltest.cpp +++ b/tests/multipagesortfilterproxymodeltest.cpp @@ -5,13 +5,11 @@ #include #include #include -#include -#include "../src/models/multipagesortfilterproxymodel.h" -#include "../src/models/itemarrangementproxymodel.h" +#include "multipagesortfilterproxymodel.h" +#include "itemarrangementproxymodel.h" namespace { -Q_LOGGING_CATEGORY(logTest, "dde.launchpad.test") constexpr int NameRole = Qt::UserRole + 10; } @@ -47,7 +45,6 @@ static void populateSource(QStandardItemModel &source) void TestMultipageSortFilterProxyModel::filtersByFolderAndPage() { - qCInfo(logTest) << "Filter by folderId and pageId, sorted by page then index-in-page"; QStandardItemModel source; populateSource(source); @@ -91,7 +88,6 @@ void TestMultipageSortFilterProxyModel::filtersByFolderAndPage() void TestMultipageSortFilterProxyModel::noMatchYieldsEmpty() { - qCInfo(logTest) << "A folderId matching no source row should yield an empty proxy"; QStandardItemModel source; populateSource(source); @@ -106,7 +102,6 @@ void TestMultipageSortFilterProxyModel::noMatchYieldsEmpty() void TestMultipageSortFilterProxyModel::filterOnlyModeChangesSorting() { - qCInfo(logTest) << "filterOnlyMode=true, after a forced re-sort, should fall back to sortRole-only ordering"; QStandardItemModel source; populateSource(source); @@ -145,7 +140,6 @@ void TestMultipageSortFilterProxyModel::filterOnlyModeChangesSorting() void TestMultipageSortFilterProxyModel::filterOnlyModeToggleDoesNotAutoResort() { - qCInfo(logTest) << "Toggling filterOnlyMode alone must not re-sort (signal is not wired in source)"; QStandardItemModel source; populateSource(source); diff --git a/tests/recentlyinstalledproxymodeltest.cpp b/tests/recentlyinstalledproxymodeltest.cpp index e273f13c..34da2aa2 100644 --- a/tests/recentlyinstalledproxymodeltest.cpp +++ b/tests/recentlyinstalledproxymodeltest.cpp @@ -5,12 +5,10 @@ #include #include #include -#include -#include "../src/models/recentlyinstalledproxymodel.h" +#include "recentlyinstalledproxymodel.h" namespace { -Q_LOGGING_CATEGORY(logTest, "dde.launchpad.test") constexpr int NameRole = Qt::UserRole + 1; constexpr int InstalledTimeRole = Qt::UserRole + 2; @@ -59,7 +57,6 @@ static void configureProxy(RecentlyInstalledProxyModel &proxy, QStandardItemMode void TestRecentlyInstalledProxyModel::filtersAndSortsByInstalledTime() { - qCInfo(logTest) << "Only never-launched, installed apps should be kept, newest first"; QStandardItemModel source; populateSource(source); @@ -74,7 +71,6 @@ void TestRecentlyInstalledProxyModel::filtersAndSortsByInstalledTime() void TestRecentlyInstalledProxyModel::ascendingOrder() { - qCInfo(logTest) << "Ascending sort should order by installed time ascending"; QStandardItemModel source; populateSource(source); @@ -89,7 +85,6 @@ void TestRecentlyInstalledProxyModel::ascendingOrder() void TestRecentlyInstalledProxyModel::componentCompleteSortsDescending() { - qCInfo(logTest) << "componentComplete() should sort by installed time descending (newest first)"; QStandardItemModel source; populateSource(source); diff --git a/tests/searchfilterproxymodeltest.cpp b/tests/searchfilterproxymodeltest.cpp index b0e823ba..388ea243 100644 --- a/tests/searchfilterproxymodeltest.cpp +++ b/tests/searchfilterproxymodeltest.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2024 -2026 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2024 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later @@ -8,14 +8,12 @@ #include #include #include -#include -#include "../src/models/searchfilterproxymodel.h" -#include "../src/models/appsmodel.h" -#include "../src/models/categorizedsortproxymodel.h" +#include "searchfilterproxymodel.h" +#include "appsmodel.h" +#include "categorizedsortproxymodel.h" namespace { -Q_LOGGING_CATEGORY(logTest, "dde.launchpad.test") enum SourceRoles { SourceDesktopIdRole = Qt::UserRole + 1, @@ -67,22 +65,17 @@ private slots: void TestSearchFilterProxyModel::initTestCase() { - qCInfo(logTest) << "Initializing test case for SearchFilterProxyModel"; // 初始化测试环境 setupTestData(); - qCInfo(logTest) << "Test case initialization completed"; } void TestSearchFilterProxyModel::cleanupTestCase() { - qCInfo(logTest) << "Cleaning up test case for SearchFilterProxyModel"; // 清理测试环境 - qCInfo(logTest) << "Test case cleanup completed"; } void TestSearchFilterProxyModel::setupTestData() { - qCDebug(logTest) << "Setting up test data for SearchFilterProxyModel"; m_sourceModel.clear(); m_sourceModel.setItemRoleNames({ { SourceDesktopIdRole, QByteArrayLiteral("desktopId") }, @@ -137,7 +130,6 @@ void TestSearchFilterProxyModel::setupTestData() m_sourceModel.appendRow(item); AppsModel::instance().setSourceModel(&m_sourceModel); AppsModel::instance().setReady(true); - qCInfo(logTest) << "Added" << testItems.size() << "test items to AppsModel"; } QStandardItem *TestSearchFilterProxyModel::createTestAppItem(const QString &desktopId, @@ -147,11 +139,13 @@ QStandardItem *TestSearchFilterProxyModel::createTestAppItem(const QString &desk const QString &vendor, int launchedTimes) { - qCDebug(logTest) << "Creating test app item:" << desktopId << "name:" << name << "launched times:" << launchedTimes; auto item = new QStandardItem; item->setData(desktopId, SourceDesktopIdRole); item->setData(displayName.isEmpty() ? name : displayName, SourceNameRole); - item->setData(QStringLiteral("application-default-icon"), SourceIconNameRole); + // Empty icon name so AppMgr::waitForIcon() resolves it immediately and does + // not defer the row (a non-empty unresolvable icon would make the noDisplay + // unhide in testSharedModelAdapter() delayed, failing the rowCount check). + item->setData(QString(), SourceIconNameRole); item->setData(genericName, SourceGenericNameRole); item->setData(vendor, SourceVendorRole); item->setData(launchedTimes, SourceLaunchedTimesRole); @@ -161,208 +155,151 @@ QStandardItem *TestSearchFilterProxyModel::createTestAppItem(const QString &desk void TestSearchFilterProxyModel::testBasicSearch() { - qCInfo(logTest) << "Starting basic search tests"; SearchFilterProxyModel &model = SearchFilterProxyModel::instance(); // 测试空搜索 - qCDebug(logTest) << "Testing empty search"; model.setFilterRegularExpression(QRegularExpression("")); int totalItems = AppsModel::instance().rowCount(); - qCDebug(logTest) << "Empty search returned" << model.rowCount() << "items, expected" << totalItems; QCOMPARE(model.rowCount(), totalItems); // 测试基本搜索 - 完全匹配 - qCDebug(logTest) << "Testing exact match search for 'Calculator'"; model.setFilterRegularExpression(QRegularExpression("Calculator")); - qCDebug(logTest) << "Calculator search returned" << model.rowCount() << "items"; QCOMPARE(model.rowCount(), 1); QCOMPARE(model.data(model.index(0, 0), AppsModel::DesktopIdRole).toString(), "org.deepin.calculator.desktop"); // 测试基本搜索 - 部分匹配 - qCDebug(logTest) << "Testing partial match search for 'Text'"; model.setFilterRegularExpression(QRegularExpression("Text")); - qCDebug(logTest) << "Text search returned" << model.rowCount() << "items"; QCOMPARE(model.rowCount(), 1); QCOMPARE(model.data(model.index(0, 0), AppsModel::DesktopIdRole).toString(), "org.deepin.editor.desktop"); // 测试基本搜索 - 不区分大小写 - qCDebug(logTest) << "Testing case-insensitive search for 'calculator'"; model.setFilterRegularExpression(QRegularExpression("calculator")); - qCDebug(logTest) << "Case-insensitive search returned" << model.rowCount() << "items"; QCOMPARE(model.rowCount(), 1); QCOMPARE(model.data(model.index(0, 0), AppsModel::DesktopIdRole).toString(), "org.deepin.calculator.desktop"); // 测试基本搜索 - 多个结果 - qCDebug(logTest) << "Testing multi-result search for 'e'"; model.setFilterRegularExpression(QRegularExpression("e")); - qCDebug(logTest) << "Multi-result search returned" << model.rowCount() << "items"; QVERIFY(model.rowCount() > 1); - qCInfo(logTest) << "Basic search tests completed successfully"; } void TestSearchFilterProxyModel::testChineseSearch() { - qCInfo(logTest) << "Starting Chinese search tests"; SearchFilterProxyModel &model = SearchFilterProxyModel::instance(); // 测试中文完全匹配 - qCDebug(logTest) << "Testing Chinese exact match for '音乐'"; model.setFilterRegularExpression(QRegularExpression("音乐")); - qCDebug(logTest) << "Chinese search for '音乐' returned" << model.rowCount() << "items"; QCOMPARE(model.rowCount(), 1); QCOMPARE(model.data(model.index(0, 0), AppsModel::DesktopIdRole).toString(), "org.deepin.music.desktop"); // 测试中文部分匹配 - qCDebug(logTest) << "Testing Chinese partial match for '阅读'"; model.setFilterRegularExpression(QRegularExpression("阅读")); - qCDebug(logTest) << "Chinese search for '阅读' returned" << model.rowCount() << "items"; QCOMPARE(model.rowCount(), 1); QCOMPARE(model.data(model.index(0, 0), AppsModel::DesktopIdRole).toString(), "org.deepin.reader.desktop"); - qCInfo(logTest) << "Chinese search tests completed successfully"; } void TestSearchFilterProxyModel::testPinyinSearch() { - qCInfo(logTest) << "Starting Pinyin search tests"; SearchFilterProxyModel &model = SearchFilterProxyModel::instance(); // 测试拼音搜索 - qCDebug(logTest) << "Testing Pinyin search for 'yinyue'"; model.setFilterRegularExpression(QRegularExpression("yinyue")); - qCDebug(logTest) << "Pinyin search for 'yinyue' returned" << model.rowCount() << "items"; QCOMPARE(model.rowCount(), 1); QCOMPARE(model.data(model.index(0, 0), AppsModel::DesktopIdRole).toString(), "org.deepin.music.desktop"); // 测试拼音部分匹配 - qCDebug(logTest) << "Testing Pinyin partial match for 'yuedu'"; model.setFilterRegularExpression(QRegularExpression("yuedu")); - qCDebug(logTest) << "Pinyin search for 'yuedu' returned" << model.rowCount() << "items"; QCOMPARE(model.rowCount(), 1); QCOMPARE(model.data(model.index(0, 0), AppsModel::DesktopIdRole).toString(), "org.deepin.reader.desktop"); // 测试拼音首字母匹配 - qCDebug(logTest) << "Testing Pinyin initials match for 'rl'"; model.setFilterRegularExpression(QRegularExpression("rl")); - qCDebug(logTest) << "Pinyin initials search for 'rl' returned" << model.rowCount() << "items"; QCOMPARE(model.rowCount(), 1); QCOMPARE(model.data(model.index(0, 0), AppsModel::DesktopIdRole).toString(), "org.deepin.calendar.desktop"); - qCDebug(logTest) << "Testing Pinyin initials match for 'aqzx'"; model.setFilterRegularExpression(QRegularExpression("aqzx")); - qCDebug(logTest) << "Pinyin initials search for 'aqzx' returned" << model.rowCount() << "items"; QCOMPARE(model.rowCount(), 1); QCOMPARE(model.data(model.index(0, 0), AppsModel::DesktopIdRole).toString(), "org.deepin.defender.desktop"); - qCInfo(logTest) << "Pinyin search tests completed successfully"; } void TestSearchFilterProxyModel::testJianpinSearch() { - qCInfo(logTest) << "Starting Jianpin (abbreviated Pinyin) search tests"; SearchFilterProxyModel &model = SearchFilterProxyModel::instance(); // 测试简拼搜索 - qCDebug(logTest) << "Testing Jianpin search for 'yy' (音乐)"; model.setFilterRegularExpression(QRegularExpression("yy")); - qCDebug(logTest) << "Jianpin search for 'yy' returned" << model.rowCount() << "items"; QCOMPARE(model.rowCount(), 1); QCOMPARE(model.data(model.index(0, 0), AppsModel::DesktopIdRole).toString(), "org.deepin.music.desktop"); // 测试简拼部分匹配 - qCDebug(logTest) << "Testing Jianpin partial match for 'yd' (阅读)"; model.setFilterRegularExpression(QRegularExpression("yd")); - qCDebug(logTest) << "Jianpin search for 'yd' returned" << model.rowCount() << "items"; QCOMPARE(model.rowCount(), 1); QCOMPARE(model.data(model.index(0, 0), AppsModel::DesktopIdRole).toString(), "org.deepin.reader.desktop"); - qCInfo(logTest) << "Jianpin search tests completed successfully"; } void TestSearchFilterProxyModel::testSorting() { - qCInfo(logTest) << "Starting sorting tests"; SearchFilterProxyModel &model = SearchFilterProxyModel::instance(); // 测试排序 - 根据匹配权重和启动次数 - qCDebug(logTest) << "Testing sorting with search pattern 'e'"; model.setFilterRegularExpression(QRegularExpression("e")); int resultCount = model.rowCount(); - qCDebug(logTest) << "Search for 'e' returned" << resultCount << "items"; QVERIFY(resultCount > 2); // 验证排序结果 - qCDebug(logTest) << "Verifying sort order based on launch times and match weight"; // 检查前几个结果是否符合预期的排序规则 bool foundBrowser = false; bool foundEditor = false; int checkCount = qMin(3, resultCount); - qCDebug(logTest) << "Checking first" << checkCount << "results for expected apps"; for (int i = 0; i < checkCount; ++i) { QString desktopId = model.data(model.index(i, 0), AppsModel::DesktopIdRole).toString(); - qCDebug(logTest) << "Result" << i << ":" << desktopId; if (desktopId == "org.deepin.browser.desktop") { - qCDebug(logTest) << "Found browser at position" << i; foundBrowser = true; } else if (desktopId == "org.deepin.editor.desktop") { - qCDebug(logTest) << "Found editor at position" << i; foundEditor = true; } } // 由于浏览器的启动次数更高,它应该排在编辑器前面 - qCDebug(logTest) << "Browser found:" << foundBrowser << "Editor found:" << foundEditor; QVERIFY(foundBrowser); - qCInfo(logTest) << "Sorting tests completed successfully"; } void TestSearchFilterProxyModel::testSpecialCharacters() { - qCInfo(logTest) << "Starting special characters search tests"; SearchFilterProxyModel &model = SearchFilterProxyModel::instance(); // 测试普通字符搜索 - 确保基本功能正常 - qCDebug(logTest) << "Testing basic 'App' search"; model.setFilterRegularExpression(QRegularExpression("App")); int appCount = model.rowCount(); - qCDebug(logTest) << "Basic 'App' search returned" << appCount << "items"; QVERIFY(appCount >= 14); // 测试特殊字符搜索 - qCDebug(logTest) << "Testing search with hyphen 'App-'"; model.setFilterRegularExpression(QRegularExpression("App-")); int hyphenCount = model.rowCount(); - qCDebug(logTest) << "'App-' search returned" << hyphenCount << "items"; QVERIFY(hyphenCount > 0); - qCDebug(logTest) << "Testing search with underscore 'App_'"; model.setFilterRegularExpression(QRegularExpression("App_")); int underscoreCount = model.rowCount(); - qCDebug(logTest) << "'App_' search returned" << underscoreCount << "items"; QVERIFY(underscoreCount > 0); // 验证特殊应用存在 - qCDebug(logTest) << "Verifying special apps are found in general 'App' search"; bool foundSpecialApp = false; model.setFilterRegularExpression(QRegularExpression("App")); int totalAppCount = model.rowCount(); - qCDebug(logTest) << "Searching through" << totalAppCount << "apps for special apps"; QVERIFY(totalAppCount > 0); for (int i = 0; i < totalAppCount; ++i) { QString desktopId = model.data(model.index(i, 0), AppsModel::DesktopIdRole).toString(); - qCDebug(logTest) << "Checking app at index" << i << ":" << desktopId; if (desktopId.startsWith("org.special.app")) { - qCDebug(logTest) << "Found special app:" << desktopId; foundSpecialApp = true; break; } } - qCDebug(logTest) << "Special app found:" << foundSpecialApp; QVERIFY(foundSpecialApp); - qCInfo(logTest) << "Special characters search tests completed successfully"; } void TestSearchFilterProxyModel::testSharedModelAdapter() diff --git a/tests/sortproxymodeltest.cpp b/tests/sortproxymodeltest.cpp index 8dc99443..3fb8aaa0 100644 --- a/tests/sortproxymodeltest.cpp +++ b/tests/sortproxymodeltest.cpp @@ -6,12 +6,10 @@ #include #include #include -#include -#include "../src/models/sortproxymodel.h" +#include "sortproxymodel.h" namespace { -Q_LOGGING_CATEGORY(logTest, "dde.launchpad.test") // A minimal list model that allows changing data WITHOUT auto-emitting dataChanged, // so we can control exactly which roles trigger handleDataChanged. This isolates @@ -69,7 +67,6 @@ private slots: void TestSortProxyModel::ascendingAndDescendingOrder() { - qCInfo(logTest) << "SortProxyModel should sort ascending and descending by the display role"; QStandardItemModel source; source.appendRow(new QStandardItem(QStringLiteral("cherry"))); source.appendRow(new QStandardItem(QStringLiteral("apple"))); @@ -92,7 +89,6 @@ void TestSortProxyModel::ascendingAndDescendingOrder() void TestSortProxyModel::mappingBetweenSourceAndProxy() { - qCInfo(logTest) << "mapFromSource/mapToSource should be consistent with the sort order"; QStandardItemModel source; source.appendRow(new QStandardItem(QStringLiteral("cherry"))); // source row 0 source.appendRow(new QStandardItem(QStringLiteral("apple"))); // source row 1 @@ -112,7 +108,6 @@ void TestSortProxyModel::mappingBetweenSourceAndProxy() void TestSortProxyModel::caseSensitivity() { - qCInfo(logTest) << "SortProxyModel should honor sortCaseSensitivity"; QStandardItemModel source; source.appendRow(new QStandardItem(QStringLiteral("apple"))); source.appendRow(new QStandardItem(QStringLiteral("Banana"))); @@ -133,7 +128,6 @@ void TestSortProxyModel::caseSensitivity() void TestSortProxyModel::sortByCustomRole() { - qCInfo(logTest) << "SortProxyModel should sort by a custom sortRole"; constexpr int WeightRole = Qt::UserRole + 1; QStandardItemModel source; source.setItemRoleNames({{Qt::DisplayRole, QByteArrayLiteral("display")}, @@ -160,7 +154,6 @@ void TestSortProxyModel::sortByCustomRole() void TestSortProxyModel::rowsInsertedKeepsOrder() { - qCInfo(logTest) << "Inserted source rows should appear at their sorted position"; QStandardItemModel source; source.appendRow(new QStandardItem(QStringLiteral("apple"))); source.appendRow(new QStandardItem(QStringLiteral("cherry"))); @@ -181,7 +174,6 @@ void TestSortProxyModel::rowsInsertedKeepsOrder() void TestSortProxyModel::rowsRemovedKeepsOrder() { - qCInfo(logTest) << "Removed source rows should disappear while keeping the rest sorted"; QStandardItemModel source; source.appendRow(new QStandardItem(QStringLiteral("apple"))); source.appendRow(new QStandardItem(QStringLiteral("banana"))); @@ -201,7 +193,6 @@ void TestSortProxyModel::rowsRemovedKeepsOrder() void TestSortProxyModel::dataChangedReorders() { - qCInfo(logTest) << "Changing source data should reorder the proxy"; QStandardItemModel source; source.appendRow(new QStandardItem(QStringLiteral("apple"))); source.appendRow(new QStandardItem(QStringLiteral("banana"))); @@ -221,7 +212,6 @@ void TestSortProxyModel::dataChangedReorders() void TestSortProxyModel::replaceSourceModelResets() { - qCInfo(logTest) << "Replacing the source model should reset to the new rows"; QStandardItemModel sourceA; sourceA.appendRow(new QStandardItem(QStringLiteral("zeta"))); sourceA.appendRow(new QStandardItem(QStringLiteral("alpha"))); @@ -246,7 +236,6 @@ void TestSortProxyModel::replaceSourceModelResets() void TestSortProxyModel::sortColumnMinusOneRestoresNaturalOrder() { - qCInfo(logTest) << "sort(-1) should disable sorting and restore natural source order"; QStandardItemModel source; source.appendRow(new QStandardItem(QStringLiteral("cherry"))); // source row 0 source.appendRow(new QStandardItem(QStringLiteral("apple"))); // source row 1 @@ -269,7 +258,6 @@ void TestSortProxyModel::sortColumnMinusOneRestoresNaturalOrder() void TestSortProxyModel::setSortColumnToSameIsNoop() { - qCInfo(logTest) << "setSortColumn to the same column should be a noop (no signal, no reorder)"; QStandardItemModel source; source.appendRow(new QStandardItem(QStringLiteral("cherry"))); source.appendRow(new QStandardItem(QStringLiteral("apple"))); @@ -293,7 +281,6 @@ void TestSortProxyModel::setSortColumnToSameIsNoop() void TestSortProxyModel::indexWithNoSourceModel() { - qCInfo(logTest) << "index() with no source model should return invalid"; SortProxyModel proxy; QVERIFY(!proxy.index(0, 0).isValid()); QCOMPARE(proxy.rowCount(), 0); @@ -301,7 +288,6 @@ void TestSortProxyModel::indexWithNoSourceModel() void TestSortProxyModel::indexWithOutOfRangeColumn() { - qCInfo(logTest) << "index() with out-of-range column should return invalid"; QStandardItemModel source; source.appendRow(new QStandardItem(QStringLiteral("a"))); @@ -313,7 +299,6 @@ void TestSortProxyModel::indexWithOutOfRangeColumn() void TestSortProxyModel::dataWithInvalidProxyIndex() { - qCInfo(logTest) << "data() with invalid proxy index should return empty QVariant"; QStandardItemModel source; source.appendRow(new QStandardItem(QStringLiteral("a"))); @@ -324,7 +309,6 @@ void TestSortProxyModel::dataWithInvalidProxyIndex() void TestSortProxyModel::mapToSourceWithInvalidIndex() { - qCInfo(logTest) << "mapToSource() with invalid index should return invalid"; QStandardItemModel source; source.appendRow(new QStandardItem(QStringLiteral("a"))); @@ -335,7 +319,6 @@ void TestSortProxyModel::mapToSourceWithInvalidIndex() void TestSortProxyModel::mapFromSourceWithInvalidIndex() { - qCInfo(logTest) << "mapFromSource() with invalid index should return invalid"; QStandardItemModel source; source.appendRow(new QStandardItem(QStringLiteral("a"))); @@ -346,7 +329,6 @@ void TestSortProxyModel::mapFromSourceWithInvalidIndex() void TestSortProxyModel::mapFromSourceWithParentReturnsEmpty() { - qCInfo(logTest) << "mapFromSource() with a parent index should return empty (flat model)"; QStandardItemModel source; source.appendRow(new QStandardItem(QStringLiteral("a"))); @@ -361,7 +343,6 @@ void TestSortProxyModel::mapFromSourceWithParentReturnsEmpty() void TestSortProxyModel::handleModelReset() { - qCInfo(logTest) << "source model reset should reset the proxy"; QStandardItemModel source; source.appendRow(new QStandardItem(QStringLiteral("cherry"))); source.appendRow(new QStandardItem(QStringLiteral("apple"))); @@ -384,7 +365,6 @@ void TestSortProxyModel::handleModelReset() void TestSortProxyModel::removeMultipleConsecutiveRows() { - qCInfo(logTest) << "removing multiple consecutive source rows should keep proxy sorted"; QStandardItemModel source; source.appendRow(new QStandardItem(QStringLiteral("a"))); source.appendRow(new QStandardItem(QStringLiteral("b"))); @@ -407,7 +387,6 @@ void TestSortProxyModel::removeMultipleConsecutiveRows() void TestSortProxyModel::dataChangedWithEmptyRolesReorders() { - qCInfo(logTest) << "dataChanged with empty roles should trigger reorder (isolated from sort-role path)"; // Use ControllableStringListModel to change data WITHOUT auto-emitting dataChanged, // then emit dataChanged with empty roles to trigger the empty-roles reorder path. ControllableStringListModel source({QStringLiteral("apple"), @@ -436,7 +415,6 @@ void TestSortProxyModel::dataChangedWithEmptyRolesReorders() void TestSortProxyModel::dataChangedWithNonSortRoleDoesNotReorder() { - qCInfo(logTest) << "dataChanged with non-sort role should not reorder"; constexpr int UserRole1 = Qt::UserRole + 1; QStandardItemModel source; source.appendRow(new QStandardItem(QStringLiteral("apple"))); @@ -458,7 +436,6 @@ void TestSortProxyModel::dataChangedWithNonSortRoleDoesNotReorder() void TestSortProxyModel::columnCountWithAndWithoutSource() { - qCInfo(logTest) << "columnCount should return source column count or 0"; SortProxyModel proxy; QCOMPARE(proxy.columnCount(), 0); @@ -470,7 +447,6 @@ void TestSortProxyModel::columnCountWithAndWithoutSource() void TestSortProxyModel::sortColumnAndSortOrderGetters() { - qCInfo(logTest) << "sortColumn and sortOrder getters should return current values"; QStandardItemModel source; source.appendRow(new QStandardItem(QStringLiteral("a")));