#include "zeroerr/assert.h" #include "zeroerr/unittest.h" #include "editor/EditorPropertyModel.h" #include #include #include #include using namespace eve::editor; namespace { PropertySchema schema() { PropertySchema result; result.typeId = "test.actor"; PropertyDescriptor speed; speed.path = PropertyPath("movement.speed"); speed.displayNameKey = "Speed"; speed.type = PropertyType::Float; speed.flags = PropertyFlag::Runtime; speed.numeric.minimum = 0.0; speed.numeric.maximum = 20.0; result.properties.push_back(speed); PropertyDescriptor debug; debug.path = PropertyPath("debug.name"); debug.displayNameKey = "Debug name"; debug.type = PropertyType::String; debug.flags = PropertyFlag::EditorOnly | PropertyFlag::ReadOnly; result.properties.push_back(debug); return result; } class PropertyProvider final : public IPropertyProvider { public: [[nodiscard]] eve::Result currentRevision(const SelectionSnapshot &) const override { return eve::Result::success(eve::Revision{}); } PropertySchema schema(const SelectionSnapshot &) const override { return ::schema(); } PropertyReadResult read(const SelectionSnapshot &, const PropertyPath &path) const override { ++readCount; if (path.value() == "movement.speed") { if (speedState != PropertyReadState::Value) return {speedState, {}, {}}; return {PropertyReadState::Value, EditorValue(speed), {}}; } if (path.value() == "debug.name") return {PropertyReadState::Value, EditorValue(debugName), {}}; return {}; } EditorResult makeSet(const SelectionSnapshot &, const PropertyPath &, const EditorValue &, PropertySetMode) const override { return eve::editing::applied({}); } EditorResult makeReset(const SelectionSnapshot &, const PropertyPath &) const override { return eve::editing::applied({}); } double speed = 4.0; std::string debugName = "Actor #1"; PropertyReadState speedState = PropertyReadState::Value; mutable std::size_t readCount = 0; }; } // namespace TEST_CASE("editor.property_model_routes_runtime_writes_through_command_intent") { PropertyProvider provider; SelectionSnapshot selection; HostProfile runtime = HostProfile::runtimeBuilder(); EditorPropertyModel model(schema(), selection, &provider, PropertyModelSurface::Runtime, std::move(runtime)); REQUIRE_EQ(model.schema().properties.size(), static_cast(1)); CHECK_EQ(model.schema().properties.front().path, std::string("movement.speed")); CHECK(model.read("debug.name") == std::nullopt); bool sinkCalled = false; model.setEditSink([&](const PropertyEditIntent &intent) { sinkCalled = true; CHECK_EQ(intent.command.value(), std::string("editor.property.set")); const auto *payload = intent.payload.getIf(); REQUIRE(payload != nullptr); CHECK_EQ(*payload->at("path").getIf(), std::string("movement.speed")); provider.speed = *payload->at("value").getIf(); return eve::editing::applied(); }); std::string changedPath; auto subscription = model.subscribe([&](const eve::property_access::PropertyChange &change) { changedPath = change.path; }); CHECK(model.write("movement.speed", eve::Value(9.5)).accepted); CHECK(sinkCalled); CHECK_EQ(provider.speed, 9.5); CHECK_EQ(changedPath, std::string("movement.speed")); REQUIRE(model.read("movement.speed").has_value()); CHECK_EQ(*model.read("movement.speed")->getIf(), 9.5); } TEST_CASE("editor.property_model_value_conversion_preserves_nested_data") { EditorValue::Object source; source["enabled"] = true; source["values"] = EditorValue::Array{EditorValue(3), EditorValue("four")}; const EditorValue original(std::move(source)); CHECK(toEditorValue(toPresentationValue(original)) == original); } TEST_CASE("editor.property_model_reads_one_atomically_refreshed_snapshot") { PropertyProvider provider; SelectionSnapshot selection; EditorPropertyModel model(schema(), selection, &provider); REQUIRE(model.read("movement.speed").has_value()); CHECK_EQ(*model.read("movement.speed")->getIf(), 4.0); provider.speed = 8.0; CHECK_EQ(*model.read("movement.speed")->getIf(), 4.0); REQUIRE(model.refresh().ok()); CHECK_EQ(*model.read("movement.speed")->getIf(), 8.0); } TEST_CASE("editor.property_model_notifies_value_availability_transitions") { PropertyProvider provider; SelectionSnapshot selection; EditorPropertyModel model(schema(), selection, &provider); std::vector states; auto subscription = model.subscribe([&](const eve::property_access::PropertyChange &change) { if (change.path == "movement.speed") states.push_back(change.state); }); provider.speedState = PropertyReadState::Mixed; REQUIRE(model.refresh().ok()); CHECK(model.read("movement.speed") == std::nullopt); provider.speedState = PropertyReadState::Missing; REQUIRE(model.refresh().ok()); CHECK(model.read("movement.speed") == std::nullopt); provider.speedState = PropertyReadState::Value; provider.speed = 12.0; REQUIRE(model.refresh().ok()); REQUIRE_EQ(states.size(), static_cast(3)); CHECK(static_cast(states[0]) == static_cast(eve::property_access::PropertyChangeState::Mixed)); CHECK(static_cast(states[1]) == static_cast(eve::property_access::PropertyChangeState::Missing)); CHECK(static_cast(states[2]) == static_cast(eve::property_access::PropertyChangeState::Value)); CHECK_EQ(*model.read("movement.speed")->getIf(), 12.0); } TEST_CASE("editor.property_model_queues_reentrant_snapshot_notifications") { PropertyProvider provider; SelectionSnapshot selection; EditorPropertyModel model(schema(), selection, &provider); std::vector> notifications; bool reentered = false; auto subscription = model.subscribe([&](const eve::property_access::PropertyChange &change) { notifications.emplace_back(change.path, change.revision); if (change.path == "movement.speed" && !reentered) { reentered = true; REQUIRE(model.read("debug.name").has_value()); CHECK_EQ(*model.read("debug.name")->getIf(), std::string("Actor #2")); provider.speed = 6.0; provider.debugName = "Actor #3"; REQUIRE(model.refresh().ok()); } }); provider.speed = 5.0; provider.debugName = "Actor #2"; REQUIRE(model.refresh().ok()); REQUIRE_EQ(notifications.size(), static_cast(4)); CHECK_EQ(notifications[0].first, std::string("debug.name")); CHECK_EQ(notifications[1].first, std::string("movement.speed")); CHECK_EQ(notifications[2].first, std::string("debug.name")); CHECK_EQ(notifications[3].first, std::string("movement.speed")); CHECK_EQ(notifications[0].second, notifications[1].second); CHECK(notifications[1].second < notifications[2].second); CHECK_EQ(notifications[2].second, notifications[3].second); } TEST_CASE("editor.property_model_runtime_surface_requires_runtime_world_feature") { PropertyProvider provider; SelectionSnapshot selection; HostProfile denied(HostKind::RuntimeBuilder); EditorPropertyModel model(schema(), selection, &provider, PropertyModelSurface::Runtime, std::move(denied)); CHECK(model.schema().properties.empty()); CHECK(model.read("movement.speed") == std::nullopt); CHECK_EQ(provider.readCount, static_cast(0)); }