mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-07-28 13:22:06 +00:00
feat(automation): tree.dump / tree.find / widget.get handlers
This commit is contained in:
@@ -18,6 +18,42 @@ nlohmann::json JsonRpcDispatcher::make_error(const nlohmann::json& id, int code,
|
|||||||
{"error", { {"code", code}, {"message", msg} }} };
|
{"error", { {"code", code}, {"message", msg} }} };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
std::optional<std::string> opt_str(const nlohmann::json& p, const char* key) {
|
||||||
|
if (p.is_object() && p.contains(key) && p.at(key).is_string())
|
||||||
|
return p.at(key).get<std::string>();
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
Target parse_target(const nlohmann::json& tj) {
|
||||||
|
Target t;
|
||||||
|
if (!tj.is_object()) return t;
|
||||||
|
t.id = opt_str(tj, "id");
|
||||||
|
t.path = opt_str(tj, "path");
|
||||||
|
t.name = opt_str(tj, "name");
|
||||||
|
t.klass = opt_str(tj, "class");
|
||||||
|
t.label = opt_str(tj, "label");
|
||||||
|
t.value = opt_str(tj, "value");
|
||||||
|
if (auto b = opt_str(tj, "backend"))
|
||||||
|
t.backend = (*b == "imgui") ? BackendKind::ImGui : BackendKind::Wx;
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
DumpOptions parse_dump_options(const nlohmann::json& p) {
|
||||||
|
DumpOptions o;
|
||||||
|
if (p.is_object()) {
|
||||||
|
if (p.contains("root")) o.root = opt_str(p, "root");
|
||||||
|
if (p.contains("max_depth") && p.at("max_depth").is_number_integer())
|
||||||
|
o.max_depth = p.at("max_depth").get<int>();
|
||||||
|
if (p.contains("visible_only") && p.at("visible_only").is_boolean())
|
||||||
|
o.visible_only = p.at("visible_only").get<bool>();
|
||||||
|
if (p.contains("include_imgui") && p.at("include_imgui").is_boolean())
|
||||||
|
o.include_imgui = p.at("include_imgui").get<bool>();
|
||||||
|
}
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
nlohmann::json JsonRpcDispatcher::m_version(const nlohmann::json&) {
|
nlohmann::json JsonRpcDispatcher::m_version(const nlohmann::json&) {
|
||||||
return { {"version", kAutomationVersion},
|
return { {"version", kAutomationVersion},
|
||||||
{"protocol", "2.0"},
|
{"protocol", "2.0"},
|
||||||
@@ -68,10 +104,36 @@ std::string JsonRpcDispatcher::handle_request(const std::string& body) {
|
|||||||
return dispatch(req).dump();
|
return dispatch(req).dump();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- method handlers implemented in Tasks 7-10 (stubs throw for now) ---
|
// --- method handlers implemented in Tasks 7-10 (remaining stubs throw for now) ---
|
||||||
nlohmann::json JsonRpcDispatcher::m_tree_dump(const nlohmann::json&) { throw AutomationError(kMethodNotFound, "not implemented"); }
|
nlohmann::json JsonRpcDispatcher::m_tree_dump(const nlohmann::json& params) {
|
||||||
nlohmann::json JsonRpcDispatcher::m_tree_find(const nlohmann::json&) { throw AutomationError(kMethodNotFound, "not implemented"); }
|
m_backend.refresh_ui();
|
||||||
nlohmann::json JsonRpcDispatcher::m_widget_get(const nlohmann::json&) { throw AutomationError(kMethodNotFound, "not implemented"); }
|
const UiNode root = m_backend.dump_tree(parse_dump_options(params));
|
||||||
|
return node_to_json(root, /*include_children*/ true);
|
||||||
|
}
|
||||||
|
|
||||||
|
nlohmann::json JsonRpcDispatcher::m_tree_find(const nlohmann::json& params) {
|
||||||
|
m_backend.refresh_ui();
|
||||||
|
const UiNode root = m_backend.dump_tree(DumpOptions{});
|
||||||
|
const Target target =
|
||||||
|
parse_target(params.is_object() ? params : nlohmann::json::object());
|
||||||
|
nlohmann::json arr = nlohmann::json::array();
|
||||||
|
for (const UiNode* n : find_matches(root, target))
|
||||||
|
arr.push_back(node_to_json(*n, /*include_children*/ false));
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
nlohmann::json JsonRpcDispatcher::m_widget_get(const nlohmann::json& params) {
|
||||||
|
if (!params.is_object() || !params.contains("target"))
|
||||||
|
throw AutomationError(kInvalidParams, "widget.get requires 'target'");
|
||||||
|
m_backend.refresh_ui();
|
||||||
|
const UiNode root = m_backend.dump_tree(DumpOptions{});
|
||||||
|
int count = 0;
|
||||||
|
const UiNode* node = resolve_unique(root, parse_target(params.at("target")), count);
|
||||||
|
if (count == 0) throw AutomationError(kErrNotFound, "target not found");
|
||||||
|
if (count > 1) throw AutomationError(kErrNotFound, "target is ambiguous");
|
||||||
|
return node_to_json(*node, /*include_children*/ true);
|
||||||
|
}
|
||||||
|
|
||||||
nlohmann::json JsonRpcDispatcher::m_input_click(const nlohmann::json&) { throw AutomationError(kMethodNotFound, "not implemented"); }
|
nlohmann::json JsonRpcDispatcher::m_input_click(const nlohmann::json&) { throw AutomationError(kMethodNotFound, "not implemented"); }
|
||||||
nlohmann::json JsonRpcDispatcher::m_input_type(const nlohmann::json&) { throw AutomationError(kMethodNotFound, "not implemented"); }
|
nlohmann::json JsonRpcDispatcher::m_input_type(const nlohmann::json&) { throw AutomationError(kMethodNotFound, "not implemented"); }
|
||||||
nlohmann::json JsonRpcDispatcher::m_input_key(const nlohmann::json&) { throw AutomationError(kMethodNotFound, "not implemented"); }
|
nlohmann::json JsonRpcDispatcher::m_input_key(const nlohmann::json&) { throw AutomationError(kMethodNotFound, "not implemented"); }
|
||||||
|
|||||||
@@ -42,3 +42,49 @@ TEST_CASE("missing method field -> invalid request", "[automation][rpc]") {
|
|||||||
const json resp = d.dispatch(req);
|
const json resp = d.dispatch(req);
|
||||||
CHECK(resp.at("error").at("code") == kInvalidRequest);
|
CHECK(resp.at("error").at("code") == kInvalidRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
UiNode dispatcher_tree() {
|
||||||
|
UiNode root; root.klass = "MainFrame"; root.path = "MainFrame";
|
||||||
|
UiNode b; b.id = "btn_slice"; b.klass = "Button"; b.label = "Slice plate";
|
||||||
|
b.path = "MainFrame/Button[0]"; b.rect = {10,20,100,30};
|
||||||
|
UiNode e; e.id = "btn_export"; e.klass = "Button"; e.label = "Export";
|
||||||
|
e.path = "MainFrame/Button[1]"; e.enabled = false;
|
||||||
|
root.children = {b, e};
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("tree.dump returns the serialized tree", "[automation][rpc]") {
|
||||||
|
MockUiBackend mock; mock.tree = dispatcher_tree();
|
||||||
|
JsonRpcDispatcher d(mock);
|
||||||
|
const json resp = d.dispatch({{"jsonrpc","2.0"},{"id",1},{"method","tree.dump"}});
|
||||||
|
const json& result = resp.at("result");
|
||||||
|
CHECK(result.at("class") == "MainFrame");
|
||||||
|
CHECK(result.at("children").size() == 2);
|
||||||
|
CHECK(mock.refresh_count == 1); // refreshed before reading
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("tree.find returns matching nodes", "[automation][rpc]") {
|
||||||
|
MockUiBackend mock; mock.tree = dispatcher_tree();
|
||||||
|
JsonRpcDispatcher d(mock);
|
||||||
|
const json resp = d.dispatch({{"jsonrpc","2.0"},{"id",2},{"method","tree.find"},
|
||||||
|
{"params",{{"class","Button"}}}});
|
||||||
|
CHECK(resp.at("result").size() == 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("widget.get returns a single node by id", "[automation][rpc]") {
|
||||||
|
MockUiBackend mock; mock.tree = dispatcher_tree();
|
||||||
|
JsonRpcDispatcher d(mock);
|
||||||
|
const json resp = d.dispatch({{"jsonrpc","2.0"},{"id",3},{"method","widget.get"},
|
||||||
|
{"params",{{"target",{{"id","btn_slice"}}}}}});
|
||||||
|
CHECK(resp.at("result").at("id") == "btn_slice");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("widget.get not found -> 1001", "[automation][rpc]") {
|
||||||
|
MockUiBackend mock; mock.tree = dispatcher_tree();
|
||||||
|
JsonRpcDispatcher d(mock);
|
||||||
|
const json resp = d.dispatch({{"jsonrpc","2.0"},{"id",4},{"method","widget.get"},
|
||||||
|
{"params",{{"target",{{"id","nope"}}}}}});
|
||||||
|
CHECK(resp.at("error").at("code") == kErrNotFound);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user