mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-06-22 19:50:44 +00:00
feat(automation): pure locator (id/path/predicate) with unit tests
This commit is contained in:
62
src/slic3r/GUI/Automation/Locator.cpp
Normal file
62
src/slic3r/GUI/Automation/Locator.cpp
Normal file
@@ -0,0 +1,62 @@
|
||||
#include "Locator.hpp"
|
||||
|
||||
namespace Slic3r { namespace GUI { namespace Automation {
|
||||
|
||||
static void flatten_into(const UiNode& n, std::vector<const UiNode*>& out) {
|
||||
out.push_back(&n);
|
||||
for (const UiNode& c : n.children)
|
||||
flatten_into(c, out);
|
||||
}
|
||||
|
||||
std::vector<const UiNode*> flatten(const UiNode& root) {
|
||||
std::vector<const UiNode*> out;
|
||||
flatten_into(root, out);
|
||||
return out;
|
||||
}
|
||||
|
||||
static bool matches_predicate(const UiNode& n, const Target& t) {
|
||||
if (t.backend && n.backend != *t.backend) return false;
|
||||
if (t.name && !(n.id == *t.name || n.label == *t.name)) return false;
|
||||
if (t.klass && n.klass != *t.klass) return false;
|
||||
if (t.label && n.label != *t.label) return false;
|
||||
if (t.value && !(n.has_value && n.value == *t.value)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<const UiNode*> find_matches(const UiNode& root, const Target& target) {
|
||||
const auto all = flatten(root);
|
||||
std::vector<const UiNode*> out;
|
||||
|
||||
// Resolution order: exact id -> exact path -> predicate.
|
||||
if (target.id) {
|
||||
for (const UiNode* n : all)
|
||||
if (n->id == *target.id) out.push_back(n);
|
||||
return out;
|
||||
}
|
||||
if (target.path) {
|
||||
for (const UiNode* n : all)
|
||||
if (n->path == *target.path) out.push_back(n);
|
||||
return out;
|
||||
}
|
||||
if (target.empty())
|
||||
return out; // nothing to match on
|
||||
for (const UiNode* n : all)
|
||||
if (matches_predicate(*n, target)) out.push_back(n);
|
||||
return out;
|
||||
}
|
||||
|
||||
bool evaluate_state(const UiNode* node, WaitState state,
|
||||
const std::optional<std::string>& expected_value) {
|
||||
if (node == nullptr)
|
||||
return false;
|
||||
switch (state) {
|
||||
case WaitState::Exists: return true;
|
||||
case WaitState::Visible: return node->visible;
|
||||
case WaitState::Enabled: return node->enabled && node->visible;
|
||||
case WaitState::Value:
|
||||
return node->has_value && expected_value && node->value == *expected_value;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}}} // namespace
|
||||
37
src/slic3r/GUI/Automation/Locator.hpp
Normal file
37
src/slic3r/GUI/Automation/Locator.hpp
Normal file
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
#include "IUiBackend.hpp"
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r { namespace GUI { namespace Automation {
|
||||
|
||||
// A target specification. Resolution order: id -> path -> predicate
|
||||
// (name OR class OR label OR value, all provided fields must match).
|
||||
struct Target {
|
||||
std::optional<std::string> id;
|
||||
std::optional<std::string> path;
|
||||
std::optional<std::string> name; // matches id OR label
|
||||
std::optional<std::string> klass;
|
||||
std::optional<std::string> label;
|
||||
std::optional<std::string> value;
|
||||
std::optional<BackendKind> backend;
|
||||
bool empty() const {
|
||||
return !id && !path && !name && !klass && !label && !value;
|
||||
}
|
||||
};
|
||||
|
||||
// Depth-first flatten of a tree into stable-ordered pointers (parents before children).
|
||||
std::vector<const UiNode*> flatten(const UiNode& root);
|
||||
|
||||
// All nodes matching the target spec (resolution-order aware).
|
||||
std::vector<const UiNode*> find_matches(const UiNode& root, const Target& target);
|
||||
|
||||
enum class WaitState { Exists, Visible, Enabled, Value };
|
||||
|
||||
// True if `node` satisfies the wait condition. A null node only satisfies a
|
||||
// negative... here we keep it simple: null => false for all states.
|
||||
bool evaluate_state(const UiNode* node, WaitState state,
|
||||
const std::optional<std::string>& expected_value);
|
||||
|
||||
}}} // namespace
|
||||
@@ -3,7 +3,9 @@ get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
|
||||
add_executable(${_TEST_NAME}_tests
|
||||
automation_tests.cpp
|
||||
test_serializer.cpp
|
||||
test_locator.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/slic3r/GUI/Automation/WidgetSerializer.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/slic3r/GUI/Automation/Locator.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
|
||||
|
||||
92
tests/automation/test_locator.cpp
Normal file
92
tests/automation/test_locator.cpp
Normal file
@@ -0,0 +1,92 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
#include "slic3r/GUI/Automation/Locator.hpp"
|
||||
|
||||
using namespace Slic3r::GUI::Automation;
|
||||
|
||||
namespace {
|
||||
UiNode make_tree() {
|
||||
UiNode root;
|
||||
root.klass = "MainFrame";
|
||||
root.path = "MainFrame";
|
||||
|
||||
UiNode panel;
|
||||
panel.klass = "Panel";
|
||||
panel.path = "MainFrame/Panel[0]";
|
||||
|
||||
UiNode slice;
|
||||
slice.id = "btn_slice";
|
||||
slice.klass = "Button";
|
||||
slice.label = "Slice plate";
|
||||
slice.path = "MainFrame/Panel[0]/Button[0]";
|
||||
|
||||
UiNode export_btn;
|
||||
export_btn.id = "btn_export";
|
||||
export_btn.klass = "Button";
|
||||
export_btn.label = "Export";
|
||||
export_btn.path = "MainFrame/Panel[0]/Button[1]";
|
||||
|
||||
UiNode dup; // duplicate label, used for ambiguity tests
|
||||
dup.klass = "Button";
|
||||
dup.label = "Export";
|
||||
dup.path = "MainFrame/Panel[0]/Button[2]";
|
||||
|
||||
panel.children = {slice, export_btn, dup};
|
||||
root.children = {panel};
|
||||
return root;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("flatten yields parents before children", "[automation][locator]") {
|
||||
const auto tree = make_tree();
|
||||
const auto all = flatten(tree);
|
||||
REQUIRE(all.size() == 5);
|
||||
CHECK(all.front()->klass == "MainFrame");
|
||||
}
|
||||
|
||||
TEST_CASE("find_matches by exact id returns one", "[automation][locator]") {
|
||||
const auto tree = make_tree();
|
||||
Target t; t.id = "btn_slice";
|
||||
const auto m = find_matches(tree, t);
|
||||
REQUIRE(m.size() == 1);
|
||||
CHECK(m[0]->label == "Slice plate");
|
||||
}
|
||||
|
||||
TEST_CASE("find_matches by exact path returns one", "[automation][locator]") {
|
||||
const auto tree = make_tree();
|
||||
Target t; t.path = "MainFrame/Panel[0]/Button[1]";
|
||||
const auto m = find_matches(tree, t);
|
||||
REQUIRE(m.size() == 1);
|
||||
CHECK(m[0]->id == "btn_export");
|
||||
}
|
||||
|
||||
TEST_CASE("find_matches by predicate (label) can be ambiguous",
|
||||
"[automation][locator]") {
|
||||
const auto tree = make_tree();
|
||||
Target t; t.label = "Export";
|
||||
const auto m = find_matches(tree, t);
|
||||
CHECK(m.size() == 2); // btn_export + the duplicate
|
||||
}
|
||||
|
||||
TEST_CASE("find_matches predicate combines fields (AND)",
|
||||
"[automation][locator]") {
|
||||
const auto tree = make_tree();
|
||||
Target t; t.label = "Export"; t.klass = "Button"; t.id = std::nullopt;
|
||||
// id/path absent -> predicate mode. Both fields must match.
|
||||
t.id = std::nullopt;
|
||||
const auto m = find_matches(tree, t);
|
||||
CHECK(m.size() == 2);
|
||||
}
|
||||
|
||||
TEST_CASE("find_matches by name matches id OR label", "[automation][locator]") {
|
||||
const auto tree = make_tree();
|
||||
Target byId; byId.name = "btn_slice";
|
||||
CHECK(find_matches(tree, byId).size() == 1);
|
||||
Target byLabel; byLabel.name = "Slice plate";
|
||||
CHECK(find_matches(tree, byLabel).size() == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("find_matches not found returns empty", "[automation][locator]") {
|
||||
const auto tree = make_tree();
|
||||
Target t; t.id = "nope";
|
||||
CHECK(find_matches(tree, t).empty());
|
||||
}
|
||||
Reference in New Issue
Block a user