Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
# SPDX-License-Identifier: CC0-1.0

add_subdirectory(panels)
add_subdirectory(applets)
131 changes: 131 additions & 0 deletions tests/applets/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd.
#
# SPDX-License-Identifier: GPL-3.0-or-later

# =============================================================================
# dde-shell applets/ unit tests (GTest).
#
# OBJECT library organization decision (per leader's request):
# Single OBJECT lib `applets_test_objects` for the pure-logic dde-apps
# classes that have NO DConfig / DBus / Wayland / frame-singleton deps:
# * categoryutils.cpp (pure namespace functions)
# * itemspage.cpp (pure QObject logic, no external deps)
# These two are the only applets/ sources compiled into the OBJECT lib.
#
# NOT compiled into the OBJECT lib (deferred with reasons):
# * appslaunchtimes / appsdockedhelper — DConfig singleton (constructor
# calls DConfig::create + reads/writes), needs DConfig runtime; testable
# only with DConfig mock or DBus. Deferred.
# * appitem — constructor calls AppsLaunchTimesHelper::instance() (global
# singleton + DConfig); setLaunchedTimes/setDocked call singleton helpers.
# Deferred (would need mock or refactor).
# * appgroup — constructor instantiates ItemsPage (OK) but setData uses
# AppGroupManager roles (needs the model); parseGroupId has a src bug.
# Partially testable (static helpers) — covered via scoped include.
# * appgroupmanager — DConfig + AMAppItemModel reference (DBus). Deferred.
# * amappitemmodel / amappitem — DBus ObjectManager/Application proxies.
# Deferred.
# * appsapplet — instantiates AMAppItemModel (DBus) + AppGroupManager.
# Deferred.
# * amapplet / appearanceapplet — DBus session bus in load(). Deferred.
# * keynotifyapplet / shutdownapplet — Wayland platform check + DBus.
# Deferred.
# * treelandkeynotify / treelandlockscreen — Wayland private API. Deferred.
#
# applets sources that reference frame/ classes (DApplet) are NOT compiled
# here — they'd need frame_test_objects linkage and pull DConfig/DBus. The
# two pure-logic sources (categoryutils, itemspage) have ZERO frame/ deps.
#
# Coverage target: `applets_coverage` (independent from `frame_coverage`),
# ctest prefix `applets_` so `ctest -R '^applets_'` runs only these tests.
# =============================================================================

find_package(GTest REQUIRED)
find_package(Qt${QT_VERSION_MAJOR} ${REQUIRED_QT_VERSION} REQUIRED COMPONENTS Core Gui Test)

include(GoogleTest)

option(APPLETS_BUILD_COVERAGE "Enable gcov coverage instrumentation for dde-shell applets unit tests" OFF)

# ---- OBJECT library: pure-logic applets/ sources under test -----------------
add_library(applets_test_objects OBJECT
${CMAKE_SOURCE_DIR}/applets/dde-apps/categoryutils.h
${CMAKE_SOURCE_DIR}/applets/dde-apps/categoryutils.cpp
${CMAKE_SOURCE_DIR}/applets/dde-apps/itemspage.h
${CMAKE_SOURCE_DIR}/applets/dde-apps/itemspage.cpp
)
target_include_directories(applets_test_objects PUBLIC
${CMAKE_SOURCE_DIR}/applets/dde-apps
)
target_link_libraries(applets_test_objects PUBLIC
Qt${QT_VERSION_MAJOR}::Core
Qt${QT_VERSION_MAJOR}::Gui
)

# ---- Helper: declare an applets test executable -----------------------------
set(APPLETS_TEST_TARGETS "")
function(applets_add_test NAME)
add_executable(${NAME} ${ARGN})
target_link_libraries(${NAME} PRIVATE
GTest::GTest
GTest::Main
Qt${QT_VERSION_MAJOR}::Core
Qt${QT_VERSION_MAJOR}::Gui
Qt${QT_VERSION_MAJOR}::Test
applets_test_objects
)
gtest_discover_tests(${NAME} TEST_PREFIX "applets_")
list(APPEND APPLETS_TEST_TARGETS ${NAME})
set(APPLETS_TEST_TARGETS "${APPLETS_TEST_TARGETS}" PARENT_SCOPE)
endfunction()

# ---- Test executables -------------------------------------------------------
# categoryutils: pure namespace functions (parseBestMatchedCategory,
# parseDDECategoryString, parseXdgCategoryString) with rich branching.
applets_add_test(categoryutils_tests categoryutilstests.cpp)

# itemspage: pure-logic pagination manager (append/insert/move/remove/find).
applets_add_test(itemspage_tests itemspagetests.cpp)

# NOTE: appgroup static helpers (idIsFolder/groupIdFromNumber/parseGroupId) are
# deferred: appgroup.h → appitem.h → am.h → QDBusObjectPath (Qt6::DBus) +
# appgroupmanager.h → DConfig (Dtk6::Core) + QQmlEngine (Qt6::Qml) + yaml-cpp.
# The dependency chain is too heavy for 3 trivial one-liner static methods.
# Defer to a future batch with DConfig mock or after src refactoring.
# Also: parseGroupId has an off-by-one bug (mid(len+1) should be mid(len)) —
# recorded as a src defect, not fixed here.

# ---- Coverage instrumentation + report generation ---------------------------
if(APPLETS_BUILD_COVERAGE)
message(STATUS "applets tests: coverage instrumentation ENABLED (APPLETS_BUILD_COVERAGE=ON)")
target_compile_options(applets_test_objects PRIVATE -fprofile-arcs -ftest-coverage -O0 -g)
foreach(_t IN LISTS APPLETS_TEST_TARGETS)
target_compile_options(${_t} PRIVATE -fprofile-arcs -ftest-coverage -O0 -g)
target_link_options(${_t} PRIVATE -fprofile-arcs)
endforeach()

find_program(LCOV_BIN lcov)
find_program(GENHTML_BIN genhtml)
if(LCOV_BIN AND GENHTML_BIN)
set(APPLETS_COVERAGE_DIR "${CMAKE_BINARY_DIR}/applets_coverage")
add_custom_target(applets_coverage
DEPENDS ${APPLETS_TEST_TARGETS}
COMMAND ${CMAKE_COMMAND} -E make_directory "${APPLETS_COVERAGE_DIR}"
COMMAND ${CMAKE_CTEST_COMMAND} --test-dir "${CMAKE_BINARY_DIR}" -R "^applets_" --output-on-failure
COMMAND ${LCOV_BIN} --capture --directory "${CMAKE_BINARY_DIR}"
--output-file "${APPLETS_COVERAGE_DIR}/applets.info"
COMMAND ${LCOV_BIN} --remove "${APPLETS_COVERAGE_DIR}/applets.info"
'*/usr/include/*' '*/usr/lib/*' '*/3rdparty/*' '*/tests/*'
'*/moc_*' '*/qrc_*' '*/_deps/*' '*_json.h'
-o "${APPLETS_COVERAGE_DIR}/applets_filtered.info"
COMMAND ${GENHTML_BIN} "${APPLETS_COVERAGE_DIR}/applets_filtered.info"
-o "${APPLETS_COVERAGE_DIR}/html" --branch-coverage
COMMAND ${CMAKE_COMMAND} -E echo "=== Applets coverage report: ${APPLETS_COVERAGE_DIR}/html/index.html ==="
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}"
COMMENT "Run applets unit tests and generate lcov branch-coverage report"
VERBATIM
)
else()
message(WARNING "lcov or genhtml not found; 'applets_coverage' target not created.")
endif()
endif()
159 changes: 159 additions & 0 deletions tests/applets/categoryutilstests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: GPL-3.0-or-later

// Unit tests for CategoryUtils (applets/dde-apps/categoryutils), a pure
// namespace with three functions that parse desktop-file category strings into
// DDE Categorytype enums. Rich branching: DDE-name match, XDG-name match,
// best-match voting, music+video tie-break, empty input.

#include <gtest/gtest.h>

Check warning on line 10 in tests/applets/categoryutilstests.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <gtest/gtest.h> not found. Please note: Cppcheck does not need standard library headers to get proper results.

#include <QList>

Check warning on line 12 in tests/applets/categoryutilstests.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QList> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QString>

Check warning on line 13 in tests/applets/categoryutilstests.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QString> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QStringList>

Check warning on line 14 in tests/applets/categoryutilstests.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QStringList> not found. Please note: Cppcheck does not need standard library headers to get proper results.

#include "categoryutils.h"

Check warning on line 16 in tests/applets/categoryutilstests.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: "categoryutils.h" not found.

using namespace CategoryUtils;

// --- parseDDECategoryString ---

TEST(ParseDDECategoryString, KnownDDECategories)
{
EXPECT_EQ(parseDDECategoryString(QStringLiteral("internet")), Categorytype::CategoryInternet);
EXPECT_EQ(parseDDECategoryString(QStringLiteral("chat")), Categorytype::CategoryChat);
EXPECT_EQ(parseDDECategoryString(QStringLiteral("music")), Categorytype::CategoryMusic);
EXPECT_EQ(parseDDECategoryString(QStringLiteral("video")), Categorytype::CategoryVideo);
EXPECT_EQ(parseDDECategoryString(QStringLiteral("graphics")), Categorytype::CategoryGraphics);
EXPECT_EQ(parseDDECategoryString(QStringLiteral("game")), Categorytype::CategoryGame);
EXPECT_EQ(parseDDECategoryString(QStringLiteral("office")), Categorytype::CategoryOffice);
EXPECT_EQ(parseDDECategoryString(QStringLiteral("reading")), Categorytype::CategoryReading);
EXPECT_EQ(parseDDECategoryString(QStringLiteral("development")), Categorytype::CategoryDevelopment);
EXPECT_EQ(parseDDECategoryString(QStringLiteral("system")), Categorytype::CategorySystem);
EXPECT_EQ(parseDDECategoryString(QStringLiteral("others")), Categorytype::CategoryOthers);
}

TEST(ParseDDECategoryString, UnknownReturnsErr)
{
EXPECT_EQ(parseDDECategoryString(QStringLiteral("nonexistent")), Categorytype::CategoryErr);
EXPECT_EQ(parseDDECategoryString(QStringLiteral("")), Categorytype::CategoryErr);
EXPECT_EQ(parseDDECategoryString(QStringLiteral("INTERNET")), Categorytype::CategoryErr); // case-sensitive
}

// --- parseXdgCategoryString ---

TEST(ParseXdgCategoryString, KnownXdgCategories)
{
// Single-match XDG names
EXPECT_EQ(parseXdgCategoryString(QStringLiteral("webbrowser")).size(), 1);
EXPECT_EQ(parseXdgCategoryString(QStringLiteral("webbrowser")).first(), Categorytype::CategoryInternet);
EXPECT_EQ(parseXdgCategoryString(QStringLiteral("ide")).first(), Categorytype::CategoryDevelopment);
EXPECT_EQ(parseXdgCategoryString(QStringLiteral("boardgame")).first(), Categorytype::CategoryGame);
}

TEST(ParseXdgCategoryString, MultiMatchXdgCategories)
{
// "audiovideo" maps to both Music and Video
auto result = parseXdgCategoryString(QStringLiteral("audiovideo"));
EXPECT_EQ(result.size(), 2);
EXPECT_TRUE(result.contains(Categorytype::CategoryMusic));
EXPECT_TRUE(result.contains(Categorytype::CategoryVideo));

// "player" maps to both Music and Video
result = parseXdgCategoryString(QStringLiteral("player"));
EXPECT_EQ(result.size(), 2);
EXPECT_TRUE(result.contains(Categorytype::CategoryMusic));
EXPECT_TRUE(result.contains(Categorytype::CategoryVideo));
}

TEST(ParseXdgCategoryString, UnknownReturnsEmpty)
{
EXPECT_TRUE(parseXdgCategoryString(QStringLiteral("nonexistent")).isEmpty());
EXPECT_TRUE(parseXdgCategoryString(QStringLiteral("")).isEmpty());
}

TEST(ParseXdgCategoryString, XPrefixCategories)
{
// x-prefixed categories
EXPECT_EQ(parseXdgCategoryString(QStringLiteral("x-midi")).first(), Categorytype::CategoryMusic);
EXPECT_EQ(parseXdgCategoryString(QStringLiteral("x-bluetooth")).first(), Categorytype::CategorySystem);
EXPECT_EQ(parseXdgCategoryString(QStringLiteral("x-quran")).first(), Categorytype::CategoryReading);
}

// --- parseBestMatchedCategory ---

TEST(ParseBestMatchedCategory, EmptyInputReturnsOthers)
{
EXPECT_EQ(parseBestMatchedCategory({}), Categorytype::CategoryOthers);
}

TEST(ParseBestMatchedCategory, SingleDDECategory)
{
EXPECT_EQ(parseBestMatchedCategory({QStringLiteral("music")}), Categorytype::CategoryMusic);
EXPECT_EQ(parseBestMatchedCategory({QStringLiteral("development")}), Categorytype::CategoryDevelopment);
EXPECT_EQ(parseBestMatchedCategory({QStringLiteral("system")}), Categorytype::CategorySystem);
}

TEST(ParseBestMatchedCategory, SingleXdgCategory)
{
EXPECT_EQ(parseBestMatchedCategory({QStringLiteral("webbrowser")}), Categorytype::CategoryInternet);
EXPECT_EQ(parseBestMatchedCategory({QStringLiteral("ide")}), Categorytype::CategoryDevelopment);
}

TEST(ParseBestMatchedCategory, MultipleSameCategory)
{
// Multiple categories that all map to the same type
EXPECT_EQ(parseBestMatchedCategory({QStringLiteral("music"), QStringLiteral("player")}),
Categorytype::CategoryMusic);
}

TEST(ParseBestMatchedCategory, VotingPicksMostCommon)
{
// 2x game + 1x system → game wins
EXPECT_EQ(parseBestMatchedCategory({QStringLiteral("game"), QStringLiteral("arcadegame"), QStringLiteral("system")}),
Categorytype::CategoryGame);
}

TEST(ParseBestMatchedCategory, OnlyOthersReturnsOthers)
{
// Categories that only map to Others are removed; empty map → Others
EXPECT_EQ(parseBestMatchedCategory({QStringLiteral("accessories")}), Categorytype::CategoryOthers);
EXPECT_EQ(parseBestMatchedCategory({QStringLiteral("accessories"), QStringLiteral("core")}),
Categorytype::CategoryOthers);
}

TEST(ParseBestMatchedCategory, MusicVideoTieBreakReturnsVideo)
{
// Tie between Music and Video → special tie-break returns Video
EXPECT_EQ(parseBestMatchedCategory({QStringLiteral("music"), QStringLiteral("video")}),
Categorytype::CategoryVideo);
}

TEST(ParseBestMatchedCategory, UnknownCategoryIgnored)
{
// Unknown category strings are ignored; only known ones vote
EXPECT_EQ(parseBestMatchedCategory({QStringLiteral("nonexistent"), QStringLiteral("music")}),
Categorytype::CategoryMusic);
// All unknown → Others
EXPECT_EQ(parseBestMatchedCategory({QStringLiteral("nonexistent1"), QStringLiteral("nonexistent2")}),
Categorytype::CategoryOthers);
}

TEST(ParseBestMatchedCategory, CaseInsensitiveInput)
{
// parseBestMatchedCategory calls toLower() on each category
EXPECT_EQ(parseBestMatchedCategory({QStringLiteral("MUSIC")}), Categorytype::CategoryMusic);
EXPECT_EQ(parseBestMatchedCategory({QStringLiteral("Development")}), Categorytype::CategoryDevelopment);
}

TEST(ParseBestMatchedCategory, TieWithoutMusicVideoReturnsFirstSorted)
{
// Tie between two non-music/video categories → sorted, returns first
// game + development tie (1 each) → sorted: Development < Game → Development
auto result = parseBestMatchedCategory({QStringLiteral("game"), QStringLiteral("ide")});
// Both have 1 vote, sorted: CategoryDevelopment(8) < CategoryGame(5)?
// Actually enum values: Game=5, Development=8 → sorted ascending: Game < Development
// So first sorted = Game
EXPECT_EQ(result, Categorytype::CategoryGame);
}
Loading
Loading