mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-06-19 02:13:27 +00:00
feat(automation): pure UI node model + JSON serializer with unit test
This commit is contained in:
102
src/slic3r/GUI/Automation/IUiBackend.hpp
Normal file
102
src/slic3r/GUI/Automation/IUiBackend.hpp
Normal file
@@ -0,0 +1,102 @@
|
||||
#pragma once
|
||||
// PURE header: no wx / ImGui / GL includes. Safe to compile in the display-free
|
||||
// unit-test target. Shared by the dispatcher, serializer, locator, and backends.
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r { namespace GUI { namespace Automation {
|
||||
|
||||
enum class BackendKind { Wx, ImGui };
|
||||
|
||||
struct Rect { int x = 0, y = 0, w = 0, h = 0; };
|
||||
|
||||
// One node of the unified UI tree. `handle` is opaque (wxWindow* cast to uintptr_t
|
||||
// for wx, item index for ImGui); it is used by WxUiBackend to recover concrete
|
||||
// objects and is NEVER serialized.
|
||||
struct UiNode {
|
||||
BackendKind backend = BackendKind::Wx;
|
||||
std::string id; // automation id if set, else derived path id
|
||||
std::string path; // positional path, e.g. "MainFrame/Panel[2]/Button[0]"
|
||||
std::string klass; // wx class name or imgui item type
|
||||
std::string label;
|
||||
Rect rect; // screen coordinates
|
||||
bool enabled = true;
|
||||
bool visible = true;
|
||||
bool has_value = false;
|
||||
std::string value; // when applicable (text/choice/check/slider)
|
||||
std::uint64_t handle = 0;
|
||||
std::vector<UiNode> children; // wx only; imgui items are flat under their window
|
||||
};
|
||||
|
||||
struct DumpOptions {
|
||||
std::optional<std::string> root; // id/path to root the dump at
|
||||
int max_depth = -1; // -1 = unlimited
|
||||
bool visible_only = false;
|
||||
bool include_imgui = true;
|
||||
};
|
||||
|
||||
enum class MouseButton { Left, Right, Middle };
|
||||
enum class KeyModifier { Ctrl, Shift, Alt, Cmd };
|
||||
|
||||
struct KeyChord {
|
||||
std::vector<KeyModifier> modifiers;
|
||||
std::string key; // normalized lowercase: "s", "enter", "f5", "tab", ...
|
||||
};
|
||||
|
||||
struct AppState {
|
||||
std::string active_tab;
|
||||
bool project_loaded = false;
|
||||
bool slicing = false;
|
||||
int slice_progress = -1; // -1 = unknown
|
||||
std::optional<std::string> modal_dialog;
|
||||
bool foreground = false;
|
||||
};
|
||||
|
||||
struct PngImage {
|
||||
std::vector<unsigned char> png; // encoded PNG bytes
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
};
|
||||
|
||||
// Thrown by backends/dispatcher; carries a JSON-RPC application error code.
|
||||
struct AutomationError : std::runtime_error {
|
||||
int code;
|
||||
AutomationError(int code, std::string msg)
|
||||
: std::runtime_error(std::move(msg)), code(code) {}
|
||||
};
|
||||
|
||||
// Backend abstraction. The dispatcher orchestrates; the backend only snapshots
|
||||
// and executes primitives on already-resolved nodes.
|
||||
class IUiBackend {
|
||||
public:
|
||||
virtual ~IUiBackend() = default;
|
||||
|
||||
// Force a fresh frame so transient ImGui items are recorded before a read or
|
||||
// action. No-op for non-GUI backends.
|
||||
virtual void refresh_ui() = 0;
|
||||
|
||||
// Snapshot the UI tree (wx hierarchy + flat imgui items under their windows).
|
||||
virtual UiNode dump_tree(const DumpOptions& opts) = 0;
|
||||
|
||||
// Application-level state snapshot.
|
||||
virtual AppState app_state() = 0;
|
||||
|
||||
// Click a resolved node (uses its rect/handle). Raises/focuses first.
|
||||
virtual bool click(const UiNode& node, MouseButton button, bool dbl,
|
||||
const std::vector<KeyModifier>& modifiers) = 0;
|
||||
// Type into the currently-focused control.
|
||||
virtual bool type_text(const std::string& text) = 0;
|
||||
// Send key chords (e.g. ctrl+s) to the focused window.
|
||||
virtual bool send_keys(const std::vector<KeyChord>& chords) = 0;
|
||||
|
||||
// Screenshots. target == nullptr => main frame.
|
||||
virtual PngImage screenshot_window(const UiNode* target) = 0;
|
||||
virtual PngImage screenshot_viewport3d(std::optional<int> plate,
|
||||
std::optional<int> width,
|
||||
std::optional<int> height) = 0;
|
||||
};
|
||||
|
||||
}}} // namespace Slic3r::GUI::Automation
|
||||
43
src/slic3r/GUI/Automation/WidgetSerializer.cpp
Normal file
43
src/slic3r/GUI/Automation/WidgetSerializer.cpp
Normal file
@@ -0,0 +1,43 @@
|
||||
#include "WidgetSerializer.hpp"
|
||||
|
||||
namespace Slic3r { namespace GUI { namespace Automation {
|
||||
|
||||
static const char* backend_name(BackendKind k) {
|
||||
return k == BackendKind::Wx ? "wx" : "imgui";
|
||||
}
|
||||
|
||||
nlohmann::json node_to_json(const UiNode& node, bool include_children) {
|
||||
nlohmann::json j;
|
||||
j["backend"] = backend_name(node.backend);
|
||||
j["id"] = node.id;
|
||||
j["path"] = node.path;
|
||||
j["class"] = node.klass;
|
||||
j["label"] = node.label;
|
||||
j["rect"] = { {"x", node.rect.x}, {"y", node.rect.y},
|
||||
{"w", node.rect.w}, {"h", node.rect.h} };
|
||||
j["enabled"] = node.enabled;
|
||||
j["visible"] = node.visible;
|
||||
if (node.has_value)
|
||||
j["value"] = node.value;
|
||||
if (include_children && node.backend == BackendKind::Wx) {
|
||||
nlohmann::json arr = nlohmann::json::array();
|
||||
for (const UiNode& c : node.children)
|
||||
arr.push_back(node_to_json(c, true));
|
||||
j["children"] = std::move(arr);
|
||||
}
|
||||
return j;
|
||||
}
|
||||
|
||||
nlohmann::json app_state_to_json(const AppState& s) {
|
||||
nlohmann::json j;
|
||||
j["active_tab"] = s.active_tab;
|
||||
j["project_loaded"] = s.project_loaded;
|
||||
j["slicing"] = s.slicing;
|
||||
j["slice_progress"] = s.slice_progress;
|
||||
j["foreground"] = s.foreground;
|
||||
if (s.modal_dialog)
|
||||
j["modal_dialog"] = *s.modal_dialog;
|
||||
return j;
|
||||
}
|
||||
|
||||
}}} // namespace
|
||||
14
src/slic3r/GUI/Automation/WidgetSerializer.hpp
Normal file
14
src/slic3r/GUI/Automation/WidgetSerializer.hpp
Normal file
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
#include "IUiBackend.hpp"
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace Slic3r { namespace GUI { namespace Automation {
|
||||
|
||||
// Serialize a node to the unified JSON shape from the design spec (§5).
|
||||
// `include_children` controls recursion into UiNode::children.
|
||||
nlohmann::json node_to_json(const UiNode& node, bool include_children);
|
||||
|
||||
// Serialize an application-state snapshot.
|
||||
nlohmann::json app_state_to_json(const AppState& state);
|
||||
|
||||
}}} // namespace
|
||||
@@ -52,5 +52,6 @@ add_subdirectory(libslic3r)
|
||||
add_subdirectory(slic3rutils)
|
||||
add_subdirectory(fff_print)
|
||||
add_subdirectory(sla_print)
|
||||
add_subdirectory(automation)
|
||||
|
||||
|
||||
|
||||
16
tests/automation/CMakeLists.txt
Normal file
16
tests/automation/CMakeLists.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
|
||||
|
||||
add_executable(${_TEST_NAME}_tests
|
||||
automation_tests.cpp
|
||||
test_serializer.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/slic3r/GUI/Automation/WidgetSerializer.cpp
|
||||
)
|
||||
target_link_libraries(${_TEST_NAME}_tests test_common Catch2::Catch2WithMain nlohmann_json)
|
||||
# The nlohmann_json INTERFACE target only exposes deps_src/nlohmann on the include
|
||||
# path (so <json.hpp> resolves). The rest of the codebase reaches <nlohmann/json.hpp>
|
||||
# via the deps_src root that admesh re-exports transitively; this pure unit-test
|
||||
# target does not link admesh, so add the deps_src root explicitly to match.
|
||||
target_include_directories(${_TEST_NAME}_tests PRIVATE ${CMAKE_SOURCE_DIR}/src ${CMAKE_SOURCE_DIR}/deps_src)
|
||||
set_property(TARGET ${_TEST_NAME}_tests PROPERTY FOLDER "tests")
|
||||
orcaslicer_copy_test_dlls()
|
||||
catch_discover_tests(${_TEST_NAME}_tests)
|
||||
4
tests/automation/automation_tests.cpp
Normal file
4
tests/automation/automation_tests.cpp
Normal file
@@ -0,0 +1,4 @@
|
||||
// Catch2 provides main() via Catch2::Catch2WithMain. This TU exists so the
|
||||
// executable has at least one source plus a stable name; per-feature TEST_CASEs
|
||||
// live in the test_*.cpp files.
|
||||
#include <catch2/catch_all.hpp>
|
||||
32
tests/automation/test_serializer.cpp
Normal file
32
tests/automation/test_serializer.cpp
Normal file
@@ -0,0 +1,32 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
#include "slic3r/GUI/Automation/WidgetSerializer.hpp"
|
||||
|
||||
using namespace Slic3r::GUI::Automation;
|
||||
|
||||
TEST_CASE("node_to_json emits the unified node shape", "[automation][serializer]") {
|
||||
UiNode n;
|
||||
n.backend = BackendKind::Wx;
|
||||
n.id = "btn_slice";
|
||||
n.path = "MainFrame/Panel[2]/Button[0]";
|
||||
n.klass = "Button";
|
||||
n.label = "Slice plate";
|
||||
n.rect = {100, 200, 120, 32};
|
||||
n.enabled = true;
|
||||
n.visible = true;
|
||||
|
||||
const nlohmann::json j = node_to_json(n, /*include_children*/ false);
|
||||
|
||||
CHECK(j.at("backend") == "wx");
|
||||
CHECK(j.at("id") == "btn_slice");
|
||||
CHECK(j.at("path") == "MainFrame/Panel[2]/Button[0]");
|
||||
CHECK(j.at("class") == "Button");
|
||||
CHECK(j.at("label") == "Slice plate");
|
||||
CHECK(j.at("rect").at("x") == 100);
|
||||
CHECK(j.at("rect").at("w") == 120);
|
||||
CHECK(j.at("enabled") == true);
|
||||
CHECK(j.at("visible") == true);
|
||||
// `handle` must never leak into JSON.
|
||||
CHECK_FALSE(j.contains("handle"));
|
||||
// No value set -> no "value" key.
|
||||
CHECK_FALSE(j.contains("value"));
|
||||
}
|
||||
Reference in New Issue
Block a user