feat(automation): sync.wait_for poll loop

This commit is contained in:
SoftFever
2026-06-03 02:13:35 +08:00
parent b0325c999a
commit 8dcbc582fa
2 changed files with 72 additions and 1 deletions

View File

@@ -1,6 +1,7 @@
#include "JsonRpcDispatcher.hpp"
#include "WidgetSerializer.hpp"
#include "Locator.hpp"
#include <algorithm>
#include <chrono>
#include <thread>
@@ -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<std::string>();
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<std::string> 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<int>() : 5000;
const int poll_ms = params.contains("poll_ms") && params.at("poll_ms").is_number_integer()
? std::max(1, params.at("poll_ms").get<int>()) : 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::milliseconds>(
std::chrono::steady_clock::now() - start).count();
return { {"ok", true}, {"elapsed_ms", static_cast<int>(elapsed)} };
}
const auto elapsed_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
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());

View File

@@ -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);
}