Add XCUI expansion state and coverage tests

This commit is contained in:
2026-04-05 07:29:27 +08:00
parent 646e5855ce
commit 511e94fd30
18 changed files with 1213 additions and 53 deletions

View File

@@ -5,6 +5,7 @@
set(UI_TEST_SOURCES
test_ui_core.cpp
test_ui_editor_collection_primitives.cpp
test_ui_expansion_model.cpp
test_layout_engine.cpp
test_ui_selection_model.cpp
test_ui_runtime.cpp

View File

@@ -50,6 +50,7 @@ TEST(UIEditorCollectionPrimitivesTest, ClassifyAndFlagsMatchEditorCollectionTags
EXPECT_TRUE(UIWidgets::IsUIEditorCollectionPrimitiveHoverable(Kind::TreeItem));
EXPECT_TRUE(UIWidgets::IsUIEditorCollectionPrimitiveHoverable(Kind::ListItem));
EXPECT_TRUE(UIWidgets::IsUIEditorCollectionPrimitiveHoverable(Kind::PropertySection));
EXPECT_TRUE(UIWidgets::IsUIEditorCollectionPrimitiveHoverable(Kind::FieldRow));
EXPECT_FALSE(UIWidgets::IsUIEditorCollectionPrimitiveHoverable(Kind::TreeView));
}

View File

@@ -0,0 +1,45 @@
#include <gtest/gtest.h>
#include <XCEngine/UI/Widgets/UIExpansionModel.h>
namespace {
using XCEngine::UI::Widgets::UIExpansionModel;
TEST(UIExpansionModelTest, ExpandCollapseAndClearTrackExpandedItems) {
UIExpansionModel expansion = {};
EXPECT_FALSE(expansion.HasExpandedItems());
EXPECT_EQ(expansion.GetExpandedCount(), 0u);
EXPECT_TRUE(expansion.Expand("treeAssetsRoot"));
EXPECT_TRUE(expansion.IsExpanded("treeAssetsRoot"));
EXPECT_TRUE(expansion.HasExpandedItems());
EXPECT_EQ(expansion.GetExpandedCount(), 1u);
EXPECT_FALSE(expansion.Expand("treeAssetsRoot"));
EXPECT_TRUE(expansion.Collapse("treeAssetsRoot"));
EXPECT_FALSE(expansion.IsExpanded("treeAssetsRoot"));
EXPECT_EQ(expansion.GetExpandedCount(), 0u);
EXPECT_FALSE(expansion.Collapse("treeAssetsRoot"));
EXPECT_FALSE(expansion.Clear());
}
TEST(UIExpansionModelTest, SetAndToggleExpandedReplaceStateForMatchingItem) {
UIExpansionModel expansion = {};
EXPECT_TRUE(expansion.SetExpanded("inspectorTransform", true));
EXPECT_TRUE(expansion.IsExpanded("inspectorTransform"));
EXPECT_EQ(expansion.GetExpandedCount(), 1u);
EXPECT_FALSE(expansion.SetExpanded("inspectorTransform", true));
EXPECT_TRUE(expansion.ToggleExpanded("inspectorTransform"));
EXPECT_FALSE(expansion.IsExpanded("inspectorTransform"));
EXPECT_TRUE(expansion.ToggleExpanded("inspectorMesh"));
EXPECT_TRUE(expansion.IsExpanded("inspectorMesh"));
EXPECT_TRUE(expansion.SetExpanded("inspectorMesh", false));
EXPECT_FALSE(expansion.IsExpanded("inspectorMesh"));
}
} // namespace

View File

@@ -41,6 +41,50 @@ TEST(UITextInputControllerTest, BackspaceAndArrowKeysUseUtf8Boundaries) {
EXPECT_EQ(state.caret, 1u);
}
TEST(UITextInputControllerTest, DeleteUsesUtf8BoundariesAndLeavesCaretAtDeletePoint) {
if (static_cast<std::int32_t>(KeyCode::Delete) ==
static_cast<std::int32_t>(KeyCode::Backspace)) {
GTEST_SKIP() << "KeyCode::Delete currently aliases Backspace.";
}
UIText::UITextInputState state = {};
state.value = std::string("A") + "\xE4\xBD\xA0" + "B";
state.caret = 1u;
const auto result = UIText::HandleKeyDown(
state,
static_cast<std::int32_t>(KeyCode::Delete),
{},
{});
EXPECT_TRUE(result.handled);
EXPECT_TRUE(result.valueChanged);
EXPECT_FALSE(result.submitRequested);
EXPECT_EQ(state.value, "AB");
EXPECT_EQ(state.caret, 1u);
}
TEST(UITextInputControllerTest, DeleteClampsOversizedCaretAndDoesNotMutateAtDocumentEnd) {
if (static_cast<std::int32_t>(KeyCode::Delete) ==
static_cast<std::int32_t>(KeyCode::Backspace)) {
GTEST_SKIP() << "KeyCode::Delete currently aliases Backspace.";
}
UIText::UITextInputState state = {};
state.value = "AB";
state.caret = 99u;
const auto result = UIText::HandleKeyDown(
state,
static_cast<std::int32_t>(KeyCode::Delete),
{},
{});
EXPECT_TRUE(result.handled);
EXPECT_FALSE(result.valueChanged);
EXPECT_FALSE(result.submitRequested);
EXPECT_EQ(state.value, "AB");
EXPECT_EQ(state.caret, state.value.size());
}
TEST(UITextInputControllerTest, SingleLineEnterRequestsSubmitWithoutMutatingValue) {
UIText::UITextInputState state = {};
state.value = "prompt";
@@ -82,6 +126,64 @@ TEST(UITextInputControllerTest, MultilineEnterAndVerticalMovementStayInControlle
EXPECT_EQ(state.value, std::string("A") + "\xE4\xBD\xA0" + "Z\nBC\n");
}
TEST(UITextInputControllerTest, HomeAndEndRespectSingleLineAndMultilineBounds) {
UIText::UITextInputState singleLine = {};
singleLine.value = "prompt";
singleLine.caret = 2u;
const auto singleHome = UIText::HandleKeyDown(
singleLine,
static_cast<std::int32_t>(KeyCode::Home),
{},
{});
EXPECT_TRUE(singleHome.handled);
EXPECT_EQ(singleLine.caret, 0u);
const auto singleEnd = UIText::HandleKeyDown(
singleLine,
static_cast<std::int32_t>(KeyCode::End),
{},
{});
EXPECT_TRUE(singleEnd.handled);
EXPECT_EQ(singleLine.caret, singleLine.value.size());
UIText::UITextInputState multiline = {};
multiline.value = "root\nleaf\nend";
multiline.caret = 7u;
const UIText::UITextInputOptions options = { true, 4u };
const auto multilineHome = UIText::HandleKeyDown(
multiline,
static_cast<std::int32_t>(KeyCode::Home),
{},
options);
EXPECT_TRUE(multilineHome.handled);
EXPECT_EQ(multiline.caret, 5u);
multiline.caret = 7u;
const auto multilineEnd = UIText::HandleKeyDown(
multiline,
static_cast<std::int32_t>(KeyCode::End),
{},
options);
EXPECT_TRUE(multilineEnd.handled);
EXPECT_EQ(multiline.caret, 9u);
}
TEST(UITextInputControllerTest, ClampCaretAndInsertCharacterRecoverFromOversizedCaret) {
UIText::UITextInputState state = {};
state.value = "go";
state.caret = 42u;
UIText::ClampCaret(state);
EXPECT_EQ(state.caret, state.value.size());
state.caret = 42u;
EXPECT_TRUE(UIText::InsertCharacter(state, '!'));
EXPECT_EQ(state.value, "go!");
EXPECT_EQ(state.caret, state.value.size());
}
TEST(UITextInputControllerTest, MultilineTabAndShiftTabIndentAndOutdentCurrentLine) {
UIText::UITextInputState state = {};
state.value = "root\nnode";
@@ -112,4 +214,74 @@ TEST(UITextInputControllerTest, MultilineTabAndShiftTabIndentAndOutdentCurrentLi
EXPECT_EQ(state.caret, 5u);
}
TEST(UITextInputControllerTest, ShiftTabWithoutLeadingSpacesIsHandledWithoutMutatingText) {
UIText::UITextInputState state = {};
state.value = "root\nnode";
state.caret = 5u;
XCEngine::UI::UIInputModifiers modifiers = {};
modifiers.shift = true;
const auto result = UIText::HandleKeyDown(
state,
static_cast<std::int32_t>(KeyCode::Tab),
modifiers,
{ true, 4u });
EXPECT_TRUE(result.handled);
EXPECT_FALSE(result.valueChanged);
EXPECT_FALSE(result.submitRequested);
EXPECT_EQ(state.value, "root\nnode");
EXPECT_EQ(state.caret, 5u);
}
TEST(UITextInputControllerTest, MultilineTabIgnoresSystemModifiers) {
const auto buildState = []() {
UIText::UITextInputState state = {};
state.value = "root\nnode";
state.caret = 5u;
return state;
};
const UIText::UITextInputOptions options = { true, 4u };
XCEngine::UI::UIInputModifiers control = {};
control.control = true;
auto controlState = buildState();
const auto controlResult = UIText::HandleKeyDown(
controlState,
static_cast<std::int32_t>(KeyCode::Tab),
control,
options);
EXPECT_FALSE(controlResult.handled);
EXPECT_FALSE(controlResult.valueChanged);
EXPECT_EQ(controlState.value, "root\nnode");
EXPECT_EQ(controlState.caret, 5u);
XCEngine::UI::UIInputModifiers alt = {};
alt.alt = true;
auto altState = buildState();
const auto altResult = UIText::HandleKeyDown(
altState,
static_cast<std::int32_t>(KeyCode::Tab),
alt,
options);
EXPECT_FALSE(altResult.handled);
EXPECT_FALSE(altResult.valueChanged);
EXPECT_EQ(altState.value, "root\nnode");
EXPECT_EQ(altState.caret, 5u);
XCEngine::UI::UIInputModifiers superModifier = {};
superModifier.super = true;
auto superState = buildState();
const auto superResult = UIText::HandleKeyDown(
superState,
static_cast<std::int32_t>(KeyCode::Tab),
superModifier,
options);
EXPECT_FALSE(superResult.handled);
EXPECT_FALSE(superResult.valueChanged);
EXPECT_EQ(superState.value, "root\nnode");
EXPECT_EQ(superState.caret, 5u);
}
} // namespace