Files
EVEngine/test/editor_ui_document.cpp
西风逍遥游andCursor 24502babef Nest satellite packages under their host modules.
Keep CMake module names and trim switches, but stop flattening editing/editor and runtime bridges as top-level src/modules directories. Parent scans skip nested facets so runtime-3d can drop authoring by directory leaf.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-06 17:56:12 +08:00

186 lines
9.5 KiB
C++

#include "editor/EditorAuthority.h"
#include "editor/EditorTransactionService.h"
#include "ui/editing/UiDocument.h"
#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
using namespace eve::ui_editing;
using namespace eve::editing;
using eve::editor::LocalTransactionBackend;
TEST_CASE("editor.ui.style_inspector_preview_pick_and_anchor_gizmo_are_revision_safe") {
UiDocumentTarget document("hud");
UiLayoutValue rootLayout; rootLayout.width = 800.0; rootLayout.height = 600.0;
auto root = document.makeCreate({ObjectId("root"), ObjectId(), "window", "Root", rootLayout});
REQUIRE(root.ok()); CHECK(document.applyDomainOperation(root.value()).ok());
UiLayoutValue childLayout; childLayout.x = 20.0; childLayout.y = 30.0;
childLayout.width = 100.0; childLayout.height = 40.0;
auto child = document.makeCreate({ObjectId("play"), ObjectId("root"), "button", "Play", childLayout});
REQUIRE(child.ok()); CHECK(document.applyDomainOperation(child.value()).ok());
UiStyleValue style; style.paddingLeft = 10.0; style.paddingTop = 5.0;
style.tintR = 0.25; style.cornerRadius = 6.0; style.direction = "column";
auto styled = document.makeSetStyle(ObjectId("play"), style);
REQUIRE(styled.ok()); CHECK(document.applyDomainOperation(styled.value()).ok());
UiDocumentPreviewService previewService;
const auto preview = previewService.build(document, 800.0, 600.0);
CHECK_EQ(static_cast<int>(preview.status), static_cast<int>(EditorStatus::Applied));
auto picked = previewService.pick(preview, 25.0, 35.0);
REQUIRE(picked.ok()); CHECK_EQ(picked.value().value(), "play");
auto gizmo = previewService.anchorGizmo(document, preview, ObjectId("play"));
CHECK(gizmo.ok());
auto rename = document.makeRename(ObjectId("play"), "Play now"); REQUIRE(rename.ok());
CHECK(document.applyDomainOperation(rename.value()).ok());
CHECK_EQ(static_cast<int>(previewService.anchorGizmo(document, preview, ObjectId("play")).code()),
static_cast<int>(EditorStatus::Conflict));
}
namespace {
EditorResult<TransactionReceipt> commitUi(UiDocumentTarget& target,
LocalTransactionBackend& transactions,
const DomainOperation& operation,
const std::string& id) {
TransactionSpec specification;
specification.id = TransactionId(id);
specification.label = "Edit UI";
specification.target = TargetId(target.targetId());
specification.baseRevision = target.revision();
auto begun = transactions.begin(std::move(specification));
if (!begun.ok())
return eve::editing::failed<TransactionReceipt>(begun.code(), RuleId("test.ui.begin"),
"Could not begin UI transaction");
auto appended = transactions.append(operation);
if (!appended.ok()) {
[[maybe_unused]] const auto rolledBack = transactions.rollback();
return eve::editing::failed<TransactionReceipt>(appended.code(), RuleId("test.ui.append"),
"Could not append UI operation");
}
return transactions.commit();
}
SelectionSnapshot uiSelection(const UiDocumentTarget& target,
std::initializer_list<const char*> ids) {
SelectionSnapshot selection;
selection.channel = "ui";
for (const char* id : ids) {
SelectionItem item;
item.domain = SelectionDomain::UI;
item.target = TargetId(target.targetId());
item.item = StableId(id);
item.type = "ui.widget";
selection.items.push_back(item);
}
if (!selection.items.empty()) selection.primary = selection.items.front();
return selection;
}
} // namespace
TEST_CASE("editor.ui.document_hierarchy_operations_are_reversible_and_cycle_safe") {
UiDocumentTarget target("hud");
LocalWorldAuthority authority(&target);
LocalTransactionBackend transactions(&authority);
for (const CreateUiWidgetRequest& request : {
CreateUiWidgetRequest{ObjectId("root"), {}, "column", "Root", {0, 0, 1280, 720}},
CreateUiWidgetRequest{ObjectId("panel"), ObjectId("root"), "panel", "Panel", {10, 20, 300, 200}},
CreateUiWidgetRequest{ObjectId("label"), ObjectId("panel"), "text", "Label", {4, 4, 100, 24}}}) {
auto operation = target.makeCreate(request);
REQUIRE(operation.ok());
REQUIRE(commitUi(target, transactions, operation.value(), "ui.create." + request.id.value()).ok());
}
CHECK_EQ(target.children(ObjectId("panel")).size(), static_cast<std::size_t>(1));
CHECK_EQ(static_cast<int>(target.makeDelete(ObjectId("panel")).code()),
static_cast<int>(EditorStatus::Rejected));
CHECK_EQ(static_cast<int>(target.makeReparent(ObjectId("root"), ObjectId("label")).code()),
static_cast<int>(EditorStatus::Rejected));
auto rename = target.makeRename(ObjectId("label"), "Title");
REQUIRE(rename.ok());
REQUIRE(commitUi(target, transactions, rename.value(), "ui.rename").ok());
CHECK_EQ(target.widget(ObjectId("label")).value().name, std::string("Title"));
REQUIRE(transactions.undo().ok());
CHECK_EQ(target.widget(ObjectId("label")).value().name, std::string("Label"));
REQUIRE(transactions.redo().ok());
auto reparent = target.makeReparent(ObjectId("label"), ObjectId("root"));
REQUIRE(reparent.ok());
REQUIRE(commitUi(target, transactions, reparent.value(), "ui.reparent").ok());
CHECK(target.widget(ObjectId("label")).value().parent == ObjectId("root"));
auto remove = target.makeDelete(ObjectId("label"));
REQUIRE(remove.ok());
REQUIRE(commitUi(target, transactions, remove.value(), "ui.delete").ok());
CHECK_EQ(static_cast<int>(target.widget(ObjectId("label")).code()),
static_cast<int>(EditorStatus::NotFound));
REQUIRE(transactions.undo().ok());
CHECK_EQ(target.widget(ObjectId("label")).value().name, std::string("Title"));
}
TEST_CASE("editor.ui.multi_selection_layout_inspector_is_atomic_and_reports_mixed_values") {
UiDocumentTarget target("hud");
LocalWorldAuthority authority(&target);
LocalTransactionBackend transactions(&authority);
for (const char* id : {"left", "right"}) {
CreateUiWidgetRequest request{ObjectId(id), {}, "button", id, {}};
auto operation = target.makeCreate(request);
REQUIRE(operation.ok());
REQUIRE(commitUi(target, transactions, operation.value(), std::string("ui.create.") + id).ok());
}
const SelectionSnapshot selection = uiSelection(target, {"left", "right"});
CHECK_EQ(target.schema(selection).properties.size(), static_cast<std::size_t>(25));
auto size = target.makeSet(selection, PropertyPath("layout.size"),
EditorValue::Array{180.0, 42.0}, PropertySetMode::Absolute);
REQUIRE(size.ok());
REQUIRE(commitUi(target, transactions, size.value(), "ui.multi-size").ok());
CHECK_EQ(target.widget(ObjectId("left")).value().layout.width, 180.0);
CHECK_EQ(target.widget(ObjectId("right")).value().layout.height, 42.0);
REQUIRE(transactions.undo().ok());
CHECK_EQ(target.widget(ObjectId("left")).value().layout.width, 0.0);
UiLayoutValue moved;
moved.x = 15.0;
auto move = target.makeSetLayout(ObjectId("left"), moved);
REQUIRE(move.ok());
REQUIRE(commitUi(target, transactions, move.value(), "ui.move-one").ok());
CHECK_EQ(static_cast<int>(target.read(selection, PropertyPath("layout.position")).state),
static_cast<int>(PropertyReadState::Mixed));
CHECK_EQ(static_cast<int>(target.makeSet(selection, PropertyPath("layout.anchor"),
EditorValue::Array{1.5, 0.0},
PropertySetMode::Absolute).code()),
static_cast<int>(EditorStatus::Rejected));
}
TEST_CASE("editor.ui.snapshot_load_is_atomic_and_rejects_invalid_hierarchies") {
UiDocumentTarget source("source");
auto root = source.makeCreate({ObjectId("root"), {}, "column", "Root", {0, 0, 640, 480}});
REQUIRE(root.ok());
REQUIRE(source.applyDomainOperation(root.value()).ok());
auto child = source.makeCreate({ObjectId("child"), ObjectId("root"), "text", "Child", {8, 8, 100, 20}});
REQUIRE(child.ok());
REQUIRE(source.applyDomainOperation(child.value()).ok());
UiDocumentTarget restored("restored");
REQUIRE(restored.loadSnapshot(source.snapshotValue()).ok());
CHECK_EQ(restored.children(ObjectId("root")).size(), static_cast<std::size_t>(1));
EditorValue::Object invalidWidget;
invalidWidget["id"] = "orphan";
invalidWidget["parent"] = "missing";
invalidWidget["type"] = "text";
invalidWidget["name"] = "Orphan";
invalidWidget["text"] = "";
invalidWidget["visible"] = true;
invalidWidget["enabled"] = true;
invalidWidget["layout"] = EditorValue::Object{{"x", 0.0}, {"y", 0.0}, {"width", 10.0},
{"height", 10.0}, {"anchorX", 0.0},
{"anchorY", 0.0}, {"pivotX", 0.0},
{"pivotY", 0.0}};
EditorValue::Object invalidRoot;
invalidRoot["schemaVersion"] = 1;
invalidRoot["widgets"] = EditorValue::Array{EditorValue(std::move(invalidWidget))};
CHECK_EQ(static_cast<int>(restored.loadSnapshot(EditorValue(std::move(invalidRoot))).code()),
static_cast<int>(EditorStatus::Rejected));
CHECK(restored.widget(ObjectId("child")).ok());
}