From 0c23509e1a4a336d4e6164cf73d920830e8da4fa Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Sat, 4 Apr 2026 19:18:18 +0800 Subject: [PATCH] Add missing XCUI core UI tests --- tests/Core/UI/CMakeLists.txt | 30 +++++ tests/Core/UI/test_ui_core.cpp | 197 +++++++++++++++++++++++++++++++++ 2 files changed, 227 insertions(+) create mode 100644 tests/Core/UI/CMakeLists.txt create mode 100644 tests/Core/UI/test_ui_core.cpp diff --git a/tests/Core/UI/CMakeLists.txt b/tests/Core/UI/CMakeLists.txt new file mode 100644 index 00000000..1cd586c9 --- /dev/null +++ b/tests/Core/UI/CMakeLists.txt @@ -0,0 +1,30 @@ +# ============================================================ +# UI Core Tests +# ============================================================ + +set(UI_TEST_SOURCES + test_ui_core.cpp +) + +add_executable(core_ui_tests ${UI_TEST_SOURCES}) + +if(MSVC) + set_target_properties(core_ui_tests PROPERTIES + LINK_FLAGS "/NODEFAULTLIB:libcpmt.lib /NODEFAULTLIB:libcmt.lib" + ) +endif() + +target_link_libraries(core_ui_tests + PRIVATE + XCEngine + GTest::gtest + GTest::gtest_main +) + +target_include_directories(core_ui_tests PRIVATE + ${CMAKE_SOURCE_DIR}/engine/include + ${CMAKE_SOURCE_DIR}/tests/Fixtures +) + +include(GoogleTest) +gtest_discover_tests(core_ui_tests) diff --git a/tests/Core/UI/test_ui_core.cpp b/tests/Core/UI/test_ui_core.cpp new file mode 100644 index 00000000..8cbd5e29 --- /dev/null +++ b/tests/Core/UI/test_ui_core.cpp @@ -0,0 +1,197 @@ +#include + +#include + +namespace { + +using XCEngine::UI::HasAnyDirtyFlags; +using XCEngine::UI::IUIViewModel; +using XCEngine::UI::RevisionedViewModelBase; +using XCEngine::UI::UIBuildContext; +using XCEngine::UI::UIBuildElementDesc; +using XCEngine::UI::UIContext; +using XCEngine::UI::UIDirtyFlags; +using XCEngine::UI::UIElementChangeKind; +using XCEngine::UI::UIElementId; +using XCEngine::UI::UIElementNode; +using XCEngine::UI::UIElementTree; + +class TestViewModel : public RevisionedViewModelBase { +public: + void Touch() { + MarkViewModelChanged(); + } +}; + +UIBuildElementDesc MakeElement( + UIElementId id, + const char* typeName, + std::uint64_t localStateRevision = 0, + const IUIViewModel* viewModel = nullptr, + std::uint64_t structuralRevision = 0) { + UIBuildElementDesc desc = {}; + desc.id = id; + desc.typeName = typeName; + desc.localStateRevision = localStateRevision; + desc.viewModel = viewModel; + desc.structuralRevision = structuralRevision; + return desc; +} + +void BuildBasicTree(UIBuildContext& buildContext) { + auto root = buildContext.PushElement(MakeElement(1, "Root")); + EXPECT_TRUE(static_cast(root)); + EXPECT_TRUE(buildContext.AddLeaf(MakeElement(2, "Label"))); + auto panel = buildContext.PushElement(MakeElement(3, "Panel")); + EXPECT_TRUE(static_cast(panel)); + EXPECT_TRUE(buildContext.AddLeaf(MakeElement(4, "Button"))); +} + +const UIElementNode& RequireNode(const UIElementTree& tree, UIElementId id) { + const UIElementNode* node = tree.FindNode(id); + EXPECT_NE(node, nullptr); + return *node; +} + +TEST(UICoreTest, RebuildCreatesStableParentChildTree) { + UIContext context = {}; + const auto result = context.Rebuild(BuildBasicTree); + + EXPECT_TRUE(result.succeeded); + EXPECT_TRUE(result.treeChanged); + EXPECT_EQ(result.generation, 1u); + EXPECT_EQ(context.GetElementTree().GetRootId(), 1u); + EXPECT_EQ(context.GetElementTree().GetNodeCount(), 4u); + EXPECT_TRUE(result.HasChange(1)); + EXPECT_TRUE(result.HasChange(4)); + + const UIElementNode& root = RequireNode(context.GetElementTree(), 1); + ASSERT_EQ(root.childIds.size(), 2u); + EXPECT_EQ(root.childIds[0], 2u); + EXPECT_EQ(root.childIds[1], 3u); + EXPECT_EQ(root.depth, 0u); + + const UIElementNode& panel = RequireNode(context.GetElementTree(), 3); + ASSERT_EQ(panel.childIds.size(), 1u); + EXPECT_EQ(panel.childIds[0], 4u); + EXPECT_EQ(panel.depth, 1u); + + ASSERT_EQ(result.dirtyRootIds.size(), 1u); + EXPECT_EQ(result.dirtyRootIds[0], 1u); +} + +TEST(UICoreTest, RebuildSkipsUnchangedTreeAfterDirtyFlagsAreCleared) { + UIContext context = {}; + const auto initial = context.Rebuild(BuildBasicTree); + ASSERT_TRUE(initial.succeeded); + context.GetElementTree().ClearAllDirtyFlags(); + + const auto result = context.Rebuild(BuildBasicTree); + + EXPECT_TRUE(result.succeeded); + EXPECT_FALSE(result.treeChanged); + EXPECT_TRUE(result.changes.empty()); + EXPECT_TRUE(result.dirtyRootIds.empty()); +} + +TEST(UICoreTest, LocalStateChangeOnlyInvalidatesTheChangedLeaf) { + UIContext context = {}; + ASSERT_TRUE(context.Rebuild(BuildBasicTree).succeeded); + context.GetElementTree().ClearAllDirtyFlags(); + + const auto result = context.Rebuild([](UIBuildContext& buildContext) { + auto root = buildContext.PushElement(MakeElement(1, "Root")); + EXPECT_TRUE(static_cast(root)); + EXPECT_TRUE(buildContext.AddLeaf(MakeElement(2, "Label", 1))); + auto panel = buildContext.PushElement(MakeElement(3, "Panel")); + EXPECT_TRUE(static_cast(panel)); + EXPECT_TRUE(buildContext.AddLeaf(MakeElement(4, "Button"))); + }); + + EXPECT_TRUE(result.succeeded); + EXPECT_TRUE(result.treeChanged); + ASSERT_EQ(result.changes.size(), 1u); + ASSERT_NE(result.FindChange(2), nullptr); + EXPECT_EQ(result.FindChange(2)->kind, UIElementChangeKind::Updated); + EXPECT_TRUE(HasAnyDirtyFlags(result.FindChange(2)->dirtyFlags, UIDirtyFlags::LocalState)); + EXPECT_TRUE(HasAnyDirtyFlags(result.FindChange(2)->dirtyFlags, UIDirtyFlags::Paint)); + + ASSERT_EQ(result.dirtyRootIds.size(), 1u); + EXPECT_EQ(result.dirtyRootIds[0], 2u); + + const UIElementNode& leaf = RequireNode(context.GetElementTree(), 2); + EXPECT_TRUE(HasAnyDirtyFlags(leaf.dirtyFlags, UIDirtyFlags::LocalState)); + EXPECT_FALSE(RequireNode(context.GetElementTree(), 1).IsDirty()); +} + +TEST(UICoreTest, ViewModelRevisionChangeInvalidatesBoundElement) { + TestViewModel viewModel = {}; + UIContext context = {}; + ASSERT_TRUE(context.Rebuild([&](UIBuildContext& buildContext) { + auto root = buildContext.PushElement(MakeElement(1, "Root")); + EXPECT_TRUE(static_cast(root)); + EXPECT_TRUE(buildContext.AddLeaf(MakeElement(2, "Inspector", 0, &viewModel))); + }).succeeded); + context.GetElementTree().ClearAllDirtyFlags(); + + viewModel.Touch(); + const auto result = context.Rebuild([&](UIBuildContext& buildContext) { + auto root = buildContext.PushElement(MakeElement(1, "Root")); + EXPECT_TRUE(static_cast(root)); + EXPECT_TRUE(buildContext.AddLeaf(MakeElement(2, "Inspector", 0, &viewModel))); + }); + + EXPECT_TRUE(result.succeeded); + ASSERT_NE(result.FindChange(2), nullptr); + EXPECT_TRUE(HasAnyDirtyFlags(result.FindChange(2)->dirtyFlags, UIDirtyFlags::ViewModel)); + EXPECT_TRUE(HasAnyDirtyFlags(result.FindChange(2)->dirtyFlags, UIDirtyFlags::Paint)); + ASSERT_EQ(result.dirtyRootIds.size(), 1u); + EXPECT_EQ(result.dirtyRootIds[0], 2u); +} + +TEST(UICoreTest, StructuralChangesBubbleLayoutInvalidationToAncestors) { + UIContext context = {}; + ASSERT_TRUE(context.Rebuild([](UIBuildContext& buildContext) { + auto root = buildContext.PushElement(MakeElement(1, "Root")); + EXPECT_TRUE(static_cast(root)); + auto panel = buildContext.PushElement(MakeElement(2, "Panel")); + EXPECT_TRUE(static_cast(panel)); + EXPECT_TRUE(buildContext.AddLeaf(MakeElement(3, "Text"))); + }).succeeded); + context.GetElementTree().ClearAllDirtyFlags(); + + const auto result = context.Rebuild([](UIBuildContext& buildContext) { + auto root = buildContext.PushElement(MakeElement(1, "Root")); + EXPECT_TRUE(static_cast(root)); + auto panel = buildContext.PushElement(MakeElement(2, "Panel")); + EXPECT_TRUE(static_cast(panel)); + EXPECT_TRUE(buildContext.AddLeaf(MakeElement(3, "Text"))); + EXPECT_TRUE(buildContext.AddLeaf(MakeElement(4, "Icon"))); + }); + + EXPECT_TRUE(result.succeeded); + EXPECT_TRUE(result.HasChange(4)); + ASSERT_NE(result.FindChange(2), nullptr); + EXPECT_TRUE(HasAnyDirtyFlags(result.FindChange(2)->dirtyFlags, UIDirtyFlags::Structure)); + + const UIElementNode& root = RequireNode(context.GetElementTree(), 1); + const UIElementNode& panel = RequireNode(context.GetElementTree(), 2); + EXPECT_TRUE(HasAnyDirtyFlags(root.dirtyFlags, UIDirtyFlags::Layout)); + EXPECT_TRUE(HasAnyDirtyFlags(panel.dirtyFlags, UIDirtyFlags::Structure)); + + ASSERT_EQ(result.dirtyRootIds.size(), 1u); + EXPECT_EQ(result.dirtyRootIds[0], 1u); +} + +TEST(UICoreTest, RebuildFailsWhenElementScopesRemainOpen) { + UIContext context = {}; + const auto result = context.Rebuild([](UIBuildContext& buildContext) { + EXPECT_TRUE(buildContext.BeginElement(MakeElement(1, "Root"))); + }); + + EXPECT_FALSE(result.succeeded); + EXPECT_FALSE(result.errorMessage.empty()); + EXPECT_EQ(context.GetElementTree().GetNodeCount(), 0u); +} + +} // namespace