diff --git a/src/slic3r/GUI/Automation/JsonRpcDispatcher.cpp b/src/slic3r/GUI/Automation/JsonRpcDispatcher.cpp index 356664f7dc..5dfe9effbf 100644 --- a/src/slic3r/GUI/Automation/JsonRpcDispatcher.cpp +++ b/src/slic3r/GUI/Automation/JsonRpcDispatcher.cpp @@ -127,6 +127,33 @@ std::vector parse_keys(const nlohmann::json& params) { throw AutomationError(kInvalidParams, "'keys' is empty"); return { chord_from_tokens(tokens) }; } + +// "paths" may be a single string ("C:/a.stl") or an array of strings. Returns the +// non-empty absolute paths; throws kInvalidParams when paths is missing, not a +// string/array, contains a non-string entry, or yields no non-empty path. +std::vector parse_paths(const nlohmann::json& params) { + if (!params.is_object() || !params.contains("paths")) + throw AutomationError(kInvalidParams, "file.open requires 'paths'"); + const auto& p = params.at("paths"); + std::vector out; + if (p.is_string()) { + out.push_back(p.get()); + } else if (p.is_array()) { + for (const auto& e : p) { + if (!e.is_string()) + throw AutomationError(kInvalidParams, "'paths' entries must be strings"); + out.push_back(e.get()); + } + } else { + throw AutomationError(kInvalidParams, "'paths' must be a string or array"); + } + out.erase(std::remove_if(out.begin(), out.end(), + [](const std::string& s) { return s.empty(); }), + out.end()); + if (out.empty()) + throw AutomationError(kInvalidParams, "'paths' is empty"); + return out; +} } // namespace namespace { @@ -193,6 +220,7 @@ nlohmann::json JsonRpcDispatcher::dispatch(const nlohmann::json& request) { if (method == "sync.wait_for") return make_result(id, m_sync_wait_for(params)); if (method == "app.state") return make_result(id, m_app_state(params)); if (method == "screenshot.window") return make_result(id, m_screenshot_window(params)); + if (method == "file.open") return make_result(id, m_file_open(params)); return make_error(id, kMethodNotFound, "unknown method: " + method); } catch (const AutomationError& e) { return make_error(id, e.code, e.what()); @@ -342,4 +370,10 @@ nlohmann::json JsonRpcDispatcher::m_screenshot_window(const nlohmann::json& para return image_to_json(m_backend.screenshot_window(target_ptr)); } +nlohmann::json JsonRpcDispatcher::m_file_open(const nlohmann::json& params) { + const std::vector paths = parse_paths(params); + const int loaded = m_backend.open_files(paths); + return { {"ok", true}, {"loaded", loaded} }; +} + }}} // namespace diff --git a/src/slic3r/GUI/Automation/JsonRpcDispatcher.hpp b/src/slic3r/GUI/Automation/JsonRpcDispatcher.hpp index 401df06d72..32c95bd8a6 100644 --- a/src/slic3r/GUI/Automation/JsonRpcDispatcher.hpp +++ b/src/slic3r/GUI/Automation/JsonRpcDispatcher.hpp @@ -48,6 +48,7 @@ private: nlohmann::json m_sync_wait_for(const nlohmann::json& params); nlohmann::json m_app_state(const nlohmann::json& params); nlohmann::json m_screenshot_window(const nlohmann::json& params); + nlohmann::json m_file_open(const nlohmann::json& params); // Resolve a unique, actionable (enabled+visible) node from params["target"]. // Throws kErrNotFound (missing/ambiguous) or kErrNotActionable (disabled/hidden). diff --git a/tests/automation/test_dispatcher.cpp b/tests/automation/test_dispatcher.cpp index d074248300..86988e38e4 100644 --- a/tests/automation/test_dispatcher.cpp +++ b/tests/automation/test_dispatcher.cpp @@ -195,3 +195,65 @@ TEST_CASE("sync.wait_for times out -> 1003", "[automation][rpc]") { {"timeout_ms",30},{"poll_ms",5}}}}); CHECK(resp.at("error").at("code") == kErrWaitTimeout); } + +TEST_CASE("file.open with an array of paths routes to backend", "[automation][rpc]") { + MockUiBackend mock; + mock.open_return_count = 3; + JsonRpcDispatcher d(mock); + const json resp = d.dispatch({{"jsonrpc","2.0"},{"id",1},{"method","file.open"}, + {"params",{{"paths", json::array({"C:/abs/a.stl","C:/abs/b.stl"})}}}}); + CHECK(resp.at("result").at("ok") == true); + CHECK(resp.at("result").at("loaded") == 3); + REQUIRE(mock.opened_paths.size() == 1); + REQUIRE(mock.opened_paths[0].size() == 2); + CHECK(mock.opened_paths[0][0] == "C:/abs/a.stl"); + CHECK(mock.opened_paths[0][1] == "C:/abs/b.stl"); +} + +TEST_CASE("file.open accepts a bare string path", "[automation][rpc]") { + MockUiBackend mock; + mock.open_return_count = 1; + JsonRpcDispatcher d(mock); + const json resp = d.dispatch({{"jsonrpc","2.0"},{"id",2},{"method","file.open"}, + {"params",{{"paths","C:/abs/a.stl"}}}}); + CHECK(resp.at("result").at("loaded") == 1); + REQUIRE(mock.opened_paths.size() == 1); + REQUIRE(mock.opened_paths[0].size() == 1); + CHECK(mock.opened_paths[0][0] == "C:/abs/a.stl"); +} + +TEST_CASE("file.open with missing paths -> invalid params", "[automation][rpc]") { + MockUiBackend mock; + JsonRpcDispatcher d(mock); + const json resp = d.dispatch({{"jsonrpc","2.0"},{"id",3},{"method","file.open"}, + {"params", json::object()}}); + CHECK(resp.at("error").at("code") == kInvalidParams); + CHECK(mock.opened_paths.empty()); +} + +TEST_CASE("file.open with empty paths array -> invalid params", "[automation][rpc]") { + MockUiBackend mock; + JsonRpcDispatcher d(mock); + const json resp = d.dispatch({{"jsonrpc","2.0"},{"id",4},{"method","file.open"}, + {"params",{{"paths", json::array()}}}}); + CHECK(resp.at("error").at("code") == kInvalidParams); + CHECK(mock.opened_paths.empty()); +} + +TEST_CASE("file.open with a non-string entry -> invalid params", "[automation][rpc]") { + MockUiBackend mock; + JsonRpcDispatcher d(mock); + const json resp = d.dispatch({{"jsonrpc","2.0"},{"id",5},{"method","file.open"}, + {"params",{{"paths", json::array({"C:/a.stl", 42})}}}}); + CHECK(resp.at("error").at("code") == kInvalidParams); + CHECK(mock.opened_paths.empty()); +} + +TEST_CASE("file.open backend load failure -> 1007", "[automation][rpc]") { + MockUiBackend mock; + mock.open_should_fail = true; + JsonRpcDispatcher d(mock); + const json resp = d.dispatch({{"jsonrpc","2.0"},{"id",6},{"method","file.open"}, + {"params",{{"paths","C:/abs/a.stl"}}}}); + CHECK(resp.at("error").at("code") == kErrLoadFailed); +}