#include "zeroerr/assert.h" #include "zeroerr/unittest.h" #include "editor/EditCommand.h" #include "editor/EditorCommandService.h" #include "editor/EditorHostProfile.h" #include "editor/EditorIds.h" #include "editor/EditorSession.h" #include "editor/EditorValue.h" #include "editor/FieldTargets.h" #include "editor/TileBuffer.h" #include #include #include #include #include using namespace eve::editor; static_assert(!std::is_convertible_v); static_assert(!std::is_convertible_v); TEST_CASE("editor.v2.strong_ids_and_values") { const CommandId command("scene.object.create"); const CommandId same("scene.object.create"); const CommandId other("scene.object.delete"); CHECK(command == same); CHECK(command != other); CHECK_EQ(command.value(), std::string("scene.object.create")); EditorValue::Object root; root["name"] = "Station"; root["position"] = EditorValue::Array{1.0, 2.0, 3.0}; root["visible"] = true; const EditorValue value(std::move(root)); CHECK_EQ(static_cast(value.type()), static_cast(EditorValue::Type::Object)); CHECK(value.withinLimits(4, 16, 128)); CHECK(!value.withinLimits(1, 16, 128)); CHECK(!value.withinLimits(4, 2, 128)); CHECK(!value.withinLimits(4, 16, 4)); } TEST_CASE("editor.v2.host_profile_is_an_execution_boundary") { const CommandId place("scene.asset.place"); const CommandId importAsset("asset.import"); HostProfile runtime = HostProfile::runtimeBuilder(); CHECK(!runtime.allowsCommand(place)); runtime.allowCommand(place); CHECK(runtime.allowsCommand(place)); CHECK(!runtime.allowsCommand(importAsset)); CHECK(runtime.hasFeatures(HostFeature::RuntimeWorld)); CHECK(!runtime.hasFeatures(HostFeature::SourceAssets)); HostProfile developer = HostProfile::developer(); CHECK(developer.allowsCommand(importAsset)); CHECK(developer.hasFeatures(HostFeature::SourceAssets | HostFeature::BuildCook)); } TEST_CASE("editor.v2.command_registry_filters_and_executes") { EditorCommandService service; CommandDescriptor descriptor; descriptor.id = CommandId("scene.rename"); descriptor.ownerModule = "scene"; descriptor.displayName = "Rename"; descriptor.requiredFeatures = HostFeature::RuntimeWorld; int calls = 0; auto registered = service.registerCommand(descriptor, [&](const CommandContext&, const EditorValue& payload) { ++calls; return EditorResult::applied(payload); }); CHECK(registered.accepted()); CHECK_EQ(service.revision(), static_cast(1)); auto duplicate = service.registerCommand(descriptor, [](const CommandContext&, const EditorValue&) { return EditorResult::applied(EditorValue{}); }); CHECK_EQ(static_cast(duplicate.status), static_cast(EditorStatus::Rejected)); HostProfile runtime = HostProfile::runtimeBuilder(); CommandContext context; context.profile = &runtime; auto denied = service.execute(descriptor.id, context, EditorValue("New Name")); CHECK_EQ(static_cast(denied.status), static_cast(EditorStatus::Rejected)); CHECK_EQ(calls, 0); runtime.allowCommand(descriptor.id); auto executed = service.execute(descriptor.id, context, EditorValue("New Name")); CHECK(executed.accepted()); CHECK_EQ(calls, 1); CHECK(executed.value.has_value()); CHECK_EQ(*executed.value->getIf(), std::string("New Name")); CHECK_EQ(service.commands(runtime).size(), static_cast(1)); CHECK_EQ(service.unregisterOwner("scene"), static_cast(1)); CHECK(service.find(descriptor.id) == nullptr); } TEST_CASE("editor.v2.command_registry_gates_automation_and_exceptions") { EditorCommandService service; CommandDescriptor descriptor; descriptor.id = CommandId("scene.dangerous"); descriptor.ownerModule = "scene"; descriptor.automationAllowed = false; CHECK(service .registerCommand(descriptor, [](const CommandContext&, const EditorValue&) { throw std::runtime_error("boom"); return EditorResult::applied(EditorValue{}); }) .accepted()); HostProfile developer = HostProfile::developer(); CommandContext context; context.profile = &developer; context.source = CommandSource::Automation; auto automation = service.execute(descriptor.id, context, {}); CHECK_EQ(static_cast(automation.status), static_cast(EditorStatus::Rejected)); context.source = CommandSource::Api; auto failure = service.execute(descriptor.id, context, {}); CHECK_EQ(static_cast(failure.status), static_cast(EditorStatus::Failed)); CHECK_EQ(failure.diagnostics.size(), static_cast(1)); CHECK(failure.diagnostics.front().message.find("boom") != std::string::npos); } TEST_CASE("editor.v2.session_wraps_command_in_legacy_transaction") { TileBuffer buffer(2, 2); TileBufferTarget target("tiles", &buffer); EditorCommandService service; CommandDescriptor descriptor; descriptor.id = CommandId("map.paint-one"); descriptor.ownerModule = "map"; descriptor.displayName = "Paint One Tile"; descriptor.requiredFeatures = HostFeature::RuntimeWorld; CHECK(service .registerCommand( descriptor, [](const CommandContext& context, const EditorValue& payload) { auto* after = payload.getIf(); if (!context.session || !after) return EditorResult::error(EditorStatus::Rejected, RuleId("map.paint.invalid"), "Paint command requires an integer value"); auto command = std::make_unique("Paint One Tile", context.session->target()); if (!command->record(0, 0, static_cast(*after)) || !context.session->execute(std::move(command))) return EditorResult::error(EditorStatus::Failed, RuleId("map.paint.failed"), "Tile edit could not be applied"); return EditorResult::applied(payload); }) .accepted()); EditorSession session; HostProfile runtime = HostProfile::runtimeBuilder(); runtime.allowCommand(descriptor.id); session.setHostProfile(std::move(runtime)); session.setCommandService(&service); session.bindTarget(&target); auto result = session.executeCommand(descriptor.id, EditorValue(7)); CHECK(result.accepted()); CHECK_EQ(buffer.getGid(0, 0), 7); CHECK(session.transactions().canUndo()); CHECK(session.transactions().undo()); CHECK_EQ(buffer.getGid(0, 0), 0); session.setHostProfile(HostProfile::runtimeBuilder()); auto denied = session.executeCommand(descriptor.id, EditorValue(9)); CHECK_EQ(static_cast(denied.status), static_cast(EditorStatus::Rejected)); CHECK_EQ(buffer.getGid(0, 0), 0); }