Add missing XCUI core UI tests

This commit is contained in:
2026-04-04 19:18:18 +08:00
parent 341ce79231
commit 0c23509e1a
2 changed files with 227 additions and 0 deletions

View File

@@ -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)

View File

@@ -0,0 +1,197 @@
#include <gtest/gtest.h>
#include <XCEngine/UI/Core/UIContext.h>
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<bool>(root));
EXPECT_TRUE(buildContext.AddLeaf(MakeElement(2, "Label")));
auto panel = buildContext.PushElement(MakeElement(3, "Panel"));
EXPECT_TRUE(static_cast<bool>(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<bool>(root));
EXPECT_TRUE(buildContext.AddLeaf(MakeElement(2, "Label", 1)));
auto panel = buildContext.PushElement(MakeElement(3, "Panel"));
EXPECT_TRUE(static_cast<bool>(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<bool>(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<bool>(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<bool>(root));
auto panel = buildContext.PushElement(MakeElement(2, "Panel"));
EXPECT_TRUE(static_cast<bool>(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<bool>(root));
auto panel = buildContext.PushElement(MakeElement(2, "Panel"));
EXPECT_TRUE(static_cast<bool>(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