Add editor panel host lifecycle contract

This commit is contained in:
2026-04-07 13:24:56 +08:00
parent 6bf61ad8e2
commit b2ab516228
10 changed files with 1224 additions and 0 deletions

View File

@@ -9,6 +9,7 @@ set(EDITOR_UI_UNIT_TEST_SOURCES
test_ui_editor_menu_session.cpp
test_ui_editor_menu_bar.cpp
test_ui_editor_menu_popup.cpp
test_ui_editor_panel_host_lifecycle.cpp
test_ui_editor_panel_registry.cpp
test_ui_editor_shell_compose.cpp
test_ui_editor_shell_interaction.cpp

View File

@@ -0,0 +1,246 @@
#include <gtest/gtest.h>
#include <XCEditor/Core/UIEditorPanelHostLifecycle.h>
namespace {
using XCEngine::UI::Editor::BuildDefaultUIEditorWorkspaceSession;
using XCEngine::UI::Editor::BuildUIEditorWorkspacePanel;
using XCEngine::UI::Editor::BuildUIEditorWorkspaceSplit;
using XCEngine::UI::Editor::BuildUIEditorWorkspaceTabStack;
using XCEngine::UI::Editor::FindUIEditorPanelHostState;
using XCEngine::UI::Editor::GetUIEditorPanelHostLifecycleEventKindName;
using XCEngine::UI::Editor::TryCloseUIEditorWorkspacePanel;
using XCEngine::UI::Editor::TryHideUIEditorWorkspacePanel;
using XCEngine::UI::Editor::TryOpenUIEditorWorkspacePanel;
using XCEngine::UI::Editor::UIEditorPanelHostLifecycleRequest;
using XCEngine::UI::Editor::UIEditorPanelHostLifecycleState;
using XCEngine::UI::Editor::UIEditorPanelRegistry;
using XCEngine::UI::Editor::UIEditorWorkspaceModel;
using XCEngine::UI::Editor::UIEditorWorkspaceSession;
using XCEngine::UI::Editor::UIEditorWorkspaceSplitAxis;
using XCEngine::UI::Editor::UpdateUIEditorPanelHostLifecycle;
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::vector<std::string> FormatEvents(
const std::vector<XCEngine::UI::Editor::UIEditorPanelHostLifecycleEvent>& events) {
std::vector<std::string> formatted = {};
formatted.reserve(events.size());
for (const auto& event : events) {
formatted.push_back(
std::string(GetUIEditorPanelHostLifecycleEventKindName(event.kind)) + ":" + event.panelId);
}
return formatted;
}
} // namespace
TEST(UIEditorPanelHostLifecycleTest, InitialFrameResolvesAttachedVisibleActiveAndFocusedPanels) {
const UIEditorPanelRegistry registry = BuildPanelRegistry();
const UIEditorWorkspaceModel workspace = BuildWorkspace();
const UIEditorWorkspaceSession session =
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
UIEditorPanelHostLifecycleState lifecycleState = {};
const auto frame = UpdateUIEditorPanelHostLifecycle(
lifecycleState,
registry,
workspace,
session,
UIEditorPanelHostLifecycleRequest{ "doc-a" });
ASSERT_EQ(frame.panelStates.size(), 3u);
const auto* docA = FindUIEditorPanelHostState(frame, "doc-a");
const auto* docB = FindUIEditorPanelHostState(frame, "doc-b");
const auto* details = FindUIEditorPanelHostState(frame, "details");
ASSERT_NE(docA, nullptr);
ASSERT_NE(docB, nullptr);
ASSERT_NE(details, nullptr);
EXPECT_TRUE(docA->attached);
EXPECT_TRUE(docA->visible);
EXPECT_TRUE(docA->active);
EXPECT_TRUE(docA->focused);
EXPECT_TRUE(docB->attached);
EXPECT_FALSE(docB->visible);
EXPECT_FALSE(docB->active);
EXPECT_FALSE(docB->focused);
EXPECT_TRUE(details->attached);
EXPECT_TRUE(details->visible);
EXPECT_FALSE(details->active);
EXPECT_FALSE(details->focused);
const std::vector<std::string> expectedEvents = {
"Attached:doc-a",
"Attached:doc-b",
"Attached:details",
"Shown:doc-a",
"Shown:details",
"Activated:doc-a",
"FocusGained:doc-a"
};
EXPECT_EQ(FormatEvents(frame.events), expectedEvents);
}
TEST(UIEditorPanelHostLifecycleTest, HidingActiveTabKeepsPanelAttachedButEmitsVisibilityAndActivationTransitions) {
const UIEditorPanelRegistry registry = BuildPanelRegistry();
UIEditorWorkspaceModel workspace = BuildWorkspace();
UIEditorWorkspaceSession session =
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
UIEditorPanelHostLifecycleState lifecycleState = {};
UpdateUIEditorPanelHostLifecycle(
lifecycleState,
registry,
workspace,
session,
UIEditorPanelHostLifecycleRequest{ "doc-a" });
ASSERT_TRUE(TryHideUIEditorWorkspacePanel(registry, workspace, session, "doc-a"));
const auto frame = UpdateUIEditorPanelHostLifecycle(
lifecycleState,
registry,
workspace,
session,
UIEditorPanelHostLifecycleRequest{ "doc-b" });
const auto* docA = FindUIEditorPanelHostState(frame, "doc-a");
const auto* docB = FindUIEditorPanelHostState(frame, "doc-b");
ASSERT_NE(docA, nullptr);
ASSERT_NE(docB, nullptr);
EXPECT_TRUE(docA->attached);
EXPECT_FALSE(docA->visible);
EXPECT_FALSE(docA->active);
EXPECT_FALSE(docA->focused);
EXPECT_TRUE(docB->attached);
EXPECT_TRUE(docB->visible);
EXPECT_TRUE(docB->active);
EXPECT_TRUE(docB->focused);
const std::vector<std::string> expectedEvents = {
"FocusLost:doc-a",
"Deactivated:doc-a",
"Hidden:doc-a",
"Shown:doc-b",
"Activated:doc-b",
"FocusGained:doc-b"
};
EXPECT_EQ(FormatEvents(frame.events), expectedEvents);
}
TEST(UIEditorPanelHostLifecycleTest, ClosingHiddenPanelEmitsDetachWithoutVisibilityTransitions) {
const UIEditorPanelRegistry registry = BuildPanelRegistry();
UIEditorWorkspaceModel workspace = BuildWorkspace();
UIEditorWorkspaceSession session =
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
UIEditorPanelHostLifecycleState lifecycleState = {};
UpdateUIEditorPanelHostLifecycle(
lifecycleState,
registry,
workspace,
session,
UIEditorPanelHostLifecycleRequest{ "doc-a" });
ASSERT_TRUE(TryCloseUIEditorWorkspacePanel(registry, workspace, session, "doc-b"));
const auto frame = UpdateUIEditorPanelHostLifecycle(
lifecycleState,
registry,
workspace,
session,
UIEditorPanelHostLifecycleRequest{ "doc-a" });
const auto* docB = FindUIEditorPanelHostState(frame, "doc-b");
ASSERT_NE(docB, nullptr);
EXPECT_FALSE(docB->attached);
EXPECT_FALSE(docB->visible);
EXPECT_FALSE(docB->active);
EXPECT_FALSE(docB->focused);
const std::vector<std::string> expectedEvents = {
"Detached:doc-b"
};
EXPECT_EQ(FormatEvents(frame.events), expectedEvents);
}
TEST(UIEditorPanelHostLifecycleTest, ClearingFocusOnlyEmitsFocusLostAndReopeningPanelReattachesIt) {
const UIEditorPanelRegistry registry = BuildPanelRegistry();
UIEditorWorkspaceModel workspace = BuildWorkspace();
UIEditorWorkspaceSession session =
BuildDefaultUIEditorWorkspaceSession(registry, workspace);
UIEditorPanelHostLifecycleState lifecycleState = {};
UpdateUIEditorPanelHostLifecycle(
lifecycleState,
registry,
workspace,
session,
UIEditorPanelHostLifecycleRequest{ "doc-a" });
const auto focusLostFrame = UpdateUIEditorPanelHostLifecycle(
lifecycleState,
registry,
workspace,
session,
UIEditorPanelHostLifecycleRequest{});
EXPECT_EQ(
FormatEvents(focusLostFrame.events),
std::vector<std::string>({ "FocusLost:doc-a" }));
ASSERT_TRUE(TryCloseUIEditorWorkspacePanel(registry, workspace, session, "doc-b"));
UpdateUIEditorPanelHostLifecycle(
lifecycleState,
registry,
workspace,
session,
UIEditorPanelHostLifecycleRequest{});
ASSERT_TRUE(TryOpenUIEditorWorkspacePanel(registry, workspace, session, "doc-b"));
const auto reopenFrame = UpdateUIEditorPanelHostLifecycle(
lifecycleState,
registry,
workspace,
session,
UIEditorPanelHostLifecycleRequest{ "doc-b" });
const std::vector<std::string> expectedEvents = {
"Deactivated:doc-a",
"Hidden:doc-a",
"Attached:doc-b",
"Shown:doc-b",
"Activated:doc-b",
"FocusGained:doc-b"
};
EXPECT_EQ(FormatEvents(reopenFrame.events), expectedEvents);
}