Internalize editor shell asset definition contract

This commit is contained in:
2026-04-07 12:38:23 +08:00
parent d14fa6be07
commit 49e9b63a2d
5 changed files with 337 additions and 10 deletions

View File

@@ -16,6 +16,17 @@ TEST(EditorShellAssetValidationTest, DefaultShellAssetPassesValidation) {
const auto validation = ValidateEditorShellAsset(shellAsset);
EXPECT_TRUE(validation.IsValid()) << validation.message;
ASSERT_EQ(
shellAsset.shellDefinition.workspacePresentations.size(),
shellAsset.panelRegistry.panels.size());
ASSERT_EQ(shellAsset.shellDefinition.statusSegments.size(), 1u);
EXPECT_EQ(shellAsset.shellDefinition.statusSegments.front().label, "Editor Shell");
EXPECT_EQ(
shellAsset.shellDefinition.workspacePresentations.front().panelId,
"editor-foundation-root");
EXPECT_EQ(
shellAsset.shellDefinition.workspacePresentations.front().kind,
shellAsset.panelRegistry.panels.front().presentationKind);
}
TEST(EditorShellAssetValidationTest, ValidationRejectsWorkspacePanelMissingFromRegistry) {
@@ -50,4 +61,56 @@ TEST(EditorShellAssetValidationTest, ValidationRejectsInvalidWorkspaceSessionSta
EXPECT_EQ(validation.code, EditorShellAssetValidationCode::InvalidWorkspaceSession);
}
TEST(EditorShellAssetValidationTest, ValidationRejectsShellPresentationMissingFromRegistry) {
auto shellAsset = BuildDefaultEditorShellAsset(".");
ASSERT_EQ(
shellAsset.shellDefinition.workspacePresentations.size(),
shellAsset.panelRegistry.panels.size());
shellAsset.shellDefinition.workspacePresentations.front().panelId =
"editor-foundation-root-renamed";
const auto validation = ValidateEditorShellAsset(shellAsset);
EXPECT_EQ(
validation.code,
EditorShellAssetValidationCode::MissingShellPresentationPanelDescriptor);
}
TEST(EditorShellAssetValidationTest, ValidationRejectsDuplicateShellPresentationPanelId) {
auto shellAsset = BuildDefaultEditorShellAsset(".");
ASSERT_EQ(
shellAsset.shellDefinition.workspacePresentations.size(),
shellAsset.panelRegistry.panels.size());
shellAsset.shellDefinition.workspacePresentations.push_back(
shellAsset.shellDefinition.workspacePresentations.front());
const auto validation = ValidateEditorShellAsset(shellAsset);
EXPECT_EQ(
validation.code,
EditorShellAssetValidationCode::DuplicateShellPresentationPanelId);
}
TEST(EditorShellAssetValidationTest, ValidationRejectsMissingRequiredShellPresentation) {
auto shellAsset = BuildDefaultEditorShellAsset(".");
shellAsset.shellDefinition.workspacePresentations.clear();
const auto validation = ValidateEditorShellAsset(shellAsset);
EXPECT_EQ(
validation.code,
EditorShellAssetValidationCode::MissingRequiredShellPresentation);
}
TEST(EditorShellAssetValidationTest, ValidationRejectsShellPresentationKindMismatch) {
auto shellAsset = BuildDefaultEditorShellAsset(".");
ASSERT_EQ(
shellAsset.shellDefinition.workspacePresentations.size(),
shellAsset.panelRegistry.panels.size());
shellAsset.shellDefinition.workspacePresentations.front().kind =
XCEngine::UI::Editor::UIEditorPanelPresentationKind::ViewportShell;
const auto validation = ValidateEditorShellAsset(shellAsset);
EXPECT_EQ(
validation.code,
EditorShellAssetValidationCode::ShellPresentationKindMismatch);
}
} // namespace

View File

@@ -1,7 +1,10 @@
#include <gtest/gtest.h>
#include "Core/EditorShellAsset.h"
#include "Application.h"
#include <XCEditor/Core/UIEditorShellInteraction.h>
#include <XCEditor/Core/UIEditorWorkspaceModel.h>
#include <XCEditor/Core/UIEditorWorkspaceSession.h>
#include <XCEngine/UI/Runtime/UIScreenDocumentHost.h>
#include <XCEngine/UI/Runtime/UIScreenPlayer.h>
@@ -16,6 +19,11 @@
namespace {
using XCEngine::UI::Editor::BuildDefaultEditorShellAsset;
using XCEngine::UI::Editor::BuildStructuredEditorShellBinding;
using XCEngine::UI::Editor::BuildStructuredEditorShellServices;
using XCEngine::UI::Editor::ResolveUIEditorShellInteractionModel;
using XCEngine::UI::Editor::AreUIEditorWorkspaceModelsEquivalent;
using XCEngine::UI::Editor::AreUIEditorWorkspaceSessionsEquivalent;
using XCEngine::UI::UIDrawCommand;
using XCEngine::UI::UIDrawCommandType;
using XCEngine::UI::UIDrawData;
@@ -56,23 +64,93 @@ bool ContainsPathWithFilename(
return false;
}
bool PanelRegistriesMatch(
const XCEngine::UI::Editor::UIEditorPanelRegistry& lhs,
const XCEngine::UI::Editor::UIEditorPanelRegistry& rhs) {
if (lhs.panels.size() != rhs.panels.size()) {
return false;
}
for (std::size_t index = 0; index < lhs.panels.size(); ++index) {
const auto& left = lhs.panels[index];
const auto& right = rhs.panels[index];
if (left.panelId != right.panelId ||
left.defaultTitle != right.defaultTitle ||
left.presentationKind != right.presentationKind ||
left.placeholder != right.placeholder ||
left.canHide != right.canHide ||
left.canClose != right.canClose) {
return false;
}
}
return true;
}
bool ShellDefinitionsMatch(
const XCEngine::UI::Editor::UIEditorShellInteractionDefinition& lhs,
const XCEngine::UI::Editor::UIEditorShellInteractionDefinition& rhs) {
if (lhs.menuModel.menus.size() != rhs.menuModel.menus.size() ||
lhs.statusSegments.size() != rhs.statusSegments.size() ||
lhs.workspacePresentations.size() != rhs.workspacePresentations.size()) {
return false;
}
for (std::size_t index = 0; index < lhs.menuModel.menus.size(); ++index) {
const auto& left = lhs.menuModel.menus[index];
const auto& right = rhs.menuModel.menus[index];
if (left.menuId != right.menuId ||
left.label != right.label ||
left.items.size() != right.items.size()) {
return false;
}
}
for (std::size_t index = 0; index < lhs.statusSegments.size(); ++index) {
const auto& left = lhs.statusSegments[index];
const auto& right = rhs.statusSegments[index];
if (left.segmentId != right.segmentId ||
left.label != right.label ||
left.slot != right.slot ||
left.tone != right.tone ||
left.interactive != right.interactive ||
left.showSeparator != right.showSeparator ||
left.desiredWidth != right.desiredWidth) {
return false;
}
}
for (std::size_t index = 0; index < lhs.workspacePresentations.size(); ++index) {
const auto& left = lhs.workspacePresentations[index];
const auto& right = rhs.workspacePresentations[index];
if (left.panelId != right.panelId ||
left.kind != right.kind ||
left.viewportShellModel.spec.chrome.title != right.viewportShellModel.spec.chrome.title ||
left.viewportShellModel.spec.chrome.subtitle != right.viewportShellModel.spec.chrome.subtitle ||
left.viewportShellModel.spec.chrome.showTopBar != right.viewportShellModel.spec.chrome.showTopBar ||
left.viewportShellModel.spec.chrome.showBottomBar != right.viewportShellModel.spec.chrome.showBottomBar ||
left.viewportShellModel.frame.statusText != right.viewportShellModel.frame.statusText) {
return false;
}
}
return true;
}
} // namespace
TEST(EditorUIStructuredShellTest, AuthoredEditorShellLoadsFromRepositoryResources) {
const auto shell = BuildDefaultEditorShellAsset(RepoRootPath());
const auto binding = BuildStructuredEditorShellBinding(shell);
ASSERT_TRUE(std::filesystem::exists(shell.documentPath));
ASSERT_TRUE(std::filesystem::exists(shell.themePath));
UIScreenAsset asset = {};
asset.screenId = shell.screenId;
asset.documentPath = shell.documentPath.string();
asset.themePath = shell.themePath.string();
ASSERT_TRUE(binding.IsValid()) << binding.assetValidation.message;
ASSERT_TRUE(std::filesystem::exists(std::filesystem::path(binding.screenAsset.documentPath)));
ASSERT_TRUE(std::filesystem::exists(std::filesystem::path(binding.screenAsset.themePath)));
UIDocumentScreenHost host = {};
UIScreenPlayer player(host);
ASSERT_TRUE(player.Load(asset)) << player.GetLastError();
ASSERT_TRUE(player.Load(binding.screenAsset)) << player.GetLastError();
ASSERT_NE(player.GetDocument(), nullptr);
EXPECT_TRUE(player.GetDocument()->hasThemeDocument);
EXPECT_TRUE(ContainsPathWithFilename(player.GetDocument()->dependencies, "editor_shell.xctheme"));
@@ -91,3 +169,49 @@ TEST(EditorUIStructuredShellTest, AuthoredEditorShellLoadsFromRepositoryResource
EXPECT_FALSE(DrawDataContainsText(frame.drawData, "Right Pane Host"));
EXPECT_FALSE(DrawDataContainsText(frame.drawData, "Bottom Pane Host"));
}
TEST(EditorUIStructuredShellTest, StructuredShellBindingUsesEditorShellAssetAsSingleSource) {
auto shell = BuildDefaultEditorShellAsset(RepoRootPath());
shell.shellDefinition.statusSegments.front().label = "Asset Contract";
XCEngine::UI::Editor::UIEditorMenuDescriptor assetMenu = {};
assetMenu.menuId = "asset";
assetMenu.label = "Asset";
shell.shellDefinition.menuModel.menus = { assetMenu };
const auto binding = BuildStructuredEditorShellBinding(shell);
ASSERT_TRUE(binding.IsValid()) << binding.assetValidation.message;
EXPECT_EQ(binding.screenAsset.screenId, shell.screenId);
EXPECT_EQ(std::filesystem::path(binding.screenAsset.documentPath), shell.documentPath);
EXPECT_EQ(std::filesystem::path(binding.screenAsset.themePath), shell.themePath);
EXPECT_TRUE(PanelRegistriesMatch(binding.workspaceController.GetPanelRegistry(), shell.panelRegistry));
EXPECT_TRUE(AreUIEditorWorkspaceModelsEquivalent(binding.workspaceController.GetWorkspace(), shell.workspace));
EXPECT_TRUE(AreUIEditorWorkspaceSessionsEquivalent(binding.workspaceController.GetSession(), shell.workspaceSession));
EXPECT_TRUE(ShellDefinitionsMatch(binding.shellDefinition, shell.shellDefinition));
EXPECT_TRUE(binding.workspaceController.ValidateState().IsValid());
EXPECT_TRUE(binding.shortcutManager.ValidateConfiguration().IsValid());
const auto services = BuildStructuredEditorShellServices(binding);
ASSERT_NE(services.commandDispatcher, nullptr);
EXPECT_EQ(services.shortcutManager, &binding.shortcutManager);
const auto model = ResolveUIEditorShellInteractionModel(
binding.workspaceController,
binding.shellDefinition,
services);
ASSERT_EQ(model.resolvedMenuModel.menus.size(), 1u);
EXPECT_EQ(model.resolvedMenuModel.menus.front().menuId, "asset");
EXPECT_EQ(model.resolvedMenuModel.menus.front().label, "Asset");
ASSERT_EQ(model.statusSegments.size(), shell.shellDefinition.statusSegments.size());
EXPECT_EQ(model.statusSegments.front().label, shell.shellDefinition.statusSegments.front().label);
ASSERT_EQ(
model.workspacePresentations.size(),
shell.shellDefinition.workspacePresentations.size());
EXPECT_EQ(
model.workspacePresentations.front().panelId,
shell.shellDefinition.workspacePresentations.front().panelId);
EXPECT_EQ(
model.workspacePresentations.front().kind,
shell.shellDefinition.workspacePresentations.front().kind);
}