diff --git a/src/slic3r/GUI/Automation/JsonRpcDispatcher.cpp b/src/slic3r/GUI/Automation/JsonRpcDispatcher.cpp index 08140b4017..a3269f9a6c 100644 --- a/src/slic3r/GUI/Automation/JsonRpcDispatcher.cpp +++ b/src/slic3r/GUI/Automation/JsonRpcDispatcher.cpp @@ -1,6 +1,7 @@ #include "JsonRpcDispatcher.hpp" #include "WidgetSerializer.hpp" #include "Locator.hpp" +#include #include #include @@ -291,7 +292,43 @@ nlohmann::json JsonRpcDispatcher::m_input_key(const nlohmann::json& params) { return { {"ok", ok} }; } -nlohmann::json JsonRpcDispatcher::m_sync_wait_for(const nlohmann::json&) { throw AutomationError(kMethodNotFound, "not implemented"); } +nlohmann::json JsonRpcDispatcher::m_sync_wait_for(const nlohmann::json& params) { + if (!params.is_object() || !params.contains("target") || !params.contains("state")) + throw AutomationError(kInvalidParams, "sync.wait_for requires 'target' and 'state'"); + + const Target target = parse_target(params.at("target")); + const std::string state_s = params.at("state").get(); + WaitState state; + if (state_s == "exists") state = WaitState::Exists; + else if (state_s == "visible") state = WaitState::Visible; + else if (state_s == "enabled") state = WaitState::Enabled; + else if (state_s == "value") state = WaitState::Value; + else throw AutomationError(kInvalidParams, "unknown state: " + state_s); + + std::optional expected = opt_str(params, "value"); + const int timeout_ms = params.contains("timeout_ms") && params.at("timeout_ms").is_number_integer() + ? params.at("timeout_ms").get() : 5000; + const int poll_ms = params.contains("poll_ms") && params.at("poll_ms").is_number_integer() + ? std::max(1, params.at("poll_ms").get()) : 100; + + const auto start = std::chrono::steady_clock::now(); + for (;;) { + m_backend.refresh_ui(); + const UiNode root = m_backend.dump_tree(DumpOptions{}); + int count = 0; + const UiNode* node = resolve_unique(root, target, count); + if (evaluate_state(node, state, expected)) { + const auto elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - start).count(); + return { {"ok", true}, {"elapsed_ms", static_cast(elapsed)} }; + } + const auto elapsed_ms = std::chrono::duration_cast( + std::chrono::steady_clock::now() - start).count(); + if (elapsed_ms >= timeout_ms) + throw AutomationError(kErrWaitTimeout, "wait_for timed out for state: " + state_s); + std::this_thread::sleep_for(std::chrono::milliseconds(poll_ms)); + } +} nlohmann::json JsonRpcDispatcher::m_app_state(const nlohmann::json&) { return app_state_to_json(m_backend.app_state()); diff --git a/tests/automation/test_dispatcher.cpp b/tests/automation/test_dispatcher.cpp index 5a73ebbdc8..963d604ef4 100644 --- a/tests/automation/test_dispatcher.cpp +++ b/tests/automation/test_dispatcher.cpp @@ -170,3 +170,37 @@ TEST_CASE("screenshot.viewport3d returns base64 + dims", "[automation][rpc]") { CHECK(mock.screenshot_viewport_count == 1); CHECK(resp.at("result").at("png_base64").is_string()); } + +TEST_CASE("sync.wait_for succeeds once the condition holds", "[automation][rpc]") { + MockUiBackend mock; + // First 2 polls: btn disabled. 3rd poll: enabled. + mock.tree_provider = [](int call) { + UiNode root; root.klass = "MainFrame"; root.path = "MainFrame"; + UiNode b; b.id = "btn_slice"; b.klass = "Button"; b.path = "MainFrame/Button[0]"; + b.visible = true; b.enabled = (call >= 2); + root.children = {b}; + return root; + }; + JsonRpcDispatcher d(mock); + const json resp = d.dispatch({{"jsonrpc","2.0"},{"id",1},{"method","sync.wait_for"}, + {"params",{{"target",{{"id","btn_slice"}}},{"state","enabled"}, + {"timeout_ms",2000},{"poll_ms",1}}}}); + CHECK(resp.at("result").at("ok") == true); + CHECK(mock.dump_count >= 3); +} + +TEST_CASE("sync.wait_for times out -> 1003", "[automation][rpc]") { + MockUiBackend mock; + mock.tree_provider = [](int) { + UiNode root; root.klass = "MainFrame"; root.path = "MainFrame"; + UiNode b; b.id = "btn_slice"; b.visible = true; b.enabled = false; + b.path = "MainFrame/Button[0]"; + root.children = {b}; + return root; + }; + JsonRpcDispatcher d(mock); + const json resp = d.dispatch({{"jsonrpc","2.0"},{"id",2},{"method","sync.wait_for"}, + {"params",{{"target",{{"id","btn_slice"}}},{"state","enabled"}, + {"timeout_ms",30},{"poll_ms",5}}}}); + CHECK(resp.at("error").at("code") == kErrWaitTimeout); +}