#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(preview.status), static_cast(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(previewService.anchorGizmo(document, preview, ObjectId("play")).code()), static_cast(EditorStatus::Conflict)); } namespace { EditorResult 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(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(appended.code(), RuleId("test.ui.append"), "Could not append UI operation"); } return transactions.commit(); } SelectionSnapshot uiSelection(const UiDocumentTarget& target, std::initializer_list 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(1)); CHECK_EQ(static_cast(target.makeDelete(ObjectId("panel")).code()), static_cast(EditorStatus::Rejected)); CHECK_EQ(static_cast(target.makeReparent(ObjectId("root"), ObjectId("label")).code()), static_cast(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(target.widget(ObjectId("label")).code()), static_cast(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(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(target.read(selection, PropertyPath("layout.position")).state), static_cast(PropertyReadState::Mixed)); CHECK_EQ(static_cast(target.makeSet(selection, PropertyPath("layout.anchor"), EditorValue::Array{1.5, 0.0}, PropertySetMode::Absolute).code()), static_cast(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(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(restored.loadSnapshot(EditorValue(std::move(invalidRoot))).code()), static_cast(EditorStatus::Rejected)); CHECK(restored.widget(ObjectId("child")).ok()); }