feat(xcui): advance core and editor validation flow

This commit is contained in:
2026-04-06 16:20:46 +08:00
parent 33bb84f650
commit 2d030a97da
128 changed files with 9961 additions and 773 deletions

View File

@@ -1,19 +1,19 @@
set(EDITOR_UI_UNIT_TEST_SOURCES
test_editor_shell_asset_validation.cpp
test_input_modifier_tracker.cpp
test_editor_validation_registry.cpp
test_structured_editor_shell.cpp
test_ui_editor_panel_registry.cpp
test_ui_editor_collection_primitives.cpp
test_ui_editor_panel_chrome.cpp
test_ui_editor_workspace_controller.cpp
test_ui_editor_workspace_model.cpp
# Migration bridge: editor-facing XCUI primitive tests still reuse the
# legacy source location until they are relocated under tests/UI/Editor/unit.
${CMAKE_SOURCE_DIR}/tests/Core/UI/test_ui_editor_collection_primitives.cpp
${CMAKE_SOURCE_DIR}/tests/Core/UI/test_ui_editor_panel_chrome.cpp
test_ui_editor_workspace_session.cpp
)
add_executable(editor_ui_tests ${EDITOR_UI_UNIT_TEST_SOURCES})
target_link_libraries(editor_ui_tests
PRIVATE
editor_ui_validation_registry
XCNewEditorLib
GTest::gtest_main
)
@@ -23,7 +23,6 @@ target_include_directories(editor_ui_tests
${CMAKE_SOURCE_DIR}/new_editor/include
${CMAKE_SOURCE_DIR}/new_editor/src
${CMAKE_SOURCE_DIR}/engine/include
${CMAKE_SOURCE_DIR}/tests/UI/Editor/integration/shared/src
)
if(MSVC)

View File

@@ -0,0 +1,53 @@
#include <gtest/gtest.h>
#include "editor/EditorShellAsset.h"
#include <XCNewEditor/Editor/UIEditorPanelRegistry.h>
namespace {
using XCEngine::NewEditor::BuildDefaultEditorShellAsset;
using XCEngine::NewEditor::EditorShellAssetValidationCode;
using XCEngine::NewEditor::FindUIEditorPanelDescriptor;
using XCEngine::NewEditor::ValidateEditorShellAsset;
TEST(EditorShellAssetValidationTest, DefaultShellAssetPassesValidation) {
const auto shellAsset = BuildDefaultEditorShellAsset(".");
const auto validation = ValidateEditorShellAsset(shellAsset);
EXPECT_TRUE(validation.IsValid()) << validation.message;
}
TEST(EditorShellAssetValidationTest, ValidationRejectsWorkspacePanelMissingFromRegistry) {
auto shellAsset = BuildDefaultEditorShellAsset(".");
auto* documentPanel =
const_cast<XCEngine::NewEditor::UIEditorPanelDescriptor*>(
FindUIEditorPanelDescriptor(shellAsset.panelRegistry, "editor-foundation-root"));
ASSERT_NE(documentPanel, nullptr);
documentPanel->panelId = "editor-foundation-root-renamed";
const auto validation = ValidateEditorShellAsset(shellAsset);
EXPECT_EQ(validation.code, EditorShellAssetValidationCode::MissingPanelDescriptor)
<< validation.message;
}
TEST(EditorShellAssetValidationTest, ValidationRejectsWorkspaceTitleDriftFromRegistry) {
auto shellAsset = BuildDefaultEditorShellAsset(".");
shellAsset.workspace.activePanelId = "editor-foundation-root";
shellAsset.workspace.root.panel.title = "Drifted Title";
const auto validation = ValidateEditorShellAsset(shellAsset);
EXPECT_EQ(validation.code, EditorShellAssetValidationCode::PanelTitleMismatch);
}
TEST(EditorShellAssetValidationTest, ValidationRejectsInvalidWorkspaceSessionState) {
auto shellAsset = BuildDefaultEditorShellAsset(".");
ASSERT_EQ(shellAsset.workspaceSession.panelStates.size(), 1u);
shellAsset.workspaceSession.panelStates.front().open = false;
const auto validation = ValidateEditorShellAsset(shellAsset);
EXPECT_EQ(validation.code, EditorShellAssetValidationCode::InvalidWorkspaceSession);
}
} // namespace

View File

@@ -1,66 +0,0 @@
#include <gtest/gtest.h>
#include "EditorValidationScenario.h"
#include <filesystem>
namespace {
using XCEngine::Tests::EditorUI::FindEditorValidationScenario;
using XCEngine::Tests::EditorUI::GetDefaultEditorValidationScenario;
using XCEngine::Tests::EditorUI::UIValidationDomain;
} // namespace
TEST(EditorValidationRegistryTest, KnownEditorValidationScenariosResolveToExistingResources) {
const auto* pointerScenario = FindEditorValidationScenario("editor.input.pointer_states");
const auto* keyboardScenario = FindEditorValidationScenario("editor.input.keyboard_focus");
const auto* scrollScenario = FindEditorValidationScenario("editor.input.scroll_view");
const auto* shortcutScenario = FindEditorValidationScenario("editor.input.shortcut_scope");
const auto* splitterScenario = FindEditorValidationScenario("editor.layout.splitter_resize");
const auto* tabStripScenario = FindEditorValidationScenario("editor.layout.tab_strip_selection");
const auto* workspaceScenario = FindEditorValidationScenario("editor.layout.workspace_compose");
ASSERT_NE(pointerScenario, nullptr);
ASSERT_NE(keyboardScenario, nullptr);
ASSERT_NE(scrollScenario, nullptr);
ASSERT_NE(shortcutScenario, nullptr);
ASSERT_NE(splitterScenario, nullptr);
ASSERT_NE(tabStripScenario, nullptr);
ASSERT_NE(workspaceScenario, nullptr);
EXPECT_EQ(pointerScenario->domain, UIValidationDomain::Editor);
EXPECT_EQ(keyboardScenario->domain, UIValidationDomain::Editor);
EXPECT_EQ(scrollScenario->domain, UIValidationDomain::Editor);
EXPECT_EQ(shortcutScenario->domain, UIValidationDomain::Editor);
EXPECT_EQ(splitterScenario->domain, UIValidationDomain::Editor);
EXPECT_EQ(tabStripScenario->domain, UIValidationDomain::Editor);
EXPECT_EQ(workspaceScenario->domain, UIValidationDomain::Editor);
EXPECT_EQ(pointerScenario->categoryId, "input");
EXPECT_EQ(keyboardScenario->categoryId, "input");
EXPECT_EQ(scrollScenario->categoryId, "input");
EXPECT_EQ(shortcutScenario->categoryId, "input");
EXPECT_EQ(splitterScenario->categoryId, "layout");
EXPECT_EQ(tabStripScenario->categoryId, "layout");
EXPECT_EQ(workspaceScenario->categoryId, "layout");
EXPECT_TRUE(std::filesystem::exists(pointerScenario->documentPath));
EXPECT_TRUE(std::filesystem::exists(pointerScenario->themePath));
EXPECT_TRUE(std::filesystem::exists(keyboardScenario->documentPath));
EXPECT_TRUE(std::filesystem::exists(keyboardScenario->themePath));
EXPECT_TRUE(std::filesystem::exists(scrollScenario->documentPath));
EXPECT_TRUE(std::filesystem::exists(scrollScenario->themePath));
EXPECT_TRUE(std::filesystem::exists(shortcutScenario->documentPath));
EXPECT_TRUE(std::filesystem::exists(shortcutScenario->themePath));
EXPECT_TRUE(std::filesystem::exists(splitterScenario->documentPath));
EXPECT_TRUE(std::filesystem::exists(splitterScenario->themePath));
EXPECT_TRUE(std::filesystem::exists(tabStripScenario->documentPath));
EXPECT_TRUE(std::filesystem::exists(tabStripScenario->themePath));
EXPECT_TRUE(std::filesystem::exists(workspaceScenario->documentPath));
EXPECT_TRUE(std::filesystem::exists(workspaceScenario->themePath));
}
TEST(EditorValidationRegistryTest, DefaultScenarioPointsToKeyboardFocusBatch) {
const auto& scenario = GetDefaultEditorValidationScenario();
EXPECT_EQ(scenario.id, "editor.input.keyboard_focus");
EXPECT_EQ(scenario.domain, UIValidationDomain::Editor);
EXPECT_TRUE(std::filesystem::exists(scenario.documentPath));
}

View File

@@ -0,0 +1,84 @@
#include <gtest/gtest.h>
#include <XCEngine/UI/Style/Theme.h>
#include <XCEngine/UI/Style/StyleTypes.h>
#include <XCNewEditor/Widgets/UIEditorCollectionPrimitives.h>
namespace {
namespace Style = XCEngine::UI::Style;
namespace UIWidgets = XCEngine::UI::Widgets;
Style::UITheme BuildEditorPrimitiveTheme() {
Style::UIThemeDefinition definition = {};
definition.SetToken("space.cardInset", Style::UIStyleValue(14.0f));
definition.SetToken("size.treeItemHeight", Style::UIStyleValue(30.0f));
definition.SetToken("size.listItemHeight", Style::UIStyleValue(64.0f));
definition.SetToken("size.fieldRowHeight", Style::UIStyleValue(36.0f));
definition.SetToken("size.propertySectionHeight", Style::UIStyleValue(156.0f));
definition.SetToken("size.treeIndent", Style::UIStyleValue(20.0f));
return Style::BuildTheme(definition);
}
TEST(UIEditorCollectionPrimitivesTest, ClassifyAndFlagsMatchEditorCollectionTags) {
using Kind = UIWidgets::UIEditorCollectionPrimitiveKind;
EXPECT_EQ(UIWidgets::ClassifyUIEditorCollectionPrimitive("ScrollView"), Kind::ScrollView);
EXPECT_EQ(UIWidgets::ClassifyUIEditorCollectionPrimitive("TreeView"), Kind::TreeView);
EXPECT_EQ(UIWidgets::ClassifyUIEditorCollectionPrimitive("TreeItem"), Kind::TreeItem);
EXPECT_EQ(UIWidgets::ClassifyUIEditorCollectionPrimitive("ListView"), Kind::ListView);
EXPECT_EQ(UIWidgets::ClassifyUIEditorCollectionPrimitive("ListItem"), Kind::ListItem);
EXPECT_EQ(UIWidgets::ClassifyUIEditorCollectionPrimitive("PropertySection"), Kind::PropertySection);
EXPECT_EQ(UIWidgets::ClassifyUIEditorCollectionPrimitive("FieldRow"), Kind::FieldRow);
EXPECT_EQ(UIWidgets::ClassifyUIEditorCollectionPrimitive("Column"), Kind::None);
EXPECT_TRUE(UIWidgets::IsUIEditorCollectionPrimitiveContainer(Kind::ScrollView));
EXPECT_TRUE(UIWidgets::IsUIEditorCollectionPrimitiveContainer(Kind::TreeView));
EXPECT_TRUE(UIWidgets::IsUIEditorCollectionPrimitiveContainer(Kind::ListView));
EXPECT_TRUE(UIWidgets::IsUIEditorCollectionPrimitiveContainer(Kind::PropertySection));
EXPECT_FALSE(UIWidgets::IsUIEditorCollectionPrimitiveContainer(Kind::TreeItem));
EXPECT_TRUE(UIWidgets::UsesUIEditorCollectionPrimitiveColumnLayout(Kind::TreeView));
EXPECT_TRUE(UIWidgets::UsesUIEditorCollectionPrimitiveColumnLayout(Kind::ListView));
EXPECT_TRUE(UIWidgets::UsesUIEditorCollectionPrimitiveColumnLayout(Kind::PropertySection));
EXPECT_FALSE(UIWidgets::UsesUIEditorCollectionPrimitiveColumnLayout(Kind::ScrollView));
EXPECT_TRUE(UIWidgets::DoesUIEditorCollectionPrimitiveClipChildren(Kind::ScrollView));
EXPECT_TRUE(UIWidgets::DoesUIEditorCollectionPrimitiveClipChildren(Kind::TreeView));
EXPECT_TRUE(UIWidgets::DoesUIEditorCollectionPrimitiveClipChildren(Kind::ListView));
EXPECT_FALSE(UIWidgets::DoesUIEditorCollectionPrimitiveClipChildren(Kind::PropertySection));
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));
}
TEST(UIEditorCollectionPrimitivesTest, ResolveMetricsUseThemeTokensAndFallbacks) {
using Kind = UIWidgets::UIEditorCollectionPrimitiveKind;
const Style::UITheme themed = BuildEditorPrimitiveTheme();
const Style::UITheme fallback = Style::UITheme();
EXPECT_FLOAT_EQ(UIWidgets::ResolveUIEditorCollectionPrimitivePadding(Kind::TreeView, themed), 14.0f);
EXPECT_FLOAT_EQ(UIWidgets::ResolveUIEditorCollectionPrimitivePadding(Kind::ListView, themed), 14.0f);
EXPECT_FLOAT_EQ(UIWidgets::ResolveUIEditorCollectionPrimitivePadding(Kind::PropertySection, themed), 14.0f);
EXPECT_FLOAT_EQ(UIWidgets::ResolveUIEditorCollectionPrimitivePadding(Kind::ScrollView, themed), 0.0f);
EXPECT_FLOAT_EQ(UIWidgets::ResolveUIEditorCollectionPrimitiveDefaultHeight(Kind::TreeItem, themed), 30.0f);
EXPECT_FLOAT_EQ(UIWidgets::ResolveUIEditorCollectionPrimitiveDefaultHeight(Kind::ListItem, themed), 64.0f);
EXPECT_FLOAT_EQ(UIWidgets::ResolveUIEditorCollectionPrimitiveDefaultHeight(Kind::FieldRow, themed), 36.0f);
EXPECT_FLOAT_EQ(UIWidgets::ResolveUIEditorCollectionPrimitiveDefaultHeight(Kind::PropertySection, themed), 156.0f);
EXPECT_FLOAT_EQ(UIWidgets::ResolveUIEditorCollectionPrimitiveDefaultHeight(Kind::TreeItem, fallback), 28.0f);
EXPECT_FLOAT_EQ(UIWidgets::ResolveUIEditorCollectionPrimitiveDefaultHeight(Kind::ListItem, fallback), 60.0f);
EXPECT_FLOAT_EQ(UIWidgets::ResolveUIEditorCollectionPrimitiveDefaultHeight(Kind::FieldRow, fallback), 32.0f);
EXPECT_FLOAT_EQ(UIWidgets::ResolveUIEditorCollectionPrimitiveDefaultHeight(Kind::PropertySection, fallback), 148.0f);
EXPECT_FLOAT_EQ(UIWidgets::ResolveUIEditorCollectionPrimitiveIndent(Kind::TreeItem, themed, 2.0f), 40.0f);
EXPECT_FLOAT_EQ(UIWidgets::ResolveUIEditorCollectionPrimitiveIndent(Kind::TreeItem, fallback, 2.0f), 36.0f);
EXPECT_FLOAT_EQ(UIWidgets::ResolveUIEditorCollectionPrimitiveIndent(Kind::ListItem, themed, 2.0f), 0.0f);
}
} // namespace

View File

@@ -0,0 +1,126 @@
#include <gtest/gtest.h>
#include <XCEngine/UI/DrawData.h>
#include <XCNewEditor/Widgets/UIEditorPanelChrome.h>
namespace {
using XCEngine::UI::UIColor;
using XCEngine::UI::UIDrawCommandType;
using XCEngine::UI::UIDrawList;
using XCEngine::UI::UIRect;
using XCEngine::UI::Widgets::AppendUIEditorPanelChromeBackground;
using XCEngine::UI::Widgets::AppendUIEditorPanelChromeForeground;
using XCEngine::UI::Widgets::BuildUIEditorPanelChromeHeaderRect;
using XCEngine::UI::Widgets::ResolveUIEditorPanelChromeBorderColor;
using XCEngine::UI::Widgets::ResolveUIEditorPanelChromeBorderThickness;
using XCEngine::UI::Widgets::UIEditorPanelChromePalette;
using XCEngine::UI::Widgets::UIEditorPanelChromeState;
using XCEngine::UI::Widgets::UIEditorPanelChromeText;
void ExpectColorEq(
const UIColor& actual,
const UIColor& expected) {
EXPECT_FLOAT_EQ(actual.r, expected.r);
EXPECT_FLOAT_EQ(actual.g, expected.g);
EXPECT_FLOAT_EQ(actual.b, expected.b);
EXPECT_FLOAT_EQ(actual.a, expected.a);
}
TEST(UIEditorPanelChromeTest, HeaderRectAndBorderPolicyMatchNativeShellCardChrome) {
const UIRect panelRect(100.0f, 200.0f, 320.0f, 180.0f);
const UIEditorPanelChromePalette palette = {};
const auto headerRect = BuildUIEditorPanelChromeHeaderRect(panelRect);
EXPECT_FLOAT_EQ(headerRect.x, 100.0f);
EXPECT_FLOAT_EQ(headerRect.y, 200.0f);
EXPECT_FLOAT_EQ(headerRect.width, 320.0f);
EXPECT_FLOAT_EQ(headerRect.height, 42.0f);
ExpectColorEq(
ResolveUIEditorPanelChromeBorderColor(UIEditorPanelChromeState(), palette),
palette.borderColor);
ExpectColorEq(
ResolveUIEditorPanelChromeBorderColor(UIEditorPanelChromeState{ false, true }, palette),
palette.hoveredAccentColor);
ExpectColorEq(
ResolveUIEditorPanelChromeBorderColor(UIEditorPanelChromeState{ true, true }, palette),
palette.accentColor);
EXPECT_FLOAT_EQ(ResolveUIEditorPanelChromeBorderThickness(UIEditorPanelChromeState()), 1.0f);
EXPECT_FLOAT_EQ(ResolveUIEditorPanelChromeBorderThickness(UIEditorPanelChromeState{ true, false }), 2.0f);
}
TEST(UIEditorPanelChromeTest, BackgroundAppendEmitsSurfaceOutlineAndHeaderFill) {
UIDrawList drawList("PanelChrome");
const UIRect panelRect(40.0f, 60.0f, 400.0f, 280.0f);
const UIEditorPanelChromeState state{ true, false };
const UIEditorPanelChromePalette palette = {};
AppendUIEditorPanelChromeBackground(drawList, panelRect, state, palette);
ASSERT_EQ(drawList.GetCommandCount(), 3u);
const auto& commands = drawList.GetCommands();
EXPECT_EQ(commands[0].type, UIDrawCommandType::FilledRect);
EXPECT_EQ(commands[1].type, UIDrawCommandType::RectOutline);
EXPECT_EQ(commands[2].type, UIDrawCommandType::FilledRect);
EXPECT_FLOAT_EQ(commands[0].rect.x, 40.0f);
EXPECT_FLOAT_EQ(commands[0].rounding, 18.0f);
ExpectColorEq(commands[0].color, palette.surfaceColor);
EXPECT_FLOAT_EQ(commands[1].thickness, 2.0f);
EXPECT_FLOAT_EQ(commands[1].rounding, 18.0f);
ExpectColorEq(commands[1].color, palette.accentColor);
EXPECT_FLOAT_EQ(commands[2].rect.height, 42.0f);
EXPECT_FLOAT_EQ(commands[2].rounding, 18.0f);
ExpectColorEq(commands[2].color, palette.headerColor);
}
TEST(UIEditorPanelChromeTest, ForegroundAppendPlacesTitleSubtitleAndFooterAtCurrentOffsets) {
UIDrawList drawList("PanelChromeText");
const UIRect panelRect(100.0f, 200.0f, 320.0f, 180.0f);
const UIEditorPanelChromePalette palette = {};
const UIEditorPanelChromeText text{
"XCUI Demo",
"native queued offscreen surface",
"Active | 42 elements | 9 cmds"
};
AppendUIEditorPanelChromeForeground(drawList, panelRect, text, palette);
ASSERT_EQ(drawList.GetCommandCount(), 3u);
const auto& commands = drawList.GetCommands();
EXPECT_EQ(commands[0].type, UIDrawCommandType::Text);
EXPECT_EQ(commands[1].type, UIDrawCommandType::Text);
EXPECT_EQ(commands[2].type, UIDrawCommandType::Text);
EXPECT_EQ(commands[0].text, "XCUI Demo");
EXPECT_FLOAT_EQ(commands[0].position.x, 116.0f);
EXPECT_FLOAT_EQ(commands[0].position.y, 212.0f);
ExpectColorEq(commands[0].color, palette.textPrimary);
EXPECT_EQ(commands[1].text, "native queued offscreen surface");
EXPECT_FLOAT_EQ(commands[1].position.x, 116.0f);
EXPECT_FLOAT_EQ(commands[1].position.y, 228.0f);
ExpectColorEq(commands[1].color, palette.textSecondary);
EXPECT_EQ(commands[2].text, "Active | 42 elements | 9 cmds");
EXPECT_FLOAT_EQ(commands[2].position.x, 116.0f);
EXPECT_FLOAT_EQ(commands[2].position.y, 362.0f);
ExpectColorEq(commands[2].color, palette.textMuted);
}
TEST(UIEditorPanelChromeTest, ForegroundAppendSkipsEmptyStrings) {
UIDrawList drawList("PanelChromeEmptyText");
AppendUIEditorPanelChromeForeground(
drawList,
UIRect(0.0f, 0.0f, 320.0f, 180.0f),
UIEditorPanelChromeText{});
EXPECT_EQ(drawList.GetCommandCount(), 0u);
}
} // namespace

View File

@@ -0,0 +1,49 @@
#include <gtest/gtest.h>
#include <XCNewEditor/Editor/UIEditorPanelRegistry.h>
namespace {
using XCEngine::NewEditor::BuildDefaultEditorShellPanelRegistry;
using XCEngine::NewEditor::FindUIEditorPanelDescriptor;
using XCEngine::NewEditor::UIEditorPanelDescriptor;
using XCEngine::NewEditor::UIEditorPanelRegistry;
using XCEngine::NewEditor::UIEditorPanelRegistryValidationCode;
using XCEngine::NewEditor::ValidateUIEditorPanelRegistry;
TEST(UIEditorPanelRegistryTest, DefaultRegistryContainsShellDescriptors) {
const UIEditorPanelRegistry registry = BuildDefaultEditorShellPanelRegistry();
ASSERT_EQ(registry.panels.size(), 1u);
const UIEditorPanelDescriptor* descriptor =
FindUIEditorPanelDescriptor(registry, "editor-foundation-root");
ASSERT_NE(descriptor, nullptr);
EXPECT_FALSE(descriptor->canHide);
EXPECT_FALSE(descriptor->canClose);
EXPECT_EQ(FindUIEditorPanelDescriptor(registry, "missing-panel"), nullptr);
}
TEST(UIEditorPanelRegistryTest, ValidationRejectsEmptyPanelIdAndTitle) {
UIEditorPanelRegistry emptyIdRegistry = {};
emptyIdRegistry.panels.push_back(UIEditorPanelDescriptor{ "", "Panel", {}, true });
EXPECT_EQ(
ValidateUIEditorPanelRegistry(emptyIdRegistry).code,
UIEditorPanelRegistryValidationCode::EmptyPanelId);
UIEditorPanelRegistry emptyTitleRegistry = {};
emptyTitleRegistry.panels.push_back(UIEditorPanelDescriptor{ "panel-a", "", {}, true });
EXPECT_EQ(
ValidateUIEditorPanelRegistry(emptyTitleRegistry).code,
UIEditorPanelRegistryValidationCode::EmptyDefaultTitle);
}
TEST(UIEditorPanelRegistryTest, ValidationRejectsDuplicatePanelId) {
UIEditorPanelRegistry registry = {};
registry.panels.push_back(UIEditorPanelDescriptor{ "panel-a", "Panel A", {}, true });
registry.panels.push_back(UIEditorPanelDescriptor{ "panel-a", "Panel A Duplicate", {}, true });
const auto validation = ValidateUIEditorPanelRegistry(registry);
EXPECT_EQ(validation.code, UIEditorPanelRegistryValidationCode::DuplicatePanelId);
}
} // namespace

View File

@@ -0,0 +1,148 @@
#include <gtest/gtest.h>
#include <XCNewEditor/Editor/UIEditorWorkspaceController.h>
namespace {
using XCEngine::NewEditor::BuildDefaultUIEditorWorkspaceController;
using XCEngine::NewEditor::BuildDefaultUIEditorWorkspaceSession;
using XCEngine::NewEditor::BuildUIEditorWorkspacePanel;
using XCEngine::NewEditor::BuildUIEditorWorkspaceSplit;
using XCEngine::NewEditor::BuildUIEditorWorkspaceTabStack;
using XCEngine::NewEditor::GetUIEditorWorkspaceCommandKindName;
using XCEngine::NewEditor::GetUIEditorWorkspaceCommandStatusName;
using XCEngine::NewEditor::UIEditorPanelRegistry;
using XCEngine::NewEditor::UIEditorWorkspaceCommand;
using XCEngine::NewEditor::UIEditorWorkspaceCommandKind;
using XCEngine::NewEditor::UIEditorWorkspaceCommandStatus;
using XCEngine::NewEditor::UIEditorWorkspaceControllerValidationCode;
using XCEngine::NewEditor::UIEditorWorkspaceController;
using XCEngine::NewEditor::UIEditorWorkspaceModel;
using XCEngine::NewEditor::UIEditorWorkspaceSplitAxis;
UIEditorPanelRegistry BuildPanelRegistry() {
UIEditorPanelRegistry registry = {};
registry.panels = {
{ "doc-a", "Document A", {}, true, true, true },
{ "doc-b", "Document B", {}, true, true, true },
{ "details", "Details", {}, true, true, true },
{ "root", "Root", {}, true, false, false }
};
return registry;
}
UIEditorWorkspaceModel BuildWorkspace() {
UIEditorWorkspaceModel workspace = {};
workspace.root = BuildUIEditorWorkspaceSplit(
"root-split",
UIEditorWorkspaceSplitAxis::Horizontal,
0.66f,
BuildUIEditorWorkspaceTabStack(
"document-tabs",
{
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true),
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true)
},
0u),
BuildUIEditorWorkspacePanel("details-node", "details", "Details", true));
workspace.activePanelId = "doc-a";
return workspace;
}
} // namespace
TEST(UIEditorWorkspaceControllerTest, CommandNameHelpersExposeStableDebugNames) {
EXPECT_EQ(GetUIEditorWorkspaceCommandKindName(UIEditorWorkspaceCommandKind::HidePanel), "HidePanel");
EXPECT_EQ(GetUIEditorWorkspaceCommandStatusName(UIEditorWorkspaceCommandStatus::Changed), "Changed");
}
TEST(UIEditorWorkspaceControllerTest, ValidateStateUsesControllerLevelErrorClassification) {
UIEditorPanelRegistry registry = BuildPanelRegistry();
UIEditorWorkspaceModel workspace = BuildWorkspace();
workspace.activePanelId = "missing-panel";
UIEditorWorkspaceController controller(
registry,
workspace,
BuildDefaultUIEditorWorkspaceSession(registry, workspace));
const auto validation = controller.ValidateState();
EXPECT_EQ(validation.code, UIEditorWorkspaceControllerValidationCode::InvalidWorkspace);
}
TEST(UIEditorWorkspaceControllerTest, HideCommandChangesStateAndRepeatedHideBecomesNoOp) {
UIEditorWorkspaceController controller =
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
const auto first = controller.Dispatch({ UIEditorWorkspaceCommandKind::HidePanel, "doc-a" });
EXPECT_EQ(first.status, UIEditorWorkspaceCommandStatus::Changed);
EXPECT_EQ(first.activePanelId, "doc-b");
ASSERT_EQ(first.visiblePanelIds.size(), 2u);
EXPECT_EQ(first.visiblePanelIds[0], "doc-b");
EXPECT_EQ(first.visiblePanelIds[1], "details");
const auto second = controller.Dispatch({ UIEditorWorkspaceCommandKind::HidePanel, "doc-a" });
EXPECT_EQ(second.status, UIEditorWorkspaceCommandStatus::NoOp);
EXPECT_EQ(second.message, "Panel is already hidden.");
}
TEST(UIEditorWorkspaceControllerTest, ClosedPanelCannotBeActivatedUntilOpenedAgain) {
UIEditorWorkspaceController controller =
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
EXPECT_EQ(
controller.Dispatch({ UIEditorWorkspaceCommandKind::ClosePanel, "doc-b" }).status,
UIEditorWorkspaceCommandStatus::Changed);
const auto activateClosed =
controller.Dispatch({ UIEditorWorkspaceCommandKind::ActivatePanel, "doc-b" });
EXPECT_EQ(activateClosed.status, UIEditorWorkspaceCommandStatus::Rejected);
EXPECT_EQ(activateClosed.activePanelId, "doc-a");
const auto reopen =
controller.Dispatch({ UIEditorWorkspaceCommandKind::OpenPanel, "doc-b" });
EXPECT_EQ(reopen.status, UIEditorWorkspaceCommandStatus::Changed);
EXPECT_EQ(reopen.activePanelId, "doc-b");
}
TEST(UIEditorWorkspaceControllerTest, ResetCommandRestoresBaselineState) {
UIEditorWorkspaceController controller =
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
EXPECT_EQ(
controller.Dispatch({ UIEditorWorkspaceCommandKind::HidePanel, "doc-a" }).status,
UIEditorWorkspaceCommandStatus::Changed);
EXPECT_EQ(
controller.Dispatch({ UIEditorWorkspaceCommandKind::ClosePanel, "doc-b" }).status,
UIEditorWorkspaceCommandStatus::Changed);
EXPECT_EQ(controller.GetWorkspace().activePanelId, "details");
const auto reset = controller.Dispatch({ UIEditorWorkspaceCommandKind::ResetWorkspace, {} });
EXPECT_EQ(reset.status, UIEditorWorkspaceCommandStatus::Changed);
EXPECT_EQ(reset.activePanelId, "doc-a");
ASSERT_EQ(reset.visiblePanelIds.size(), 2u);
EXPECT_EQ(reset.visiblePanelIds[0], "doc-a");
EXPECT_EQ(reset.visiblePanelIds[1], "details");
const auto repeatReset = controller.Dispatch({ UIEditorWorkspaceCommandKind::ResetWorkspace, {} });
EXPECT_EQ(repeatReset.status, UIEditorWorkspaceCommandStatus::NoOp);
}
TEST(UIEditorWorkspaceControllerTest, RejectsUnknownPanelAndNonCloseablePanelCommands) {
UIEditorWorkspaceController controller =
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
const auto unknown = controller.Dispatch({ UIEditorWorkspaceCommandKind::ShowPanel, "missing" });
EXPECT_EQ(unknown.status, UIEditorWorkspaceCommandStatus::Rejected);
UIEditorWorkspaceModel rootWorkspace = {};
rootWorkspace.root = BuildUIEditorWorkspacePanel("root-node", "root", "Root", true);
rootWorkspace.activePanelId = "root";
UIEditorWorkspaceController rootController =
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), rootWorkspace);
const auto nonCloseable =
rootController.Dispatch({ UIEditorWorkspaceCommandKind::ClosePanel, "root" });
EXPECT_EQ(nonCloseable.status, UIEditorWorkspaceCommandStatus::Rejected);
EXPECT_EQ(rootController.GetWorkspace().activePanelId, "root");
}

View File

@@ -0,0 +1,183 @@
#include <gtest/gtest.h>
#include <XCNewEditor/Editor/UIEditorPanelRegistry.h>
#include <XCNewEditor/Editor/UIEditorWorkspaceModel.h>
#include <XCNewEditor/Editor/UIEditorWorkspaceSession.h>
#include <vector>
namespace {
using XCEngine::NewEditor::BuildDefaultUIEditorWorkspaceSession;
using XCEngine::NewEditor::BuildUIEditorWorkspacePanel;
using XCEngine::NewEditor::BuildUIEditorWorkspaceSplit;
using XCEngine::NewEditor::BuildUIEditorWorkspaceTabStack;
using XCEngine::NewEditor::CollectUIEditorWorkspaceVisiblePanels;
using XCEngine::NewEditor::FindUIEditorPanelDescriptor;
using XCEngine::NewEditor::FindUIEditorPanelSessionState;
using XCEngine::NewEditor::TryActivateUIEditorWorkspacePanel;
using XCEngine::NewEditor::TryCloseUIEditorWorkspacePanel;
using XCEngine::NewEditor::TryHideUIEditorWorkspacePanel;
using XCEngine::NewEditor::TryOpenUIEditorWorkspacePanel;
using XCEngine::NewEditor::TryShowUIEditorWorkspacePanel;
using XCEngine::NewEditor::UIEditorPanelRegistry;
using XCEngine::NewEditor::UIEditorWorkspaceModel;
using XCEngine::NewEditor::UIEditorWorkspaceSession;
using XCEngine::NewEditor::UIEditorWorkspaceSessionValidationCode;
using XCEngine::NewEditor::UIEditorWorkspaceSplitAxis;
using XCEngine::NewEditor::ValidateUIEditorWorkspaceSession;
UIEditorPanelRegistry BuildPanelRegistry() {
UIEditorPanelRegistry registry = {};
registry.panels = {
{ "doc-a", "Document A", {}, true, true, true },
{ "doc-b", "Document B", {}, true, true, true },
{ "details", "Details", {}, true, true, true },
{ "root", "Root", {}, true, false, false }
};
return registry;
}
UIEditorWorkspaceModel BuildWorkspace() {
UIEditorWorkspaceModel workspace = {};
workspace.root = BuildUIEditorWorkspaceSplit(
"root-split",
UIEditorWorkspaceSplitAxis::Horizontal,
0.66f,
BuildUIEditorWorkspaceTabStack(
"document-tabs",
{
BuildUIEditorWorkspacePanel("doc-a-node", "doc-a", "Document A", true),
BuildUIEditorWorkspacePanel("doc-b-node", "doc-b", "Document B", true)
},
0u),
BuildUIEditorWorkspacePanel("details-node", "details", "Details", true));
workspace.activePanelId = "doc-a";
return workspace;
}
std::vector<std::string> CollectVisiblePanelIds(
const UIEditorWorkspaceModel& workspace,
const UIEditorWorkspaceSession& session) {
const auto panels = CollectUIEditorWorkspaceVisiblePanels(workspace, session);
std::vector<std::string> ids = {};
ids.reserve(panels.size());
for (const auto& panel : panels) {
ids.push_back(panel.panelId);
}
return ids;
}
} // namespace
TEST(UIEditorWorkspaceSessionTest, DefaultSessionTracksEveryWorkspacePanelAsOpenAndVisible) {
const UIEditorPanelRegistry registry = BuildPanelRegistry();
const UIEditorWorkspaceModel workspace = BuildWorkspace();
const UIEditorWorkspaceSession session =
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
const auto validation = ValidateUIEditorWorkspaceSession(registry, workspace, session);
ASSERT_TRUE(validation.IsValid()) << validation.message;
ASSERT_EQ(session.panelStates.size(), 3u);
ASSERT_NE(FindUIEditorPanelSessionState(session, "doc-a"), nullptr);
ASSERT_NE(FindUIEditorPanelSessionState(session, "doc-b"), nullptr);
ASSERT_NE(FindUIEditorPanelSessionState(session, "details"), nullptr);
const auto visibleIds = CollectVisiblePanelIds(workspace, session);
ASSERT_EQ(visibleIds.size(), 2u);
EXPECT_EQ(visibleIds[0], "doc-a");
EXPECT_EQ(visibleIds[1], "details");
}
TEST(UIEditorWorkspaceSessionTest, HidingActiveDocumentSelectsNextVisiblePanelInTabStack) {
const UIEditorPanelRegistry registry = BuildPanelRegistry();
UIEditorWorkspaceModel workspace = BuildWorkspace();
UIEditorWorkspaceSession session =
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
ASSERT_TRUE(TryHideUIEditorWorkspacePanel(registry, workspace, session, "doc-a"));
const auto* hiddenState = FindUIEditorPanelSessionState(session, "doc-a");
ASSERT_NE(hiddenState, nullptr);
EXPECT_TRUE(hiddenState->open);
EXPECT_FALSE(hiddenState->visible);
EXPECT_EQ(workspace.activePanelId, "doc-b");
EXPECT_EQ(workspace.root.children.front().selectedTabIndex, 1u);
const auto visibleIds = CollectVisiblePanelIds(workspace, session);
ASSERT_EQ(visibleIds.size(), 2u);
EXPECT_EQ(visibleIds[0], "doc-b");
EXPECT_EQ(visibleIds[1], "details");
}
TEST(UIEditorWorkspaceSessionTest, ShowingHiddenPanelRestoresVisibilityAndActivation) {
const UIEditorPanelRegistry registry = BuildPanelRegistry();
UIEditorWorkspaceModel workspace = BuildWorkspace();
UIEditorWorkspaceSession session =
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
ASSERT_TRUE(TryHideUIEditorWorkspacePanel(registry, workspace, session, "doc-a"));
ASSERT_TRUE(TryShowUIEditorWorkspacePanel(registry, workspace, session, "doc-a"));
EXPECT_EQ(workspace.activePanelId, "doc-a");
EXPECT_EQ(workspace.root.children.front().selectedTabIndex, 0u);
const auto* restoredState = FindUIEditorPanelSessionState(session, "doc-a");
ASSERT_NE(restoredState, nullptr);
EXPECT_TRUE(restoredState->open);
EXPECT_TRUE(restoredState->visible);
}
TEST(UIEditorWorkspaceSessionTest, ClosingPanelPreventsActivationUntilReopened) {
const UIEditorPanelRegistry registry = BuildPanelRegistry();
UIEditorWorkspaceModel workspace = BuildWorkspace();
UIEditorWorkspaceSession session =
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
ASSERT_TRUE(TryCloseUIEditorWorkspacePanel(registry, workspace, session, "doc-a"));
EXPECT_FALSE(TryActivateUIEditorWorkspacePanel(registry, workspace, session, "doc-a"));
EXPECT_EQ(workspace.activePanelId, "doc-b");
ASSERT_TRUE(TryOpenUIEditorWorkspacePanel(registry, workspace, session, "doc-a"));
EXPECT_EQ(workspace.activePanelId, "doc-a");
EXPECT_EQ(workspace.root.children.front().selectedTabIndex, 0u);
}
TEST(UIEditorWorkspaceSessionTest, NonCloseablePanelCannotBeClosedOrHidden) {
UIEditorPanelRegistry registry = BuildPanelRegistry();
UIEditorWorkspaceModel workspace = {};
workspace.root = BuildUIEditorWorkspacePanel("root-node", "root", "Root", true);
workspace.activePanelId = "root";
UIEditorWorkspaceSession session =
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
ASSERT_NE(FindUIEditorPanelDescriptor(registry, "root"), nullptr);
EXPECT_FALSE(TryCloseUIEditorWorkspacePanel(registry, workspace, session, "root"));
EXPECT_FALSE(TryHideUIEditorWorkspacePanel(registry, workspace, session, "root"));
const auto* rootState = FindUIEditorPanelSessionState(session, "root");
ASSERT_NE(rootState, nullptr);
EXPECT_TRUE(rootState->open);
EXPECT_TRUE(rootState->visible);
}
TEST(UIEditorWorkspaceSessionTest, ValidationRejectsMissingPanelStateAndInvalidActivePanel) {
const UIEditorPanelRegistry registry = BuildPanelRegistry();
UIEditorWorkspaceModel workspace = BuildWorkspace();
UIEditorWorkspaceSession session =
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
session.panelStates.pop_back();
auto validation = ValidateUIEditorWorkspaceSession(registry, workspace, session);
EXPECT_EQ(validation.code, UIEditorWorkspaceSessionValidationCode::MissingPanelState);
session = BuildDefaultUIEditorWorkspaceSession(registry, workspace);
ASSERT_TRUE(TryHideUIEditorWorkspacePanel(registry, workspace, session, "doc-a"));
ASSERT_TRUE(TryHideUIEditorWorkspacePanel(registry, workspace, session, "doc-b"));
workspace.activePanelId = "doc-a";
validation = ValidateUIEditorWorkspaceSession(registry, workspace, session);
EXPECT_EQ(validation.code, UIEditorWorkspaceSessionValidationCode::InvalidActivePanelId);
}