#include "common/Runtime.h" #include "ui/Inspector.h" #include "ui/UIHost.h" #include "zeroerr/assert.h" #include "zeroerr/unittest.h" #include #include #include #include using namespace eve; using namespace eve::ui; namespace { const char* kInspectorScript = R"SQ( class InspectorBase { baseName = "base" baseLevel = 1 } class InspectorHero extends InspectorBase { hp = 100.0 name = "Hero" alive = true job = "warrior" function update(dt) { baseLevel += 1 } } )SQ"; UINode* nodeById(UIHost* host, const std::string& id) { if (host == nullptr) return nullptr; auto node = host->findById(id); return node ? &node->get() : nullptr; } UIHost* resolveHost(UIHostHandle handle) { auto host = UIHost::resolve(handle); return host ? &host->get() : nullptr; } } // namespace TEST_CASE("runtime.createInstanceReflectsAndWritesProperties") { Runtime runtime(512, ssq::Libs::ALL); runtime.initialize(); runtime.runSource(kInspectorScript, "inspector.nut"); const ReflectedClass* cls = runtime.reflectedClass("InspectorHero"); REQUIRE(cls != nullptr); CHECK_EQ(cls->base, std::string("InspectorBase")); // Member metadata + Squirrel attribute helpers. auto hp = std::find_if(cls->members.begin(), cls->members.end(), [](const ReflectedMember& m) { return m.name == "hp"; }); REQUIRE(hp != cls->members.end()); CHECK_EQ(hp->attrFloat("min"), 0.f); CHECK_EQ(hp->attrFloat("max"), 100.f); auto job = std::find_if(cls->members.begin(), cls->members.end(), [](const ReflectedMember& m) { return m.name == "job"; }); REQUIRE(job != cls->members.end()); CHECK(job->attrOptions("options") == std::vector({"warrior", "mage", "rogue"})); // Instance creation runs the default constructor and roots the object. ssq::Object hero = runtime.createInstance("InspectorHero"); CHECK(static_cast(hero.getType()) == static_cast(ssq::Type::INSTANCE)); CHECK_EQ(runtime.classNameOf(hero), std::string("InspectorHero")); CHECK_EQ(runtime.readProperty(hero, "name").asString(), std::string("Hero")); // reflectInstance merges own + inherited members with live values. const std::vector members = runtime.reflectInstance(hero); auto byName = [&](const std::string& name) -> const ReflectedMember* { auto it = std::find_if(members.begin(), members.end(), [&](const ReflectedMember& m) { return m.name == name; }); return it == members.end() ? nullptr : &*it; }; const ReflectedMember* baseLevel = byName("baseLevel"); REQUIRE(baseLevel != nullptr); CHECK(static_cast(baseLevel->value.kind) == static_cast(ReflectedValueKind::Integer)); CHECK_EQ(baseLevel->value.integer, int64_t(1)); const ReflectedMember* update = byName("update"); REQUIRE(update != nullptr); CHECK(update->method); // Typed writes preserve the script slot's type. ReflectedValue damage; damage.kind = ReflectedValueKind::Float; damage.floating = 4.5; CHECK(runtime.writeProperty(hero, "hp", damage)); CHECK_EQ(runtime.readProperty(hero, "hp").asFloat(), 4.5); ReflectedValue level; level.kind = ReflectedValueKind::Float; // float in, integer slot stays integer level.floating = 7.0; CHECK(runtime.writeProperty(hero, "baseLevel", level)); const ReflectedValue levelAfter = runtime.readProperty(hero, "baseLevel"); CHECK(static_cast(levelAfter.kind) == static_cast(ReflectedValueKind::Integer)); CHECK_EQ(levelAfter.integer, int64_t(7)); ReflectedValue name; name.kind = ReflectedValueKind::String; name.text = "Axe"; CHECK(runtime.writeProperty(hero, "name", name)); CHECK_EQ(runtime.readProperty(hero, "name").asString(), std::string("Axe")); ReflectedValue off; off.kind = ReflectedValueKind::Bool; CHECK(runtime.writeProperty(hero, "alive", off)); CHECK(!runtime.readProperty(hero, "alive").asBool()); // Methods and missing slots are not writable. ReflectedValue value; value.kind = ReflectedValueKind::Integer; value.integer = 1; CHECK(!runtime.writeProperty(hero, "update", value)); CHECK(!runtime.writeProperty(hero, "missing", value)); // Constructor side effects run on createInstance. runtime.runSource("class InspectorCtor { x = 0\n constructor() { x = 42 } }", "ctor.nut"); ssq::Object ctorInstance = runtime.createInstance("InspectorCtor"); CHECK_EQ(runtime.readProperty(ctorInstance, "x").asInt(), int64_t(42)); // Classes defined outside the Runtime API (dofile/compilestring path) are // picked up by scanClasses(); redefining a name refreshes the reflection. const auto runRaw = [&](const char* source) { auto scope = runtime.enter(); auto stack = runtime.guard(); HSQUIRRELVM vm = runtime.handle(); const SQInteger top = sq_gettop(vm); REQUIRE(SQ_SUCCEEDED(sq_compilebuffer(vm, source, static_cast(std::strlen(source)), "external.nut", SQTrue))); sq_pushroottable(vm); REQUIRE(SQ_SUCCEEDED(sq_call(vm, 1, SQFalse, SQTrue))); sq_settop(vm, top); }; runRaw("class ExternalScanned { speed = 2.5 }"); CHECK(runtime.reflectedClass("ExternalScanned") == nullptr); CHECK(runtime.scanClasses() >= 1); const ReflectedClass* external = runtime.reflectedClass("ExternalScanned"); REQUIRE(external != nullptr); REQUIRE(!external->members.empty()); CHECK_EQ(runtime.scanClasses(), size_t(0)); // unchanged classes are skipped runRaw("class ExternalScanned { speed = 9.0\n label = \"new\" }"); runtime.scanClasses(); const ReflectedClass* refreshed = runtime.reflectedClass("ExternalScanned"); REQUIRE(refreshed != nullptr); bool hasLabel = false; for (const ReflectedMember& member : refreshed->members) if (member.name == "label") hasLabel = true; CHECK(hasLabel); } TEST_CASE("inspector.buildsEditablePanelAndBindsTwoWay") { Runtime runtime(512, ssq::Libs::ALL); runtime.initialize(); runtime.runSource(kInspectorScript, "inspector.nut"); Inspector inspector; inspector.refresh(); CHECK(inspector.selectClass("InspectorHero")); inspector.open(); UIHost* host = resolveHost(inspector.host()); REQUIRE(host != nullptr); // Class + instance selectors exist. REQUIRE(nodeById(host, "class") != nullptr); REQUIRE(nodeById(host, "instance") != nullptr); REQUIRE(nodeById(host, "add_instance") != nullptr); // Own + inherited property widgets were generated. UINode* hp = nodeById(host, "prop_hp"); REQUIRE(hp != nullptr); CHECK(static_cast(hp->type) == static_cast(NodeType::Slider)); CHECK_EQ(hp->value, 100.f); UINode* name = nodeById(host, "prop_name"); REQUIRE(name != nullptr); CHECK(static_cast(name->type) == static_cast(NodeType::InputText)); CHECK_EQ(name->valueText, std::string("Hero")); UINode* alive = nodeById(host, "prop_alive"); REQUIRE(alive != nullptr); CHECK(static_cast(alive->type) == static_cast(NodeType::Checkbox)); CHECK(alive->checked); UINode* job = nodeById(host, "prop_job"); REQUIRE(job != nullptr); CHECK(static_cast(job->type) == static_cast(NodeType::Combo)); // Base-class properties are grouped under their own header. REQUIRE(nodeById(host, "prop_baseLevel") != nullptr); REQUIRE(nodeById(host, "cls_InspectorBase") != nullptr); // View → model: fire the slider handler, then verify the script instance. const ssq::Object selected = inspector.selectedInstance(); REQUIRE(static_cast(selected.getType()) == static_cast(ssq::Type::INSTANCE)); auto tree = host->tree(); REQUIRE_GE(hp->handlerValue, 1u); tree->valueHandlers[size_t(hp->handlerValue - 1)](42.f); CHECK_EQ(runtime.readProperty(selected, "hp").asFloat(), 42.0); // Model → view: external change is pulled into the tree by sync(). ReflectedValue value; value.kind = ReflectedValueKind::Float; value.floating = 55.0; const bool written = runtime.writeProperty(selected, "hp", value); CHECK(written); inspector.sync(); CHECK_EQ(nodeById(host, "prop_hp")->value, 55.f); // inspectObject() binds the panel to a caller-provided live object: edits // write straight back into that exact script instance. ssq::Object hero2 = runtime.createInstance("InspectorHero"); CHECK(inspector.inspectObject(hero2)); CHECK_EQ(inspector.selectedIndex(), 0); UINode* hp2 = nodeById(host, "prop_hp"); REQUIRE(hp2 != nullptr); auto tree2 = host->tree(); REQUIRE_GE(hp2->handlerValue, 1u); tree2->valueHandlers[size_t(hp2->handlerValue - 1)](88.f); CHECK_EQ(runtime.readProperty(hero2, "hp").asFloat(), 88.0); } TEST_CASE("runtime.arrayTableAndNestedInstanceEditing") { Runtime runtime(512, ssq::Libs::ALL); runtime.initialize(); runtime.runSource(R"SQ( class SkillData { name = "slash" } class NestedHolder { skills = ["slash", "bash"] buffs = { haste = 1.0 } pet = null constructor() { pet = SkillData() // Rebuild mutable defaults per instance (Squirrel shares class-field // table/array defaults between instances). skills = ["slash", "bash"] buffs = { haste = 1.0 } } } )SQ", "nested.nut"); ssq::Object holder = runtime.createInstance("NestedHolder"); // Array member editing. CHECK_EQ(runtime.arraySize(holder, "skills"), size_t(2)); CHECK_EQ(runtime.arrayGet(holder, "skills", 0).asString(), std::string("slash")); ReflectedValue element; element.kind = ReflectedValueKind::String; element.text = "kick"; CHECK(runtime.arraySet(holder, "skills", 0, element)); CHECK_EQ(runtime.arrayGet(holder, "skills", 0).asString(), std::string("kick")); ReflectedValue extra; extra.kind = ReflectedValueKind::String; extra.text = "jump"; CHECK(runtime.arrayAppend(holder, "skills", extra)); CHECK_EQ(runtime.arraySize(holder, "skills"), size_t(3)); CHECK(runtime.arrayRemove(holder, "skills", 1)); CHECK_EQ(runtime.arraySize(holder, "skills"), size_t(2)); CHECK_EQ(runtime.arrayGet(holder, "skills", 1).asString(), std::string("jump")); // Table member editing. CHECK_EQ(runtime.tableKeys(holder, "buffs"), std::vector({"haste"})); CHECK_EQ(runtime.tableGet(holder, "buffs", "haste").asFloat(), 1.0); ReflectedValue speed; speed.kind = ReflectedValueKind::Float; speed.floating = 2.0; CHECK(runtime.tableSet(holder, "buffs", "speed", speed)); CHECK_EQ(runtime.tableGet(holder, "buffs", "speed").asFloat(), 2.0); CHECK(runtime.tableRemove(holder, "buffs", "haste")); CHECK_EQ(runtime.tableKeys(holder, "buffs"), std::vector({"speed"})); // Nested instance property. const ssq::Object pet = runtime.readObjectProperty(holder, "pet"); CHECK(static_cast(pet.getType()) == static_cast(ssq::Type::INSTANCE)); CHECK_EQ(runtime.classNameOf(pet), std::string("SkillData")); CHECK_EQ(runtime.readProperty(pet, "name").asString(), std::string("slash")); // Inspector: expanded array + table + nested navigation. Inspector inspector; inspector.refresh(); CHECK(inspector.selectClass("NestedHolder")); inspector.open(); UIHost* host = resolveHost(inspector.host()); REQUIRE(host != nullptr); UINode* skill0 = nodeById(host, "arr_NestedHolder_skills_0"); REQUIRE(skill0 != nullptr); auto tree = host->tree(); REQUIRE_GE(skill0->handlerText, 1u); tree->textHandlers[size_t(skill0->handlerText - 1)]("spin"); const ssq::Object selected = inspector.selectedInstance(); CHECK_EQ(runtime.arrayGet(selected, "skills", 0).asString(), std::string("spin")); UINode* buffHaste = nodeById(host, "tbl_NestedHolder_buffs_haste"); REQUIRE(buffHaste != nullptr); REQUIRE_GE(buffHaste->handlerText, 1u); tree->textHandlers[size_t(buffHaste->handlerText - 1)]("1.5"); CHECK_EQ(runtime.tableGet(selected, "buffs", "haste").asFloat(), 1.5); // Nested navigation: open pet -> edit its property -> back. UINode* openPet = nodeById(host, "open_NestedHolder_pet"); REQUIRE(openPet != nullptr); REQUIRE_GE(openPet->handlerClick, 1u); tree->clickHandlers[size_t(openPet->handlerClick - 1)](); UINode* petName = nodeById(host, "prop_name"); REQUIRE(petName != nullptr); auto tree2 = host->tree(); REQUIRE_GE(petName->handlerText, 1u); tree2->textHandlers[size_t(petName->handlerText - 1)]("cleave"); CHECK_EQ(runtime.readProperty( inspector.selectedInstance(), "name").asString(), std::string("cleave")); UINode* back = nodeById(host, "inspector_back"); REQUIRE(back != nullptr); REQUIRE_GE(back->handlerClick, 1u); tree2->clickHandlers[size_t(back->handlerClick - 1)](); REQUIRE(nodeById(host, "arr_NestedHolder_skills_0") != nullptr); CHECK_EQ(runtime.classNameOf(inspector.selectedInstance()), std::string("NestedHolder")); } TEST_CASE("inspector.pickSceneInspectsPickedObject") { Runtime runtime(512, ssq::Libs::ALL); runtime.initialize(); runtime.runSource("class PickedHero { name = \"picked\" }", "pick.nut"); const ssq::Object picked = runtime.createInstance("PickedHero"); Inspector inspector; inspector.refresh(); inspector.setPickScene([picked]() { return picked; }); inspector.open(); UIHost* host = resolveHost(inspector.host()); REQUIRE(host != nullptr); UINode *pick = nodeById(host, "inspector_pick"); REQUIRE(pick != nullptr); auto tree = host->tree(); REQUIRE_GE(pick->handlerClick, 1u); tree->clickHandlers[size_t(pick->handlerClick - 1)](); CHECK_EQ(inspector.selectedClass(), std::string("PickedHero")); CHECK_EQ(inspector.instanceCount(), 1); CHECK_EQ(runtime.readProperty(inspector.selectedInstance(), "name").asString(), std::string("picked")); }