8#include "absl/strings/str_cat.h"
9#include "absl/strings/str_format.h"
10#include "absl/time/time.h"
18std::optional<absl::Time> OptionalTimeFromMillis(int64_t millis) {
22 return absl::FromUnixMillis(millis);
25yaze::test::WidgetType ConvertWidgetTypeFilterToProto(
WidgetTypeFilter filter) {
26 using ProtoType = yaze::test::WidgetType;
29 return ProtoType::WIDGET_TYPE_ALL;
31 return ProtoType::WIDGET_TYPE_BUTTON;
33 return ProtoType::WIDGET_TYPE_INPUT;
35 return ProtoType::WIDGET_TYPE_MENU;
37 return ProtoType::WIDGET_TYPE_TAB;
39 return ProtoType::WIDGET_TYPE_CHECKBOX;
41 return ProtoType::WIDGET_TYPE_SLIDER;
43 return ProtoType::WIDGET_TYPE_CANVAS;
45 return ProtoType::WIDGET_TYPE_SELECTABLE;
47 return ProtoType::WIDGET_TYPE_OTHER;
50 return ProtoType::WIDGET_TYPE_UNSPECIFIED;
55 yaze::test::GetTestStatusResponse::TestStatus status) {
56 using ProtoStatus = yaze::test::GetTestStatusResponse::TestStatus;
58 case ProtoStatus::GetTestStatusResponse_TestStatus_TEST_STATUS_QUEUED:
60 case ProtoStatus::GetTestStatusResponse_TestStatus_TEST_STATUS_RUNNING:
62 case ProtoStatus::GetTestStatusResponse_TestStatus_TEST_STATUS_PASSED:
64 case ProtoStatus::GetTestStatusResponse_TestStatus_TEST_STATUS_FAILED:
66 case ProtoStatus::GetTestStatusResponse_TestStatus_TEST_STATUS_TIMEOUT:
68 case ProtoStatus::GetTestStatusResponse_TestStatus_TEST_STATUS_UNSPECIFIED:
75 int64_t frame_id, int32_t pending_editor_actions,
76 int32_t pending_layout_actions,
bool layout_rebuild_pending,
77 int32_t pending_harness_tests, int32_t queued_harness_tests,
78 int32_t running_harness_tests, int64_t sampled_at_ms,
bool idle) {
87 state.
sampled_at = OptionalTimeFromMillis(sampled_at_ms);
96 : server_address_(server_address) {}
101 grpc::CreateChannel(
server_address_, grpc::InsecureChannelCredentials());
103 return absl::InternalError(
"Failed to create gRPC channel");
106 stub_ = yaze::test::ImGuiTestHarness::NewStub(channel);
108 return absl::InternalError(
"Failed to create gRPC stub");
112 auto result =
Ping(
"connection_test");
114 return absl::UnavailableError(
115 absl::StrFormat(
"Failed to connect to test harness at %s: %s",
120 return absl::OkStatus();
122 return absl::UnimplementedError(
123 "GUI automation requires YAZE_WITH_GRPC=ON at build time");
128 const std::string& message) {
131 return absl::FailedPreconditionError(
132 "Not connected. Call Connect() first.");
135 yaze::test::PingRequest request;
136 request.set_message(message);
138 yaze::test::PingResponse response;
139 grpc::ClientContext context;
141 grpc::Status status = stub_->Ping(&context, request, &response);
144 return absl::InternalError(
145 absl::StrFormat(
"Ping RPC failed: %s", status.error_message()));
151 absl::StrFormat(
"Server version: %s (timestamp: %lld)",
152 response.yaze_version(), response.timestamp_ms());
157 return absl::UnimplementedError(
"gRPC not available");
162 const std::string& script_path,
bool ci_mode,
163 const std::map<std::string, std::string>& parameter_overrides) {
166 return absl::FailedPreconditionError(
167 "Not connected. Call Connect() first.");
170 yaze::test::ReplayTestRequest request;
171 request.set_script_path(script_path);
172 request.set_ci_mode(ci_mode);
173 for (
const auto& [key, value] : parameter_overrides) {
174 (*request.mutable_parameter_overrides())[key] = value;
177 yaze::test::ReplayTestResponse response;
178 grpc::ClientContext context;
180 grpc::Status status = stub_->ReplayTest(&context, request, &response);
182 return absl::InternalError(
183 absl::StrCat(
"ReplayTest RPC failed: ", status.error_message()));
187 result.
success = response.success();
188 result.
message = response.message();
191 result.
logs.assign(response.logs().begin(), response.logs().end());
192 result.
assertions.reserve(response.assertions_size());
193 for (
const auto& assertion_proto : response.assertions()) {
195 assertion.
description = assertion_proto.description();
196 assertion.
passed = assertion_proto.passed();
198 assertion.
actual_value = assertion_proto.actual_value();
200 result.
assertions.push_back(std::move(assertion));
205 return absl::UnimplementedError(
"gRPC not available");
210 const std::string& output_path,
const std::string& session_name,
211 const std::string& description) {
214 return absl::FailedPreconditionError(
215 "Not connected. Call Connect() first.");
218 yaze::test::StartRecordingRequest request;
219 request.set_output_path(output_path);
220 request.set_session_name(session_name);
221 request.set_description(description);
223 yaze::test::StartRecordingResponse response;
224 grpc::ClientContext context;
225 grpc::Status status = stub_->StartRecording(&context, request, &response);
228 return absl::InternalError(
229 absl::StrCat(
"StartRecording RPC failed: ", status.error_message()));
233 result.
success = response.success();
234 result.
message = response.message();
236 result.
started_at = OptionalTimeFromMillis(response.started_at_ms());
239 return absl::UnimplementedError(
"gRPC not available");
244 const std::string& recording_id,
bool discard) {
247 return absl::FailedPreconditionError(
248 "Not connected. Call Connect() first.");
250 if (recording_id.empty()) {
251 return absl::InvalidArgumentError(
"recording_id must not be empty");
254 yaze::test::StopRecordingRequest request;
255 request.set_recording_id(recording_id);
256 request.set_discard(discard);
258 yaze::test::StopRecordingResponse response;
259 grpc::ClientContext context;
260 grpc::Status status = stub_->StopRecording(&context, request, &response);
263 return absl::InternalError(
264 absl::StrCat(
"StopRecording RPC failed: ", status.error_message()));
268 result.
success = response.success();
269 result.
message = response.message();
272 result.
duration = std::chrono::milliseconds(response.duration_ms());
275 return absl::UnimplementedError(
"gRPC not available");
280 const std::string& target,
ClickType type,
const std::string& widget_key) {
283 return absl::FailedPreconditionError(
284 "Not connected. Call Connect() first.");
287 yaze::test::ClickRequest request;
288 request.set_target(target);
289 if (!widget_key.empty()) {
290 request.set_widget_key(widget_key);
295 request.set_type(yaze::test::ClickRequest::CLICK_TYPE_LEFT);
298 request.set_type(yaze::test::ClickRequest::CLICK_TYPE_RIGHT);
301 request.set_type(yaze::test::ClickRequest::CLICK_TYPE_MIDDLE);
304 request.set_type(yaze::test::ClickRequest::CLICK_TYPE_DOUBLE);
308 yaze::test::ClickResponse response;
309 grpc::ClientContext context;
311 grpc::Status status = stub_->Click(&context, request, &response);
314 return absl::InternalError(
315 absl::StrFormat(
"Click RPC failed: %s", status.error_message()));
319 result.
success = response.success();
320 result.
message = response.message();
322 std::chrono::milliseconds(response.execution_time_ms());
323 result.
test_id = response.test_id();
328 return absl::UnimplementedError(
"gRPC not available");
333 const std::string& target,
const std::string& text,
bool clear_first,
334 const std::string& widget_key) {
337 return absl::FailedPreconditionError(
338 "Not connected. Call Connect() first.");
341 yaze::test::TypeRequest request;
342 request.set_target(target);
343 if (!widget_key.empty()) {
344 request.set_widget_key(widget_key);
346 request.set_text(text);
347 request.set_clear_first(clear_first);
349 yaze::test::TypeResponse response;
350 grpc::ClientContext context;
352 grpc::Status status = stub_->Type(&context, request, &response);
355 return absl::InternalError(
356 absl::StrFormat(
"Type RPC failed: %s", status.error_message()));
360 result.
success = response.success();
361 result.
message = response.message();
363 std::chrono::milliseconds(response.execution_time_ms());
364 result.
test_id = response.test_id();
369 return absl::UnimplementedError(
"gRPC not available");
374 const std::string& condition,
int timeout_ms,
int poll_interval_ms,
375 const std::string& widget_key) {
378 return absl::FailedPreconditionError(
379 "Not connected. Call Connect() first.");
382 yaze::test::WaitRequest request;
383 request.set_condition(condition);
384 if (!widget_key.empty()) {
385 request.set_widget_key(widget_key);
387 request.set_timeout_ms(timeout_ms);
388 request.set_poll_interval_ms(poll_interval_ms);
390 yaze::test::WaitResponse response;
391 grpc::ClientContext context;
393 grpc::Status status = stub_->Wait(&context, request, &response);
396 return absl::InternalError(
397 absl::StrFormat(
"Wait RPC failed: %s", status.error_message()));
401 result.
success = response.success();
402 result.
message = response.message();
403 result.
execution_time = std::chrono::milliseconds(response.elapsed_ms());
404 result.
test_id = response.test_id();
409 return absl::UnimplementedError(
"gRPC not available");
414 const std::string& condition,
const std::string& widget_key) {
417 return absl::FailedPreconditionError(
418 "Not connected. Call Connect() first.");
421 yaze::test::AssertRequest request;
422 request.set_condition(condition);
423 if (!widget_key.empty()) {
424 request.set_widget_key(widget_key);
427 yaze::test::AssertResponse response;
428 grpc::ClientContext context;
430 grpc::Status status = stub_->Assert(&context, request, &response);
433 return absl::InternalError(
434 absl::StrFormat(
"Assert RPC failed: %s", status.error_message()));
438 result.
success = response.success();
439 result.
message = response.message();
443 result.
test_id = response.test_id();
448 return absl::UnimplementedError(
"gRPC not available");
456 return absl::FailedPreconditionError(
457 "Not connected. Call Connect() first.");
460 yaze::test::FlushUiActionsRequest request;
461 request.set_timeout_ms(timeout_ms);
463 yaze::test::FlushUiActionsResponse response;
464 grpc::ClientContext context;
465 grpc::Status status = stub_->FlushUiActions(&context, request, &response);
467 return absl::InternalError(absl::StrFormat(
"FlushUiActions RPC failed: %s",
468 status.error_message()));
471 const bool idle = response.pending_editor_actions() == 0 &&
472 response.pending_layout_actions() == 0 &&
473 !response.layout_rebuild_pending() &&
474 response.pending_harness_tests() == 0;
475 return BuildUiSyncStateSnapshot(
476 response.frame_id(), response.pending_editor_actions(),
477 response.pending_layout_actions(), response.layout_rebuild_pending(),
478 response.pending_harness_tests(), response.queued_harness_tests(),
479 response.running_harness_tests(), 0, idle);
482 return absl::UnimplementedError(
"gRPC not available");
487 int timeout_ms,
int stable_frames,
bool flush_first,
int poll_interval_ms) {
490 return absl::FailedPreconditionError(
491 "Not connected. Call Connect() first.");
494 yaze::test::WaitForIdleRequest request;
495 request.set_timeout_ms(timeout_ms);
496 request.set_stable_frames(stable_frames);
497 request.set_flush_first(flush_first);
498 request.set_poll_interval_ms(poll_interval_ms);
500 yaze::test::WaitForIdleResponse response;
501 grpc::ClientContext context;
502 grpc::Status status = stub_->WaitForIdle(&context, request, &response);
504 return absl::InternalError(
505 absl::StrFormat(
"WaitForIdle RPC failed: %s", status.error_message()));
509 result.
success = response.success();
510 result.
message = response.message();
511 result.
elapsed = std::chrono::milliseconds(response.elapsed_ms());
514 const bool idle = response.pending_editor_actions() == 0 &&
515 response.pending_layout_actions() == 0 &&
516 !response.layout_rebuild_pending() &&
517 response.pending_harness_tests() == 0;
518 result.
state = BuildUiSyncStateSnapshot(
519 response.last_frame_id(), response.pending_editor_actions(),
520 response.pending_layout_actions(), response.layout_rebuild_pending(),
521 response.pending_harness_tests(), response.queued_harness_tests(),
522 response.running_harness_tests(), 0, idle);
529 (void)poll_interval_ms;
530 return absl::UnimplementedError(
"gRPC not available");
537 return absl::FailedPreconditionError(
538 "Not connected. Call Connect() first.");
541 yaze::test::GetUiSyncStateRequest request;
542 yaze::test::GetUiSyncStateResponse response;
543 grpc::ClientContext context;
544 grpc::Status status = stub_->GetUiSyncState(&context, request, &response);
546 return absl::InternalError(absl::StrFormat(
"GetUiSyncState RPC failed: %s",
547 status.error_message()));
550 return BuildUiSyncStateSnapshot(
551 response.frame_id(), response.pending_editor_actions(),
552 response.pending_layout_actions(), response.layout_rebuild_pending(),
553 response.pending_harness_tests(), response.queued_harness_tests(),
554 response.running_harness_tests(), response.sampled_at_ms(),
557 return absl::UnimplementedError(
"gRPC not available");
562 const std::string& region,
const std::string& format) {
565 return absl::FailedPreconditionError(
566 "Not connected. Call Connect() first.");
569 yaze::test::ScreenshotRequest request;
570 request.set_window_title(
"");
573 yaze::test::ScreenshotRequest::IMAGE_FORMAT_BMP);
575 yaze::test::ScreenshotResponse response;
576 grpc::ClientContext context;
578 grpc::Status status = stub_->Screenshot(&context, request, &response);
581 return absl::InternalError(
582 absl::StrFormat(
"Screenshot RPC failed: %s", status.error_message()));
586 result.
success = response.success();
587 result.
message = response.message();
592 return absl::UnimplementedError(
"gRPC not available");
597 const std::string& test_id) {
600 return absl::FailedPreconditionError(
601 "Not connected. Call Connect() first.");
604 yaze::test::GetTestStatusRequest request;
605 request.set_test_id(test_id);
607 yaze::test::GetTestStatusResponse response;
608 grpc::ClientContext context;
610 grpc::Status status = stub_->GetTestStatus(&context, request, &response);
613 return absl::InternalError(absl::StrFormat(
"GetTestStatus RPC failed: %s",
614 status.error_message()));
619 details.
status = ConvertStatusProto(response.status());
620 details.
queued_at = OptionalTimeFromMillis(response.queued_at_ms());
621 details.
started_at = OptionalTimeFromMillis(response.started_at_ms());
622 details.
completed_at = OptionalTimeFromMillis(response.completed_at_ms());
626 response.assertion_failures().end());
629 return absl::UnimplementedError(
"gRPC not available");
634 const std::string& category_filter,
int page_size,
635 const std::string& page_token) {
638 return absl::FailedPreconditionError(
639 "Not connected. Call Connect() first.");
642 yaze::test::ListTestsRequest request;
643 if (!category_filter.empty()) {
644 request.set_category_filter(category_filter);
647 request.set_page_size(page_size);
649 if (!page_token.empty()) {
650 request.set_page_token(page_token);
653 yaze::test::ListTestsResponse response;
654 grpc::ClientContext context;
656 grpc::Status status = stub_->ListTests(&context, request, &response);
659 return absl::InternalError(
660 absl::StrFormat(
"ListTests RPC failed: %s", status.error_message()));
666 result.
tests.reserve(response.tests_size());
668 for (
const auto& test_info : response.tests()) {
670 summary.
test_id = test_info.test_id();
671 summary.
name = test_info.name();
672 summary.
category = test_info.category();
674 OptionalTimeFromMillis(test_info.last_run_timestamp_ms());
679 result.
tests.push_back(std::move(summary));
684 return absl::UnimplementedError(
"gRPC not available");
689 const std::string& test_id,
bool include_logs) {
692 return absl::FailedPreconditionError(
693 "Not connected. Call Connect() first.");
696 yaze::test::GetTestResultsRequest request;
697 request.set_test_id(test_id);
698 request.set_include_logs(include_logs);
700 yaze::test::GetTestResultsResponse response;
701 grpc::ClientContext context;
703 grpc::Status status = stub_->GetTestResults(&context, request, &response);
706 return absl::InternalError(absl::StrFormat(
"GetTestResults RPC failed: %s",
707 status.error_message()));
712 result.
success = response.success();
714 result.
category = response.category();
715 result.
executed_at = OptionalTimeFromMillis(response.executed_at_ms());
718 result.
assertions.reserve(response.assertions_size());
719 for (
const auto& assertion : response.assertions()) {
722 outcome.
passed = assertion.passed();
726 result.
assertions.push_back(std::move(outcome));
730 result.
logs.assign(response.logs().begin(), response.logs().end());
733 for (
const auto& metric : response.metrics()) {
734 result.
metrics.emplace(metric.first, metric.second);
744 return absl::UnimplementedError(
"gRPC not available");
752 return absl::FailedPreconditionError(
753 "Not connected. Call Connect() first.");
756 yaze::test::DiscoverWidgetsRequest request;
760 request.set_type_filter(ConvertWidgetTypeFilterToProto(query.
type_filter));
767 yaze::test::DiscoverWidgetsResponse response;
768 grpc::ClientContext context;
769 grpc::Status status = stub_->DiscoverWidgets(&context, request, &response);
772 return absl::InternalError(absl::StrFormat(
"DiscoverWidgets RPC failed: %s",
773 status.error_message()));
778 if (response.generated_at_ms() > 0) {
779 result.
generated_at = OptionalTimeFromMillis(response.generated_at_ms());
782 result.
windows.reserve(response.windows_size());
783 for (
const auto& window_proto : response.windows()) {
785 window_info.
name = window_proto.name();
786 window_info.
visible = window_proto.visible();
787 window_info.
widgets.reserve(window_proto.widgets_size());
789 for (
const auto& widget_proto : window_proto.widgets()) {
791 widget.
path = widget_proto.path();
792 widget.
widget_key = widget_proto.widget_key();
794 widget.
alias_of = widget_proto.alias_of();
795 widget.
label = widget_proto.label();
796 widget.
type = widget_proto.type();
799 widget.
visible = widget_proto.visible();
800 widget.
enabled = widget_proto.enabled();
801 widget.
has_bounds = widget_proto.has_bounds();
803 widget.
bounds.
min_x = widget_proto.bounds().min_x();
804 widget.
bounds.
min_y = widget_proto.bounds().min_y();
805 widget.
bounds.
max_x = widget_proto.bounds().max_x();
806 widget.
bounds.
max_y = widget_proto.bounds().max_y();
810 widget.
widget_id = widget_proto.widget_id();
813 OptionalTimeFromMillis(widget_proto.last_seen_at_ms());
814 widget.
stale = widget_proto.stale();
815 window_info.
widgets.push_back(std::move(widget));
818 result.
windows.push_back(std::move(window_info));
824 return absl::UnimplementedError(
"gRPC not available");
absl::StatusOr< ReplayTestResult > ReplayTest(const std::string &script_path, bool ci_mode, const std::map< std::string, std::string > ¶meter_overrides={})
absl::StatusOr< AutomationResult > Type(const std::string &target, const std::string &text, bool clear_first=false, const std::string &widget_key="")
Type text into an input field.
absl::StatusOr< AutomationResult > Screenshot(const std::string ®ion="full", const std::string &format="PNG")
Capture a screenshot.
absl::Status Connect()
Connect to the test harness server.
absl::StatusOr< StartRecordingResult > StartRecording(const std::string &output_path, const std::string &session_name, const std::string &description)
absl::StatusOr< AutomationResult > Ping(const std::string &message="ping")
Check if the server is reachable and responsive.
absl::StatusOr< TestStatusDetails > GetTestStatus(const std::string &test_id)
Fetch the current execution status for a harness test.
absl::StatusOr< UiSyncStateSnapshot > GetUiSyncState()
Fetch low-level UI synchronization counters for diagnostics.
GuiAutomationClient(const std::string &server_address)
Construct a new GUI automation client.
absl::StatusOr< DiscoverWidgetsResult > DiscoverWidgets(const DiscoverWidgetsQuery &query)
absl::StatusOr< AutomationResult > Assert(const std::string &condition, const std::string &widget_key="")
Assert a GUI state condition.
absl::StatusOr< TestResultDetails > GetTestResults(const std::string &test_id, bool include_logs=false)
Retrieve detailed results for a harness test execution.
absl::StatusOr< UiSyncStateSnapshot > FlushUiActions(int timeout_ms=2000)
Force a frame-boundary flush for deferred UI actions.
absl::StatusOr< StopRecordingResult > StopRecording(const std::string &recording_id, bool discard=false)
absl::StatusOr< WaitForIdleResult > WaitForIdle(int timeout_ms=5000, int stable_frames=2, bool flush_first=true, int poll_interval_ms=25)
Wait until UI reaches deterministic idle criteria.
absl::StatusOr< AutomationResult > Wait(const std::string &condition, int timeout_ms=5000, int poll_interval_ms=100, const std::string &widget_key="")
Wait for a condition to be met.
absl::StatusOr< AutomationResult > Click(const std::string &target, ClickType type=ClickType::kLeft, const std::string &widget_key="")
Click a GUI element.
absl::StatusOr< ListTestsResult > ListTests(const std::string &category_filter="", int page_size=100, const std::string &page_token="")
Enumerate harness tests with optional filtering.
std::string server_address_
TestRunStatus
Execution status codes returned by the harness.
ClickType
Type of click action to perform.
Individual assertion outcome within a harness test.
std::string error_message
std::string expected_value
Result of a GUI automation action.
std::string resolved_widget_key
std::string resolved_path
std::chrono::milliseconds execution_time
std::string expected_value
std::vector< WidgetDescriptor > widgets
Aggregated metadata about a harness test.
std::optional< absl::Time > last_run_at
Result container for ListTests RPC.
std::string next_page_token
std::vector< HarnessTestSummary > tests
std::vector< AssertionOutcome > assertions
std::vector< std::string > logs
std::string replay_session_id
std::optional< absl::Time > started_at
std::chrono::milliseconds duration
Detailed execution results for a specific harness test.
std::optional< absl::Time > executed_at
std::map< std::string, int > metrics
std::vector< AssertionOutcome > assertions
std::vector< std::string > logs
int64_t screenshot_size_bytes
std::string failure_context
std::string screenshot_path
Detailed information about an individual test execution.
std::vector< std::string > assertion_failures
std::optional< absl::Time > completed_at
std::optional< absl::Time > started_at
std::string error_message
std::optional< absl::Time > queued_at
int pending_editor_actions
int pending_layout_actions
int pending_harness_tests
int running_harness_tests
bool layout_rebuild_pending
std::optional< absl::Time > sampled_at
std::chrono::milliseconds elapsed
int stable_frames_observed
std::string timeout_reason
UiSyncStateSnapshot state