diff --git a/src/slic3r/GUI/Automation/JsonRpcDispatcher.cpp b/src/slic3r/GUI/Automation/JsonRpcDispatcher.cpp index 22ae6567f1..d98277d9cc 100644 --- a/src/slic3r/GUI/Automation/JsonRpcDispatcher.cpp +++ b/src/slic3r/GUI/Automation/JsonRpcDispatcher.cpp @@ -18,6 +18,42 @@ nlohmann::json JsonRpcDispatcher::make_error(const nlohmann::json& id, int code, {"error", { {"code", code}, {"message", msg} }} }; } +namespace { +std::optional 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(); + 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(); + if (p.contains("visible_only") && p.at("visible_only").is_boolean()) + o.visible_only = p.at("visible_only").get(); + if (p.contains("include_imgui") && p.at("include_imgui").is_boolean()) + o.include_imgui = p.at("include_imgui").get(); + } + return o; +} +} // namespace + nlohmann::json JsonRpcDispatcher::m_version(const nlohmann::json&) { return { {"version", kAutomationVersion}, {"protocol", "2.0"}, @@ -68,10 +104,36 @@ std::string JsonRpcDispatcher::handle_request(const std::string& body) { return dispatch(req).dump(); } -// --- method handlers implemented in Tasks 7-10 (stubs throw for now) --- -nlohmann::json JsonRpcDispatcher::m_tree_dump(const nlohmann::json&) { throw AutomationError(kMethodNotFound, "not implemented"); } -nlohmann::json JsonRpcDispatcher::m_tree_find(const nlohmann::json&) { throw AutomationError(kMethodNotFound, "not implemented"); } -nlohmann::json JsonRpcDispatcher::m_widget_get(const nlohmann::json&) { throw AutomationError(kMethodNotFound, "not implemented"); } +// --- method handlers implemented in Tasks 7-10 (remaining stubs throw for now) --- +nlohmann::json JsonRpcDispatcher::m_tree_dump(const nlohmann::json& params) { + m_backend.refresh_ui(); + 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_type(const nlohmann::json&) { throw AutomationError(kMethodNotFound, "not implemented"); } nlohmann::json JsonRpcDispatcher::m_input_key(const nlohmann::json&) { throw AutomationError(kMethodNotFound, "not implemented"); } diff --git a/tests/automation/test_dispatcher.cpp b/tests/automation/test_dispatcher.cpp index 65a1645f7f..006b8d9cee 100644 --- a/tests/automation/test_dispatcher.cpp +++ b/tests/automation/test_dispatcher.cpp @@ -42,3 +42,49 @@ TEST_CASE("missing method field -> invalid request", "[automation][rpc]") { const json resp = d.dispatch(req); 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); +}