Files
EVEngine/test/editor_property_model.cpp

199 lines
7.9 KiB
C++

#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include "editor/EditorPropertyModel.h"
#include <cstdint>
#include <string>
#include <utility>
#include <vector>
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<eve::Revision> currentRevision(const SelectionSnapshot &) const override {
return eve::Result<eve::Revision>::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<DomainOperation> makeSet(const SelectionSnapshot &, const PropertyPath &,
const EditorValue &, PropertySetMode) const override {
return eve::editing::applied<DomainOperation>({});
}
EditorResult<DomainOperation> makeReset(const SelectionSnapshot &,
const PropertyPath &) const override {
return eve::editing::applied<DomainOperation>({});
}
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<std::size_t>(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<EditorValue::Object>();
REQUIRE(payload != nullptr);
CHECK_EQ(*payload->at("path").getIf<std::string>(), std::string("movement.speed"));
provider.speed = *payload->at("value").getIf<double>();
return eve::editing::applied<void>();
});
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<double>(), 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<double>(), 4.0);
provider.speed = 8.0;
CHECK_EQ(*model.read("movement.speed")->getIf<double>(), 4.0);
REQUIRE(model.refresh().ok());
CHECK_EQ(*model.read("movement.speed")->getIf<double>(), 8.0);
}
TEST_CASE("editor.property_model_notifies_value_availability_transitions") {
PropertyProvider provider;
SelectionSnapshot selection;
EditorPropertyModel model(schema(), selection, &provider);
std::vector<eve::property_access::PropertyChangeState> 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<std::size_t>(3));
CHECK(static_cast<int>(states[0]) == static_cast<int>(eve::property_access::PropertyChangeState::Mixed));
CHECK(static_cast<int>(states[1]) == static_cast<int>(eve::property_access::PropertyChangeState::Missing));
CHECK(static_cast<int>(states[2]) == static_cast<int>(eve::property_access::PropertyChangeState::Value));
CHECK_EQ(*model.read("movement.speed")->getIf<double>(), 12.0);
}
TEST_CASE("editor.property_model_queues_reentrant_snapshot_notifications") {
PropertyProvider provider;
SelectionSnapshot selection;
EditorPropertyModel model(schema(), selection, &provider);
std::vector<std::pair<std::string, std::uint64_t>> 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>(), 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<std::size_t>(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<std::size_t>(0));
}