Skip to content
Merged
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
11 changes: 11 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,14 @@
# SPDX-License-Identifier: CC0-1.0

add_subdirectory(panels)
add_subdirectory(frame)
add_subdirectory(applets)

# ---- coverage: run all dde-shell module tests and generate coverage reports ----
# Trigger the per-subdir frame_coverage/applets_coverage/dock_coverage targets,
# each scoped to its module (ctest -R). Scoping keeps the pre-existing
# taskmanager/notification tests (which fail on master) out of the coverage run.
add_custom_target(coverage
DEPENDS frame_coverage applets_coverage dock_coverage
COMMENT "Run all dde-shell module tests and generate coverage reports"
)
135 changes: 135 additions & 0 deletions tests/applets/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# 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" ON)

# ---- 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()
endif()

# ---- coverage: execute applets tests and generate coverage report ------------
find_program(LCOV_BIN lcov)
find_program(GENHTML_BIN genhtml)
set(_run_test_coverage "")
if(LCOV_BIN AND GENHTML_BIN)
set(APPLETS_COVERAGE_DIR "${CMAKE_BINARY_DIR}/applets_coverage")
set(_run_test_coverage
COMMAND ${CMAKE_COMMAND} -E make_directory "${APPLETS_COVERAGE_DIR}"
COMMAND ${LCOV_BIN} --capture --directory "${CMAKE_BINARY_DIR}"
--rc lcov_branch_coverage=1
--output-file "${APPLETS_COVERAGE_DIR}/applets.info"
COMMAND ${LCOV_BIN} --extract "${APPLETS_COVERAGE_DIR}/applets.info"
--rc lcov_branch_coverage=1
"${CMAKE_SOURCE_DIR}/applets/dde-apps/*"
-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 ==="
)
endif()
add_custom_target(applets_coverage
DEPENDS ${APPLETS_TEST_TARGETS}
COMMAND ${CMAKE_CTEST_COMMAND} --test-dir "${CMAKE_BINARY_DIR}" -R "^applets_" --output-on-failure
${_run_test_coverage}
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}"
COMMENT "Run applets unit tests and generate lcov branch-coverage report"
VERBATIM
)
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