feat(xcui): add editor layout persistence validation

This commit is contained in:
2026-04-06 16:59:15 +08:00
parent eef5de7ee9
commit 9015b461bb
17 changed files with 1849 additions and 121 deletions

View File

@@ -6,6 +6,7 @@ set(EDITOR_UI_UNIT_TEST_SOURCES
test_ui_editor_collection_primitives.cpp
test_ui_editor_panel_chrome.cpp
test_ui_editor_workspace_controller.cpp
test_ui_editor_workspace_layout_persistence.cpp
test_ui_editor_workspace_model.cpp
test_ui_editor_workspace_session.cpp
)

View File

@@ -0,0 +1,185 @@
#include <gtest/gtest.h>
#include <XCNewEditor/Editor/UIEditorWorkspaceController.h>
#include <XCNewEditor/Editor/UIEditorWorkspaceLayoutPersistence.h>
#include <string>
#include <string_view>
#include <utility>
namespace {
using XCEngine::NewEditor::AreUIEditorWorkspaceLayoutSnapshotsEquivalent;
using XCEngine::NewEditor::BuildDefaultUIEditorWorkspaceController;
using XCEngine::NewEditor::BuildDefaultUIEditorWorkspaceSession;
using XCEngine::NewEditor::BuildUIEditorWorkspaceLayoutSnapshot;
using XCEngine::NewEditor::BuildUIEditorWorkspacePanel;
using XCEngine::NewEditor::BuildUIEditorWorkspaceSplit;
using XCEngine::NewEditor::BuildUIEditorWorkspaceTabStack;
using XCEngine::NewEditor::DeserializeUIEditorWorkspaceLayoutSnapshot;
using XCEngine::NewEditor::SerializeUIEditorWorkspaceLayoutSnapshot;
using XCEngine::NewEditor::TryCloseUIEditorWorkspacePanel;
using XCEngine::NewEditor::TryHideUIEditorWorkspacePanel;
using XCEngine::NewEditor::UIEditorPanelRegistry;
using XCEngine::NewEditor::UIEditorWorkspaceController;
using XCEngine::NewEditor::UIEditorWorkspaceLayoutLoadCode;
using XCEngine::NewEditor::UIEditorWorkspaceLayoutOperationStatus;
using XCEngine::NewEditor::UIEditorWorkspaceCommandKind;
using XCEngine::NewEditor::UIEditorWorkspaceCommandStatus;
using XCEngine::NewEditor::UIEditorWorkspaceModel;
using XCEngine::NewEditor::UIEditorWorkspaceSession;
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 }
};
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::string ReplaceFirst(
std::string source,
std::string_view from,
std::string_view to) {
const std::size_t index = source.find(from);
EXPECT_NE(index, std::string::npos);
if (index == std::string::npos) {
return source;
}
source.replace(index, from.size(), to);
return source;
}
} // namespace
TEST(UIEditorWorkspaceLayoutPersistenceTest, SerializeAndDeserializeRoundTripPreservesWorkspaceAndSession) {
const UIEditorPanelRegistry registry = BuildPanelRegistry();
UIEditorWorkspaceModel workspace = BuildWorkspace();
UIEditorWorkspaceSession session =
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
ASSERT_TRUE(TryHideUIEditorWorkspacePanel(registry, workspace, session, "doc-a"));
ASSERT_TRUE(TryCloseUIEditorWorkspacePanel(registry, workspace, session, "doc-b"));
const auto snapshot = BuildUIEditorWorkspaceLayoutSnapshot(workspace, session);
const std::string serialized = SerializeUIEditorWorkspaceLayoutSnapshot(snapshot);
const auto loadResult =
DeserializeUIEditorWorkspaceLayoutSnapshot(registry, serialized);
ASSERT_TRUE(loadResult.IsValid()) << loadResult.message;
EXPECT_TRUE(
AreUIEditorWorkspaceLayoutSnapshotsEquivalent(loadResult.snapshot, snapshot));
}
TEST(UIEditorWorkspaceLayoutPersistenceTest, DeserializeRejectsInvalidSelectedTabIndex) {
const UIEditorPanelRegistry registry = BuildPanelRegistry();
const UIEditorWorkspaceModel workspace = BuildWorkspace();
const UIEditorWorkspaceSession session =
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
const std::string invalidSerialized = ReplaceFirst(
SerializeUIEditorWorkspaceLayoutSnapshot(
BuildUIEditorWorkspaceLayoutSnapshot(workspace, session)),
"node_tabstack \"document-tabs\" 0 2",
"node_tabstack \"document-tabs\" 5 2");
const auto loadResult =
DeserializeUIEditorWorkspaceLayoutSnapshot(registry, invalidSerialized);
EXPECT_EQ(loadResult.code, UIEditorWorkspaceLayoutLoadCode::InvalidWorkspace);
}
TEST(UIEditorWorkspaceLayoutPersistenceTest, DeserializeRejectsMissingSessionRecord) {
const UIEditorPanelRegistry registry = BuildPanelRegistry();
const UIEditorWorkspaceModel workspace = BuildWorkspace();
const UIEditorWorkspaceSession session =
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
const std::string invalidSerialized = ReplaceFirst(
SerializeUIEditorWorkspaceLayoutSnapshot(
BuildUIEditorWorkspaceLayoutSnapshot(workspace, session)),
"session \"details\" 1 1\n",
"");
const auto loadResult =
DeserializeUIEditorWorkspaceLayoutSnapshot(registry, invalidSerialized);
EXPECT_EQ(loadResult.code, UIEditorWorkspaceLayoutLoadCode::InvalidWorkspaceSession);
}
TEST(UIEditorWorkspaceLayoutPersistenceTest, RestoreSerializedLayoutRestoresSavedStateAfterFurtherMutations) {
const UIEditorPanelRegistry registry = BuildPanelRegistry();
UIEditorWorkspaceController controller =
BuildDefaultUIEditorWorkspaceController(registry, BuildWorkspace());
EXPECT_EQ(
controller.Dispatch({ UIEditorWorkspaceCommandKind::HidePanel, "doc-a" }).status,
UIEditorWorkspaceCommandStatus::Changed);
const std::string savedLayout =
SerializeUIEditorWorkspaceLayoutSnapshot(controller.CaptureLayoutSnapshot());
EXPECT_EQ(
controller.Dispatch({ UIEditorWorkspaceCommandKind::ClosePanel, "doc-b" }).status,
UIEditorWorkspaceCommandStatus::Changed);
EXPECT_EQ(controller.GetWorkspace().activePanelId, "details");
const auto restoreResult = controller.RestoreSerializedLayout(savedLayout);
EXPECT_EQ(restoreResult.status, UIEditorWorkspaceLayoutOperationStatus::Changed);
EXPECT_EQ(controller.GetWorkspace().activePanelId, "doc-b");
ASSERT_EQ(restoreResult.visiblePanelIds.size(), 2u);
EXPECT_EQ(restoreResult.visiblePanelIds[0], "doc-b");
EXPECT_EQ(restoreResult.visiblePanelIds[1], "details");
}
TEST(UIEditorWorkspaceLayoutPersistenceTest, RestoreSerializedLayoutRejectsInvalidPayloadWithoutMutatingCurrentState) {
const UIEditorPanelRegistry registry = BuildPanelRegistry();
UIEditorWorkspaceController controller =
BuildDefaultUIEditorWorkspaceController(registry, BuildWorkspace());
EXPECT_EQ(
controller.Dispatch({ UIEditorWorkspaceCommandKind::ActivatePanel, "details" }).status,
UIEditorWorkspaceCommandStatus::Changed);
const auto before = controller.CaptureLayoutSnapshot();
const std::string invalidSerialized = ReplaceFirst(
SerializeUIEditorWorkspaceLayoutSnapshot(before),
"active \"details\"",
"active \"missing\"");
const auto restoreResult = controller.RestoreSerializedLayout(invalidSerialized);
EXPECT_EQ(restoreResult.status, UIEditorWorkspaceLayoutOperationStatus::Rejected);
const auto after = controller.CaptureLayoutSnapshot();
EXPECT_TRUE(AreUIEditorWorkspaceLayoutSnapshotsEquivalent(after, before));
}
TEST(UIEditorWorkspaceLayoutPersistenceTest, RestoreLayoutSnapshotReturnsNoOpWhenStateAlreadyMatchesSnapshot) {
UIEditorWorkspaceController controller =
BuildDefaultUIEditorWorkspaceController(BuildPanelRegistry(), BuildWorkspace());
const auto result = controller.RestoreLayoutSnapshot(controller.CaptureLayoutSnapshot());
EXPECT_EQ(result.status, UIEditorWorkspaceLayoutOperationStatus::NoOp);
}