mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-27 19:01:02 +00:00
fix: send the AMS tray's own coordinates on tray selection
command_ams_select_tray split the BBL tray id into ams_id/slot_id inline and untested. Extract build_ams_change_filament_body so the mapping is a named unit that command_ams_select_tray routes verbatim, and lock it with a test: tray 9 (a 6-slot box's slot 5) sends (2, 1), never a fabricated flat lane.
This commit is contained in:
@@ -7,6 +7,8 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_filament_bitmap_utils.cpp
|
||||
test_device_progress.cpp
|
||||
test_device_manager.cpp
|
||||
test_device_manager_integration.cpp
|
||||
test_web_media_controller.cpp
|
||||
test_network_versions.cpp
|
||||
test_action_source.cpp
|
||||
test_plugin_host_api.cpp
|
||||
@@ -20,11 +22,13 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_orca_printer_agent.cpp
|
||||
test_plugin_install.cpp
|
||||
test_plugin_lifecycle.cpp
|
||||
test_plugin_printer_agent.cpp
|
||||
test_slicing_pipeline_bindings.cpp
|
||||
test_slicing_pipeline_config.cpp
|
||||
test_plugin_sort.cpp
|
||||
test_plugin_cloud_metadata.cpp
|
||||
test_plugin_audit.cpp
|
||||
test_shortcuts.cpp
|
||||
../fff_print/test_helpers.cpp
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <slic3r/GUI/DeviceCore/DevManager.h>
|
||||
#include <slic3r/GUI/DeviceManager.hpp>
|
||||
#include <libslic3r/AppConfig.hpp>
|
||||
#include <slic3r/Utils/NetworkAgent.hpp>
|
||||
#include <slic3r/Utils/OrcaCloudServiceAgent.hpp>
|
||||
#include <slic3r/Utils/OrcaPrinterAgent.hpp>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <memory>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
using namespace Slic3r;
|
||||
using json = nlohmann::json;
|
||||
|
||||
namespace {
|
||||
|
||||
class StubCloudAgent final : public OrcaCloudServiceAgent
|
||||
{
|
||||
public:
|
||||
StubCloudAgent() : OrcaCloudServiceAgent("") {}
|
||||
|
||||
int get_user_print_info(unsigned int* http_code, std::string* http_body) override
|
||||
{
|
||||
if (http_code)
|
||||
*http_code = 200;
|
||||
if (http_body)
|
||||
*http_body = R"({"devices":[]})";
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string get_user_name() override { return "integration-test-user"; }
|
||||
};
|
||||
|
||||
class TestPrinterAgent final : public OrcaPrinterAgent
|
||||
{
|
||||
public:
|
||||
explicit TestPrinterAgent(std::string id)
|
||||
: OrcaPrinterAgent(""), m_info{std::move(id), "Integration Test Agent", "1.0", "test agent"}
|
||||
{
|
||||
}
|
||||
|
||||
AgentInfo get_agent_info() override { return m_info; }
|
||||
|
||||
private:
|
||||
AgentInfo m_info;
|
||||
};
|
||||
|
||||
struct ScopedAppConfig
|
||||
{
|
||||
AppConfig config;
|
||||
};
|
||||
|
||||
std::string machine_list_response(const std::string& provider, const std::string& agent_id,
|
||||
std::uint64_t generation, const std::string& name)
|
||||
{
|
||||
json machine;
|
||||
machine["dev_id"] = "integration-device";
|
||||
machine["dev_name"] = name;
|
||||
machine["dev_online"] = true;
|
||||
machine["task_status"] = "idle";
|
||||
|
||||
json response;
|
||||
response["provider"] = provider;
|
||||
response["agent_id"] = agent_id;
|
||||
response["generation"] = generation;
|
||||
response["devices"] = json::array({machine});
|
||||
return response.dump();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("Network agent stamps user-machine responses with request context", "[DeviceManager][integration]")
|
||||
{
|
||||
auto cloud = std::make_shared<StubCloudAgent>();
|
||||
NetworkAgent network(cloud, nullptr);
|
||||
network.set_printer_agent(std::make_shared<TestPrinterAgent>("integration-agent-a"));
|
||||
|
||||
const std::uint64_t generation_before = network.get_user_machine_list_generation();
|
||||
unsigned int http_code = 0;
|
||||
std::string body;
|
||||
REQUIRE(network.get_user_print_info(&http_code, &body, ORCA_CLOUD_PROVIDER) == 0);
|
||||
|
||||
const json response = json::parse(body);
|
||||
CHECK(http_code == 200);
|
||||
CHECK(response["provider"] == ORCA_CLOUD_PROVIDER);
|
||||
CHECK(response["agent_id"] == "integration-agent-a");
|
||||
CHECK(response["generation"] == generation_before + 1);
|
||||
}
|
||||
|
||||
TEST_CASE("Device manager ignores stale cloud machine responses", "[DeviceManager][integration]")
|
||||
{
|
||||
ScopedAppConfig app_config;
|
||||
NetworkAgent network(nullptr, std::make_shared<TestPrinterAgent>("integration-agent"));
|
||||
network.set_printer_agent(std::make_shared<TestPrinterAgent>("integration-agent"));
|
||||
DeviceManager manager(&network, false, &app_config.config);
|
||||
|
||||
const std::uint64_t current_generation = network.get_user_machine_list_generation();
|
||||
manager.parse_user_print_info(machine_list_response(ORCA_CLOUD_PROVIDER, "integration-agent",
|
||||
current_generation, "Fresh name"));
|
||||
|
||||
auto machines = manager.get_user_machinelist();
|
||||
REQUIRE(machines.size() == 1);
|
||||
REQUIRE(machines.at("integration-device") != nullptr);
|
||||
CHECK(machines.at("integration-device")->get_dev_name() == "Fresh name");
|
||||
|
||||
manager.parse_user_print_info(machine_list_response(BBL_CLOUD_PROVIDER, "integration-agent",
|
||||
current_generation, "Stale provider"));
|
||||
manager.parse_user_print_info(machine_list_response(ORCA_CLOUD_PROVIDER, "other-agent",
|
||||
current_generation, "Stale agent"));
|
||||
manager.parse_user_print_info(machine_list_response(ORCA_CLOUD_PROVIDER, "integration-agent",
|
||||
current_generation + 1, "Stale generation"));
|
||||
|
||||
machines = manager.get_user_machinelist();
|
||||
REQUIRE(machines.size() == 1);
|
||||
CHECK(machines.at("integration-device")->get_dev_name() == "Fresh name");
|
||||
}
|
||||
|
||||
TEST_CASE("Device manager filters and rehomes devices by printer-agent ownership", "[DeviceManager][integration]")
|
||||
{
|
||||
ScopedAppConfig app_config;
|
||||
auto agent_a = std::make_shared<TestPrinterAgent>("integration-agent-a");
|
||||
auto agent_b = std::make_shared<TestPrinterAgent>("integration-agent-b");
|
||||
NetworkAgent network(nullptr, agent_a);
|
||||
DeviceManager manager(&network, false, &app_config.config);
|
||||
|
||||
BBLocalMachine machine;
|
||||
machine.dev_id = "integration-lan-device";
|
||||
machine.dev_name = "Integration LAN device";
|
||||
machine.dev_ip = "192.0.2.10";
|
||||
machine.printer_type = "C11";
|
||||
|
||||
MachineObject* object = manager.insert_local_device(machine, "lan", "free", "", "access-code");
|
||||
REQUIRE(object != nullptr);
|
||||
CHECK(object->printer_agent_id == "integration-agent-a");
|
||||
CHECK(manager.get_my_machine_list("integration-agent-a").count(machine.dev_id) == 1);
|
||||
CHECK(manager.get_my_machine_list("integration-agent-b").empty());
|
||||
|
||||
network.set_printer_agent(agent_b);
|
||||
CHECK(manager.get_my_machine_list("integration-agent-b").empty());
|
||||
|
||||
manager.on_machine_alive(R"({
|
||||
"dev_name":"Rediscovered device",
|
||||
"dev_id":"integration-lan-device",
|
||||
"dev_ip":"192.0.2.10",
|
||||
"dev_type":"C11",
|
||||
"dev_signal":"strong",
|
||||
"connect_type":"lan",
|
||||
"bind_state":"free"
|
||||
})");
|
||||
|
||||
CHECK(object->printer_agent_id == "integration-agent-b");
|
||||
CHECK(manager.get_my_machine_list("integration-agent-a").empty());
|
||||
CHECK(manager.get_my_machine_list("integration-agent-b").count(machine.dev_id) == 1);
|
||||
}
|
||||
@@ -115,7 +115,7 @@ TEST_CASE("Malformed string progress leaves a fresh machine unchanged", "[Device
|
||||
MachineObject machine(nullptr, nullptr, "test", "test-device", "127.0.0.1");
|
||||
REQUIRE(machine.subtask_ == nullptr);
|
||||
|
||||
CHECK_THROWS_AS(machine.update_print_progress(json("not-a-percent")), std::invalid_argument);
|
||||
CHECK_NOTHROW(machine.update_print_progress(json("not-a-percent")));
|
||||
CHECK(machine.mc_print_percent == 0);
|
||||
CHECK(machine.subtask_ == nullptr);
|
||||
}
|
||||
|
||||
@@ -423,3 +423,21 @@ TEST_CASE("a filament_slots reply admits ams_filament_setting without ams_ops",
|
||||
&unsupported);
|
||||
CHECK(unsupported);
|
||||
}
|
||||
|
||||
// A box wider than 4 slots is shown as several 4-tray units, so the BBL tray id
|
||||
// a panel reports must split into that unit's own (ams_id, slot_id). A flat lane
|
||||
// would be the tray id, which is not the layout lane for a wide box
|
||||
// (REQ-STS-008 §7.8).
|
||||
TEST_CASE("an AMS tray selection sends the tray's ams_id and slot_id", "[OrcaPrinterAgent]") {
|
||||
// Unit 2 tray 1 is BBL tray 9: a 6-slot box's slot 5 addressed as (2, 1).
|
||||
auto body = nlohmann::json::parse(OrcaPrinterAgent::build_ams_change_filament_body(9, 123));
|
||||
CHECK(body["print"]["selector"] == "lane");
|
||||
CHECK(body["print"]["ams_id"] == 2);
|
||||
CHECK(body["print"]["slot_id"] == 1);
|
||||
CHECK_FALSE(body["print"].contains("lane"));
|
||||
|
||||
// Unit 0 tray 3 is tray 3.
|
||||
body = nlohmann::json::parse(OrcaPrinterAgent::build_ams_change_filament_body(3, 124));
|
||||
CHECK(body["print"]["ams_id"] == 0);
|
||||
CHECK(body["print"]["slot_id"] == 3);
|
||||
}
|
||||
|
||||
@@ -3,8 +3,12 @@
|
||||
#include <libslic3r/Model.hpp>
|
||||
#include <libslic3r/PresetBundle.hpp>
|
||||
#include <libslic3r/TriangleMesh.hpp>
|
||||
#include <slic3r/GUI/DockPanel.hpp>
|
||||
#include <slic3r/GUI/AuiPaneLayout.hpp>
|
||||
#include <slic3r/GUI/Widgets/WebHosting.hpp>
|
||||
#include <slic3r/plugin/PythonPluginBridge.hpp>
|
||||
|
||||
#include "plugin_test_utils.hpp"
|
||||
#include "python_test_support.hpp"
|
||||
|
||||
#include <pybind11/embed.h>
|
||||
@@ -141,6 +145,8 @@ TEST_CASE("Plugin host API exposes the UI module and guards it before Orca app i
|
||||
CHECK(ui.attr("WINDOW_MODELESS").cast<long>() == 0);
|
||||
CHECK(ui.attr("WINDOW_MODAL").cast<long>() == 1);
|
||||
CHECK(has_attr(ui, "UiWindow"));
|
||||
CHECK(has_attr(ui, "create_dock_panel"));
|
||||
CHECK(has_attr(ui, "UiDockPanel"));
|
||||
|
||||
// With no wx application the UI calls marshal to a main thread that does not
|
||||
// exist here; they must fail cleanly with a clear error, not crash.
|
||||
@@ -151,6 +157,74 @@ TEST_CASE("Plugin host API exposes the UI module and guards it before Orca app i
|
||||
CHECK(error.matches(PyExc_RuntimeError));
|
||||
CHECK(std::string(error.what()).find("OrcaSlicer application is not initialized") != std::string::npos);
|
||||
}
|
||||
|
||||
try {
|
||||
ui.attr("create_dock_panel")("<p>panel</p>");
|
||||
FAIL("orca.host.ui.create_dock_panel unexpectedly succeeded without a wx application");
|
||||
} catch (const py::error_already_set& error) {
|
||||
CHECK(error.matches(PyExc_RuntimeError));
|
||||
CHECK(std::string(error.what()).find("OrcaSlicer application is not initialized") != std::string::npos);
|
||||
}
|
||||
|
||||
// Positional arguments follow create_window(): width and height come straight after the title.
|
||||
try {
|
||||
ui.attr("create_dock_panel")("<p>panel</p>", "Panel", 400, 300);
|
||||
FAIL("orca.host.ui.create_dock_panel unexpectedly succeeded without a wx application");
|
||||
} catch (const py::error_already_set& error) {
|
||||
CHECK(error.matches(PyExc_RuntimeError));
|
||||
}
|
||||
|
||||
// An unknown dock position is rejected before the application is needed.
|
||||
try {
|
||||
ui.attr("create_dock_panel")("<p>panel</p>", py::arg("dock") = "top");
|
||||
FAIL("orca.host.ui.create_dock_panel accepted an unknown dock position");
|
||||
} catch (const py::error_already_set& error) {
|
||||
CHECK(error.matches(PyExc_ValueError));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Plugin pane names identify the plugin and title without layout delimiters", "[PluginHost]")
|
||||
{
|
||||
using Slic3r::GUI::plugin_pane_name;
|
||||
|
||||
CHECK(plugin_pane_name("dock_demo", "Scene") == "plugin:dock_demo:Scene");
|
||||
CHECK(plugin_pane_name("key", "a|b;c=d\\e").find_first_of("|;=\\") == std::string::npos);
|
||||
}
|
||||
|
||||
TEST_CASE("A pane's saved layout entry is found by pane name", "[PluginHost]")
|
||||
{
|
||||
using Slic3r::GUI::aui_pane_layout_entry;
|
||||
|
||||
const std::string sidebar = "name=sidebar;caption=;state=2099196;dir=4;layer=0;row=0;pos=0;bestw=390;besth=900";
|
||||
// The caption holds an escaped '|', which must not end the entry.
|
||||
const std::string plugin = "name=plugin:demo:Scene;caption=Scene \\| stats;state=2099198;dir=2;layer=0;row=1;pos=0;bestw=320;besth=480";
|
||||
const std::string layout = "layout3|" + sidebar + "|" + plugin + "|dock_size(4,0,0)=392|";
|
||||
|
||||
CHECK(aui_pane_layout_entry(layout, "plugin:demo:Scene") == plugin);
|
||||
CHECK(aui_pane_layout_entry(layout, "sidebar") == sidebar);
|
||||
CHECK(aui_pane_layout_entry(layout, "plugin:demo").empty());
|
||||
CHECK(aui_pane_layout_entry("", "plugin:demo:Scene").empty());
|
||||
}
|
||||
|
||||
TEST_CASE("A reloaded plugin page is recognised by its base URL, fragment aside", "[PluginHost]")
|
||||
{
|
||||
using namespace Slic3r::GUI::web_hosting;
|
||||
|
||||
// A resources path holding a space, which the web view reports escaped.
|
||||
const Slic3r::ScopedResourcesDir resources("web content check");
|
||||
|
||||
// The swapped-in page, then after an in-page anchor and a reload.
|
||||
CHECK(is_content_url(content_base_url()));
|
||||
CHECK(is_content_url(content_base_url() + "#tab2"));
|
||||
wxString escaped = content_base_url();
|
||||
escaped.Replace(" ", "%20");
|
||||
REQUIRE(escaped != content_base_url());
|
||||
CHECK(is_content_url(escaped));
|
||||
CHECK(is_content_url(escaped + "#tab2"));
|
||||
// A page the plugin linked to keeps its own URL and must be left alone.
|
||||
CHECK_FALSE(is_content_url(content_base_url() + "guide.html"));
|
||||
CHECK_FALSE(is_content_url("https://example.com/"));
|
||||
CHECK_FALSE(is_content_url(""));
|
||||
}
|
||||
|
||||
TEST_CASE("Plugin host API exposes model geometry and structure to Python", "[PluginHost][Python]")
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <slic3r/plugin/PluginManager.hpp>
|
||||
#include <slic3r/plugin/PythonInterpreter.hpp>
|
||||
#include <slic3r/plugin/PythonPluginBridge.hpp>
|
||||
#include <slic3r/plugin/PythonPluginInterface.hpp>
|
||||
#include <slic3r/Utils/IPrinterAgent.hpp>
|
||||
|
||||
#include <pybind11/embed.h>
|
||||
#include <pybind11/pybind11.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace py = pybind11;
|
||||
using namespace Slic3r;
|
||||
|
||||
namespace {
|
||||
|
||||
// Same idiom as ScopedPluginManager in test_plugin_lifecycle.cpp: the trampolines refuse to call
|
||||
// into Python unless PythonInterpreter::instance() reports initialized.
|
||||
struct ScopedPluginManager
|
||||
{
|
||||
bool initialized = PluginManager::instance().initialize();
|
||||
|
||||
~ScopedPluginManager()
|
||||
{
|
||||
PluginManager::instance().shutdown();
|
||||
PythonInterpreter::instance().shutdown();
|
||||
}
|
||||
};
|
||||
|
||||
// The host reaches a printer agent plugin through IPrinterAgent, so the tests do too. The Python
|
||||
// instance carries the overrides, so it has to outlive every call, as PluginInstanceHandle ensures
|
||||
// in production.
|
||||
struct Agent
|
||||
{
|
||||
py::object instance;
|
||||
std::shared_ptr<IPrinterAgent> agent;
|
||||
|
||||
IPrinterAgent* operator->() const { return agent.get(); }
|
||||
IPrinterAgent& operator*() const { return *agent; }
|
||||
};
|
||||
|
||||
Agent make_agent(const std::string& body)
|
||||
{
|
||||
(void) PythonPluginBridge::instance(); // force the embedded module registration into the binary
|
||||
py::dict globals;
|
||||
globals["orca"] = py::module_::import("orca");
|
||||
|
||||
py::exec("class Agent(orca.printer_agent.PrinterAgentBase):\n"
|
||||
" def get_name(self): return 'agent'\n" + body, globals);
|
||||
py::object instance = globals["Agent"]();
|
||||
auto capability = instance.cast<std::shared_ptr<PluginCapabilityInterface>>();
|
||||
capability->set_audit_plugin_key("agent_plugin");
|
||||
return {instance, std::dynamic_pointer_cast<IPrinterAgent>(capability)};
|
||||
}
|
||||
|
||||
// One operation per return type and dispatch shape; the rest share their macro.
|
||||
const std::string OPERATIONS[] = {"get_agent_info", "disconnect_printer", "start_discovery", "get_user_selected_machine",
|
||||
"get_filament_sync_mode", "install_device_cert", "start_local_print", "request_bind_ticket"};
|
||||
|
||||
// What NetworkAgent answers when no printer agent is set.
|
||||
void check_answers_like_no_agent(IPrinterAgent& agent)
|
||||
{
|
||||
std::string ticket = "untouched";
|
||||
|
||||
CHECK(agent.get_agent_info().id.empty());
|
||||
CHECK(agent.disconnect_printer() == -1);
|
||||
CHECK_FALSE(agent.start_discovery(true, false));
|
||||
CHECK(agent.get_user_selected_machine().empty());
|
||||
CHECK(agent.get_filament_sync_mode() == FilamentSyncMode::none);
|
||||
CHECK_NOTHROW(agent.install_device_cert("dev", true));
|
||||
CHECK(agent.start_local_print(PrintParams{}, nullptr, nullptr) == -1);
|
||||
CHECK(agent.request_bind_ticket(&ticket) == -1);
|
||||
CHECK(ticket == "untouched");
|
||||
}
|
||||
|
||||
std::string define_all(const std::string& signature_tail, const std::string& statement)
|
||||
{
|
||||
std::string body;
|
||||
for (const std::string& operation : OPERATIONS)
|
||||
body += " def " + operation + "(self" + signature_tail + "): " + statement + "\n";
|
||||
return body;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("A printer agent operation that raises answers like a missing agent", "[PluginPrinterAgent][Python]")
|
||||
{
|
||||
ScopedPluginManager plugin_system; // declared first: destroyed last
|
||||
if (!plugin_system.initialized)
|
||||
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||
py::gil_scoped_acquire gil; // released before plugin_system's destructor shuts Python down
|
||||
|
||||
auto agent = make_agent(define_all(", *args", "raise RuntimeError('boom')"));
|
||||
REQUIRE(agent.agent);
|
||||
|
||||
check_answers_like_no_agent(*agent);
|
||||
|
||||
// The interpreter stays usable.
|
||||
CHECK(py::eval("1 + 1").cast<int>() == 2);
|
||||
}
|
||||
|
||||
TEST_CASE("A printer agent that omits its operations answers like a missing agent", "[PluginPrinterAgent][Python]")
|
||||
{
|
||||
ScopedPluginManager plugin_system;
|
||||
if (!plugin_system.initialized)
|
||||
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||
py::gil_scoped_acquire gil;
|
||||
|
||||
auto agent = make_agent("");
|
||||
REQUIRE(agent.agent);
|
||||
|
||||
check_answers_like_no_agent(*agent);
|
||||
}
|
||||
|
||||
TEST_CASE("A printer agent uses IPrinterAgent defaults for omitted commands", "[PluginPrinterAgent][Python]")
|
||||
{
|
||||
ScopedPluginManager plugin_system;
|
||||
if (!plugin_system.initialized)
|
||||
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||
py::gil_scoped_acquire gil;
|
||||
|
||||
auto agent = make_agent(" def send_message(self, dev_id, json_str, qos, flag): return 7\n"
|
||||
" def send_message_to_printer(self, dev_id, json_str, qos, flag): return 8\n");
|
||||
REQUIRE(agent.agent);
|
||||
|
||||
CHECK(agent->command_xyz_abs("dev", 1, false) == 7);
|
||||
CHECK(agent->command_set_nozzle("dev", 200, 2, true) == 8);
|
||||
CHECK(agent->command_ams_refresh_rfid("dev", "tray", 3, false) == ORCA_NETWORK_ERR_CMD_NOT_SUPPORTED);
|
||||
}
|
||||
|
||||
TEST_CASE("A printer agent operation returning the wrong type answers like a missing agent", "[PluginPrinterAgent][Python]")
|
||||
{
|
||||
ScopedPluginManager plugin_system;
|
||||
if (!plugin_system.initialized)
|
||||
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||
py::gil_scoped_acquire gil;
|
||||
|
||||
auto agent = make_agent(define_all(", *args", "return object()"));
|
||||
REQUIRE(agent.agent);
|
||||
|
||||
check_answers_like_no_agent(*agent);
|
||||
}
|
||||
|
||||
TEST_CASE("A working printer agent's answers reach the host unchanged", "[PluginPrinterAgent][Python]")
|
||||
{
|
||||
ScopedPluginManager plugin_system;
|
||||
if (!plugin_system.initialized)
|
||||
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
|
||||
py::gil_scoped_acquire gil;
|
||||
|
||||
auto agent = make_agent(" def get_agent_info(self): return orca.printer_agent.AgentInfo('id', 'name', '1', 'description')\n"
|
||||
" def disconnect_printer(self): return 7\n"
|
||||
" def start_discovery(self, start, sending): return start and not sending\n"
|
||||
" def get_user_selected_machine(self): return 'machine'\n"
|
||||
" def get_filament_sync_mode(self): return orca.printer_agent.FilamentSyncMode.Pull\n"
|
||||
" def request_bind_ticket(self): return (3, 'ticket')\n"
|
||||
" def bind_detect(self, dev_ip, sec_link, detect):\n"
|
||||
" detect.dev_id = dev_ip\n"
|
||||
" return 0\n");
|
||||
REQUIRE(agent.agent);
|
||||
|
||||
std::string ticket;
|
||||
detectResult detect;
|
||||
|
||||
CHECK(agent->get_agent_info().id == "id");
|
||||
CHECK(agent->disconnect_printer() == 7);
|
||||
CHECK(agent->start_discovery(true, false));
|
||||
CHECK(agent->get_user_selected_machine() == "machine");
|
||||
CHECK(agent->get_filament_sync_mode() == FilamentSyncMode::pull);
|
||||
CHECK(agent->request_bind_ticket(&ticket) == 3);
|
||||
CHECK(ticket == "ticket");
|
||||
CHECK(agent->bind_detect("192.168.0.2", "secure", detect) == 0);
|
||||
CHECK(detect.dev_id == "192.168.0.2");
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/generators/catch_generators.hpp>
|
||||
|
||||
#include "libslic3r/AppConfig.hpp"
|
||||
#include "slic3r/GUI/KeyChord.hpp"
|
||||
#include "slic3r/GUI/Shortcuts.hpp"
|
||||
|
||||
#include <wx/event.h>
|
||||
|
||||
using namespace Slic3r;
|
||||
using namespace Slic3r::GUI;
|
||||
|
||||
namespace {
|
||||
|
||||
wxKeyEvent key_event(wxEventType type, int key_code, int modifiers = wxMOD_NONE)
|
||||
{
|
||||
wxKeyEvent evt(type);
|
||||
evt.m_keyCode = key_code;
|
||||
evt.SetControlDown(modifiers & wxMOD_CONTROL);
|
||||
evt.SetShiftDown(modifiers & wxMOD_SHIFT);
|
||||
evt.SetAltDown(modifiers & wxMOD_ALT);
|
||||
return evt;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("KeyChord round-trips through its canonical text", "[Shortcuts]")
|
||||
{
|
||||
const auto [chord, text] = GENERATE(table<KeyChord, std::string>({
|
||||
{ { 'N', wxMOD_CONTROL }, "Ctrl+N" },
|
||||
{ { 'S', wxMOD_CONTROL | wxMOD_SHIFT }, "Ctrl+Shift+S" },
|
||||
{ { WXK_RETURN, wxMOD_SHIFT | wxMOD_ALT }, "Shift+Alt+Enter" },
|
||||
{ { WXK_TAB, wxMOD_SHIFT }, "Shift+Tab" },
|
||||
{ { WXK_DELETE }, "Del" },
|
||||
{ { WXK_F5 }, "F5" },
|
||||
{ { WXK_F12, wxMOD_CONTROL }, "Ctrl+F12" },
|
||||
{ { '+' }, "+" },
|
||||
{ { '-', wxMOD_CONTROL }, "Ctrl+-" },
|
||||
{ { '?' }, "?" },
|
||||
{ { ',', wxMOD_CONTROL }, "Ctrl+," },
|
||||
}));
|
||||
CAPTURE(text);
|
||||
CHECK(chord.to_string() == text);
|
||||
REQUIRE(KeyChord::parse(text).has_value());
|
||||
CHECK(*KeyChord::parse(text) == chord);
|
||||
}
|
||||
|
||||
TEST_CASE("KeyChord::parse accepts aliases and rejects malformed text", "[Shortcuts]")
|
||||
{
|
||||
CHECK(KeyChord::parse("control+n") == KeyChord{ 'N', wxMOD_CONTROL });
|
||||
CHECK(KeyChord::parse("Cmd+Shift+Delete") == KeyChord{ WXK_DELETE, wxMOD_CONTROL | wxMOD_SHIFT });
|
||||
CHECK(KeyChord::parse("PageUp") == KeyChord{ WXK_PAGEUP });
|
||||
CHECK(KeyChord::parse("f3") == KeyChord{ WXK_F3 });
|
||||
|
||||
CHECK_FALSE(KeyChord::parse("").has_value());
|
||||
CHECK_FALSE(KeyChord::parse("Ctrl+").has_value());
|
||||
CHECK_FALSE(KeyChord::parse("Meta+A").has_value());
|
||||
CHECK_FALSE(KeyChord::parse("F25").has_value());
|
||||
CHECK_FALSE(KeyChord::parse("Shift+/").has_value()); // Shift is part of the punctuation character
|
||||
}
|
||||
|
||||
TEST_CASE("Key events normalize to the key-down key codes", "[Shortcuts]")
|
||||
{
|
||||
CHECK(KeyChord::from_event(key_event(wxEVT_KEY_DOWN, 'A', wxMOD_CONTROL)) == KeyChord{ 'A', wxMOD_CONTROL });
|
||||
CHECK(KeyChord::from_event(key_event(wxEVT_KEY_DOWN, WXK_NUMPAD5, wxMOD_CONTROL)) == KeyChord{ '5', wxMOD_CONTROL });
|
||||
CHECK(KeyChord::from_event(key_event(wxEVT_KEY_DOWN, WXK_NUMPAD_ADD)) == KeyChord{ '+' });
|
||||
CHECK(KeyChord::from_event(key_event(wxEVT_KEY_DOWN, WXK_NUMPAD_PAGEUP)) == KeyChord{ WXK_PAGEUP });
|
||||
CHECK_FALSE(KeyChord::from_event(key_event(wxEVT_KEY_DOWN, WXK_SHIFT, wxMOD_SHIFT)).valid());
|
||||
CHECK_FALSE(KeyChord::from_event(key_event(wxEVT_KEY_DOWN, WXK_CONTROL, wxMOD_CONTROL)).valid());
|
||||
|
||||
CHECK(KeyChord::from_event(key_event(wxEVT_CHAR, 'a')) == KeyChord{ 'A' });
|
||||
CHECK(KeyChord::from_event(key_event(wxEVT_CHAR, 'A', wxMOD_SHIFT)) == KeyChord{ 'A', wxMOD_SHIFT });
|
||||
CHECK(KeyChord::from_event(key_event(wxEVT_CHAR, WXK_CONTROL_C, wxMOD_CONTROL)) == KeyChord{ 'C', wxMOD_CONTROL });
|
||||
CHECK(KeyChord::from_event(key_event(wxEVT_CHAR, '+', wxMOD_SHIFT)) == KeyChord{ '+' });
|
||||
CHECK(KeyChord::from_event(key_event(wxEVT_CHAR, WXK_DELETE)) == KeyChord{ WXK_DELETE });
|
||||
CHECK_FALSE(KeyChord::from_event(key_event(wxEVT_CHAR, 0x444)).valid()); // a Cyrillic letter is not bindable
|
||||
}
|
||||
|
||||
TEST_CASE("Punctuation chords are the ones matched on char events", "[Shortcuts]")
|
||||
{
|
||||
CHECK(KeyChord{ '+' }.is_punctuation());
|
||||
CHECK(KeyChord{ '?' }.is_punctuation());
|
||||
CHECK_FALSE(KeyChord{ 'A' }.is_punctuation());
|
||||
CHECK_FALSE(KeyChord{ '1' }.is_punctuation());
|
||||
CHECK_FALSE(KeyChord{ '=', wxMOD_CONTROL }.is_punctuation());
|
||||
CHECK_FALSE(KeyChord{ WXK_DELETE }.is_punctuation());
|
||||
}
|
||||
|
||||
TEST_CASE("Chords that only the char event can resolve are recognized", "[Shortcuts]")
|
||||
{
|
||||
CHECK(KeyChord{ '/', wxMOD_SHIFT }.needs_char_event());
|
||||
CHECK(KeyChord{ '-' }.needs_char_event());
|
||||
CHECK_FALSE(KeyChord{ '=', wxMOD_CONTROL }.needs_char_event());
|
||||
CHECK_FALSE(KeyChord{ 'A', wxMOD_SHIFT }.needs_char_event());
|
||||
CHECK_FALSE(KeyChord{ '1' }.needs_char_event());
|
||||
CHECK_FALSE(KeyChord{ WXK_F5 }.needs_char_event());
|
||||
}
|
||||
|
||||
TEST_CASE("Only modified or non-printable chords qualify as menu accelerators", "[Shortcuts]")
|
||||
{
|
||||
CHECK(KeyChord{ 'N', wxMOD_CONTROL }.is_menu_accelerator());
|
||||
CHECK(KeyChord{ 'N', wxMOD_ALT }.is_menu_accelerator());
|
||||
CHECK(KeyChord{ WXK_DELETE }.is_menu_accelerator());
|
||||
CHECK(KeyChord{ WXK_F5 }.is_menu_accelerator());
|
||||
CHECK_FALSE(KeyChord{ 'A' }.is_menu_accelerator());
|
||||
CHECK_FALSE(KeyChord{ 'A', wxMOD_SHIFT }.is_menu_accelerator());
|
||||
CHECK_FALSE(KeyChord{ '?' }.is_menu_accelerator());
|
||||
CHECK_FALSE(KeyChord{ WXK_SPACE }.is_menu_accelerator());
|
||||
CHECK_FALSE(KeyChord{}.is_menu_accelerator());
|
||||
|
||||
ShortcutRegistry registry;
|
||||
CHECK(registry.accelerator(Shortcut::NewProject) == "Ctrl+N");
|
||||
#ifdef __APPLE__
|
||||
CHECK(registry.accelerator(Shortcut::DeleteSelected) == "Backspace");
|
||||
#else
|
||||
CHECK(registry.accelerator(Shortcut::DeleteSelected) == "Del");
|
||||
#endif
|
||||
CHECK(registry.accelerator(Shortcut::Arrange).empty());
|
||||
CHECK(registry.accelerator(Shortcut::ArrangePlate).empty());
|
||||
CHECK(registry.accelerator(Shortcut::KeyboardShortcuts).empty());
|
||||
}
|
||||
|
||||
TEST_CASE("Chords convert to wx accelerator entries", "[Shortcuts]")
|
||||
{
|
||||
const wxAcceleratorEntry entry = KeyChord{ 'S', wxMOD_CONTROL | wxMOD_SHIFT }.to_accelerator_entry(42);
|
||||
CHECK(entry.GetFlags() == (wxACCEL_CTRL | wxACCEL_SHIFT));
|
||||
CHECK(entry.GetKeyCode() == 'S');
|
||||
CHECK(entry.GetCommand() == 42);
|
||||
|
||||
const wxAcceleratorEntry bare = KeyChord{ WXK_BACK }.to_accelerator_entry(7);
|
||||
CHECK(bare.GetFlags() == wxACCEL_NORMAL);
|
||||
CHECK(bare.GetKeyCode() == WXK_BACK);
|
||||
}
|
||||
|
||||
#ifndef __APPLE__
|
||||
TEST_CASE("Display text matches the canonical text without translations", "[Shortcuts]")
|
||||
{
|
||||
CHECK(KeyChord{ WXK_DELETE, wxMOD_CONTROL | wxMOD_SHIFT }.display() == "Ctrl+Shift+Del");
|
||||
CHECK(KeyChord{ WXK_DELETE, wxMOD_CONTROL | wxMOD_SHIFT }.display_parts() == std::vector<std::string>{ "Ctrl", "Shift", "Del" });
|
||||
CHECK(KeyChord{ '+' }.display() == "+");
|
||||
CHECK(KeyChord{ WXK_UP, wxMOD_SHIFT }.display() == "Shift+Arrow Up"); // the arrows keep the old dialog's names
|
||||
CHECK(KeyChord{ WXK_UP, wxMOD_SHIFT }.to_string() == "Shift+Up");
|
||||
CHECK(KeyChord{}.display().empty());
|
||||
}
|
||||
#endif
|
||||
|
||||
TEST_CASE("Every shortcut is listed under the section of its table row", "[Shortcuts]")
|
||||
{
|
||||
CHECK(shortcut_section(Shortcut::NewProject) == ShortcutSection::Project);
|
||||
CHECK(shortcut_section(Shortcut::Publish3mf) == ShortcutSection::Project);
|
||||
CHECK(shortcut_section(Shortcut::SlicePlate) == ShortcutSection::SlicingAndPrinting);
|
||||
CHECK(shortcut_section(Shortcut::GizmoBrimEars) == ShortcutSection::Gizmos);
|
||||
CHECK(shortcut_section(Shortcut::MovesSliderEnd) == ShortcutSection::Sliders);
|
||||
CHECK(shortcut_section(Shortcut::ViewDefault) == ShortcutSection::Camera);
|
||||
CHECK(shortcut_section(Shortcut::KeyboardShortcuts) == ShortcutSection::Application);
|
||||
CHECK(std::string(section_name(ShortcutSection::SlicingAndPrinting)) == "Slicing and printing");
|
||||
}
|
||||
|
||||
TEST_CASE("Default bindings never collide inside a context", "[Shortcuts]")
|
||||
{
|
||||
ShortcutRegistry registry;
|
||||
for (size_t i = 0; i < size_t(Shortcut::Count); ++i) {
|
||||
const Shortcut shortcut = Shortcut(i);
|
||||
CAPTURE(shortcut_info(shortcut).key);
|
||||
CHECK(registry.conflicts(shortcut, registry.binding(shortcut)).empty());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Shift and Ctrl variants of stepping shortcuts are left unbound", "[Shortcuts]")
|
||||
{
|
||||
ShortcutRegistry registry;
|
||||
for (size_t i = 0; i < size_t(Shortcut::Count); ++i) {
|
||||
const ShortcutInfo& info = shortcut_info(Shortcut(i));
|
||||
if (!info.modifier_variants)
|
||||
continue;
|
||||
CAPTURE(info.key);
|
||||
const KeyChord chord = registry.binding(info.id);
|
||||
for (int modifier : { int(wxMOD_SHIFT), int(wxMOD_CONTROL), int(wxMOD_SHIFT | wxMOD_CONTROL) })
|
||||
for (size_t c = 0; c < size_t(ShortcutContext::Count); ++c)
|
||||
if (info.contexts & context_bit(ShortcutContext(c)))
|
||||
CHECK_FALSE(registry.lookup(ShortcutContext(c), KeyChord{ chord.key, chord.modifiers | modifier }).has_value());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Lookups are scoped to the context of the key press", "[Shortcuts]")
|
||||
{
|
||||
ShortcutRegistry registry;
|
||||
const KeyChord ctrl_n{ 'N', wxMOD_CONTROL };
|
||||
const KeyChord ctrl_c{ 'C', wxMOD_CONTROL };
|
||||
const KeyChord a{ 'A' };
|
||||
const KeyChord c{ 'C' };
|
||||
|
||||
CHECK(registry.lookup(ShortcutContext::Global, ctrl_n) == Shortcut::NewProject);
|
||||
CHECK_FALSE(registry.lookup(ShortcutContext::Plater, ctrl_n).has_value());
|
||||
|
||||
CHECK(registry.lookup(ShortcutContext::Plater, ctrl_c) == Shortcut::Copy);
|
||||
CHECK(registry.lookup(ShortcutContext::ObjectList, ctrl_c) == Shortcut::Copy);
|
||||
CHECK_FALSE(registry.lookup(ShortcutContext::Global, ctrl_c).has_value());
|
||||
|
||||
CHECK(registry.lookup(ShortcutContext::Plater, a) == Shortcut::Arrange);
|
||||
CHECK_FALSE(registry.lookup(ShortcutContext::Preview, a).has_value());
|
||||
|
||||
CHECK(registry.lookup(ShortcutContext::Plater, c) == Shortcut::GizmoCut);
|
||||
CHECK(registry.lookup(ShortcutContext::Preview, c) == Shortcut::ToggleGcodeWindow);
|
||||
CHECK(registry.lookup(ShortcutContext::Painting, c) == Shortcut::PaintToolCircle);
|
||||
}
|
||||
|
||||
TEST_CASE("Stepping shortcuts match with Shift or Ctrl added to their binding", "[Shortcuts]")
|
||||
{
|
||||
ShortcutRegistry registry;
|
||||
using Match = ShortcutRegistry::Match;
|
||||
auto same = [](const std::optional<Match>& match, Shortcut shortcut, int step_modifiers) {
|
||||
return match.has_value() && match->shortcut == shortcut && match->step_modifiers == step_modifiers;
|
||||
};
|
||||
CHECK(same(registry.match(ShortcutContext::Preview, { WXK_UP }), Shortcut::LayerSliderUp, 0));
|
||||
CHECK(same(registry.match(ShortcutContext::Preview, { WXK_UP, wxMOD_SHIFT }), Shortcut::LayerSliderUp, wxMOD_SHIFT));
|
||||
CHECK(same(registry.match(ShortcutContext::Plater, { WXK_LEFT, wxMOD_CONTROL | wxMOD_SHIFT }), Shortcut::MoveSelectionLeft, wxMOD_CONTROL | wxMOD_SHIFT));
|
||||
CHECK(same(registry.match(ShortcutContext::Plater, { 'A', wxMOD_SHIFT }), Shortcut::ArrangePlate, 0)); // an exact binding wins
|
||||
CHECK_FALSE(registry.match(ShortcutContext::Plater, { 'Q', wxMOD_CONTROL }).has_value()); // Orient has no variants
|
||||
CHECK_FALSE(registry.match(ShortcutContext::Preview, { WXK_UP, wxMOD_ALT }).has_value()); // Alt is not a step modifier
|
||||
|
||||
CHECK_FALSE(registry.match(ShortcutContext::Preview, { WXK_HOME, wxMOD_SHIFT }).has_value()); // Home has no variants
|
||||
|
||||
// A binding with Shift or Ctrl of its own has no steps.
|
||||
registry.bind(Shortcut::LayerSliderUp, { WXK_UP, wxMOD_CONTROL });
|
||||
CHECK(same(registry.match(ShortcutContext::Preview, { WXK_UP, wxMOD_CONTROL }), Shortcut::LayerSliderUp, 0));
|
||||
CHECK_FALSE(registry.match(ShortcutContext::Preview, { WXK_UP, wxMOD_CONTROL | wxMOD_SHIFT }).has_value());
|
||||
CHECK_FALSE(registry.match(ShortcutContext::Preview, { WXK_UP, wxMOD_SHIFT }).has_value());
|
||||
|
||||
// An exact binding on the combined step wins over it.
|
||||
registry.bind(Shortcut::Arrange, { WXK_LEFT, wxMOD_CONTROL | wxMOD_SHIFT });
|
||||
CHECK(same(registry.match(ShortcutContext::Plater, { WXK_LEFT, wxMOD_CONTROL | wxMOD_SHIFT }), Shortcut::Arrange, 0));
|
||||
CHECK(same(registry.match(ShortcutContext::Plater, { WXK_LEFT, wxMOD_SHIFT }), Shortcut::MoveSelectionLeft, wxMOD_SHIFT));
|
||||
}
|
||||
|
||||
TEST_CASE("Conflicts cover shared contexts and every Global shortcut", "[Shortcuts]")
|
||||
{
|
||||
ShortcutRegistry registry;
|
||||
CHECK(registry.conflicts(Shortcut::Arrange, { 'N', wxMOD_CONTROL }) == std::vector<Shortcut>{ Shortcut::NewProject });
|
||||
CHECK(registry.conflicts(Shortcut::NewProject, { 'A' }) == std::vector<Shortcut>{ Shortcut::Arrange });
|
||||
CHECK(registry.conflicts(Shortcut::ToggleGcodeWindow, { 'C' }).empty());
|
||||
CHECK(registry.conflicts(Shortcut::ZoomIn, { 'C' }) == std::vector<Shortcut>{ Shortcut::GizmoCut, Shortcut::ToggleGcodeWindow });
|
||||
CHECK(registry.conflicts(Shortcut::Arrange, { 'A' }).empty()); // a shortcut never conflicts with itself
|
||||
|
||||
// Only the exact chord conflicts; the steps of a stepping shortcut are reserved instead.
|
||||
CHECK(registry.conflicts(Shortcut::GoToLayer, { WXK_UP, wxMOD_SHIFT }).empty());
|
||||
CHECK(registry.conflicts(Shortcut::LayerSliderUp, { 'G', wxMOD_SHIFT }) == std::vector<Shortcut>{ Shortcut::GoToLayer });
|
||||
}
|
||||
|
||||
TEST_CASE("Shift and Ctrl with a stepping shortcut's key are reserved for its steps", "[Shortcuts]")
|
||||
{
|
||||
ShortcutRegistry registry;
|
||||
CHECK(registry.step_owner(Shortcut::GoToLayer, { WXK_UP, wxMOD_SHIFT }) == Shortcut::LayerSliderUp);
|
||||
CHECK(registry.step_owner(Shortcut::NewProject, { WXK_LEFT, wxMOD_CONTROL }) == Shortcut::MoveSelectionLeft); // Global shares every context
|
||||
CHECK_FALSE(registry.step_owner(Shortcut::GoToLayer, { WXK_UP, wxMOD_CONTROL | wxMOD_SHIFT }).has_value()); // the combined step is free
|
||||
CHECK_FALSE(registry.step_owner(Shortcut::MoveSelectionLeft, { WXK_LEFT, wxMOD_SHIFT }).has_value()); // its own step
|
||||
CHECK_FALSE(registry.step_owner(Shortcut::PaintToolCircle, { WXK_UP, wxMOD_SHIFT }).has_value()); // Painting shares no context
|
||||
registry.bind(Shortcut::LayerSliderUp, { WXK_UP, wxMOD_CONTROL });
|
||||
CHECK_FALSE(registry.step_owner(Shortcut::GoToLayer, { WXK_UP, wxMOD_CONTROL | wxMOD_SHIFT }).has_value()); // a modified binding has no steps
|
||||
}
|
||||
|
||||
TEST_CASE("Custom bindings replace the default and survive a config round trip", "[Shortcuts]")
|
||||
{
|
||||
ShortcutRegistry registry;
|
||||
const KeyChord w{ 'W' };
|
||||
registry.bind(Shortcut::Arrange, w);
|
||||
|
||||
CHECK(registry.is_customized(Shortcut::Arrange));
|
||||
CHECK(registry.lookup(ShortcutContext::Plater, w) == Shortcut::Arrange);
|
||||
CHECK_FALSE(registry.lookup(ShortcutContext::Plater, { 'A' }).has_value());
|
||||
|
||||
AppConfig config;
|
||||
registry.save(config);
|
||||
CHECK(config.get("shortcuts", "arrange") == "W");
|
||||
CHECK_FALSE(config.has("shortcuts", "orient"));
|
||||
|
||||
ShortcutRegistry loaded;
|
||||
loaded.load(config);
|
||||
CHECK(loaded.lookup(ShortcutContext::Plater, w) == Shortcut::Arrange);
|
||||
CHECK(loaded.binding(Shortcut::Orient) == KeyChord{ 'Q' });
|
||||
|
||||
SECTION("rebinding to the default clears the override")
|
||||
{
|
||||
registry.bind(Shortcut::Arrange, { 'A' });
|
||||
CHECK_FALSE(registry.is_customized(Shortcut::Arrange));
|
||||
registry.save(config);
|
||||
CHECK_FALSE(config.has("shortcuts", "arrange"));
|
||||
}
|
||||
SECTION("an invalid chord unbinds and persists as none")
|
||||
{
|
||||
registry.bind(Shortcut::Arrange, KeyChord{});
|
||||
CHECK_FALSE(registry.binding(Shortcut::Arrange).valid());
|
||||
registry.save(config);
|
||||
CHECK(config.get("shortcuts", "arrange") == "none");
|
||||
loaded.load(config);
|
||||
CHECK_FALSE(loaded.lookup(ShortcutContext::Plater, { 'A' }).has_value());
|
||||
CHECK_FALSE(loaded.lookup(ShortcutContext::Plater, w).has_value());
|
||||
}
|
||||
SECTION("reset_all restores every default")
|
||||
{
|
||||
registry.reset_all();
|
||||
CHECK(registry.lookup(ShortcutContext::Plater, { 'A' }) == Shortcut::Arrange);
|
||||
CHECK_FALSE(registry.is_customized(Shortcut::Arrange));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("A Global shortcut refuses a config binding that would swallow typing", "[Shortcuts]")
|
||||
{
|
||||
AppConfig config;
|
||||
// A string literal would pick AppConfig::set's bool overload.
|
||||
config.set("shortcuts", "save_project", std::string("S"));
|
||||
config.set("shortcuts", "new_project", std::string("F9"));
|
||||
ShortcutRegistry registry;
|
||||
registry.load(config);
|
||||
CHECK(registry.binding(Shortcut::SaveProject) == KeyChord{ 'S', wxMOD_CONTROL });
|
||||
CHECK(registry.binding(Shortcut::NewProject) == KeyChord{ WXK_F9 });
|
||||
}
|
||||
|
||||
TEST_CASE("Unreadable config entries fall back to the default binding", "[Shortcuts]")
|
||||
{
|
||||
AppConfig config;
|
||||
config.set("shortcuts", "arrange", std::string("Hyper+Q"));
|
||||
config.set("shortcuts", "no_such_shortcut", std::string("Ctrl+Q"));
|
||||
|
||||
ShortcutRegistry registry;
|
||||
registry.load(config);
|
||||
CHECK_FALSE(registry.is_customized(Shortcut::Arrange));
|
||||
CHECK(registry.lookup(ShortcutContext::Plater, { 'A' }) == Shortcut::Arrange);
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <slic3r/GUI/WebMediaController.hpp>
|
||||
|
||||
#include <wx/webview.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using namespace Slic3r;
|
||||
using namespace Slic3r::GUI;
|
||||
|
||||
namespace {
|
||||
|
||||
class StubWebView final : public wxWebView
|
||||
{
|
||||
public:
|
||||
bool SetBackgroundColour(const wxColour&) override
|
||||
{
|
||||
events.emplace_back("background");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Create(wxWindow*, wxWindowID, const wxString&, const wxPoint&, const wxSize&, long, const wxString&) override { return true; }
|
||||
wxString GetCurrentTitle() const override { return {}; }
|
||||
wxString GetCurrentURL() const override { return {}; }
|
||||
bool IsBusy() const override { return false; }
|
||||
bool IsEditable() const override { return false; }
|
||||
void LoadURL(const wxString& url) override
|
||||
{
|
||||
events.emplace_back("url");
|
||||
loaded_url = url.ToStdString();
|
||||
}
|
||||
void Print() override {}
|
||||
void RegisterHandler(wxSharedPtr<wxWebViewHandler>) override {}
|
||||
void Reload(wxWebViewReloadFlags) override {}
|
||||
void SetEditable(bool) override {}
|
||||
void Stop() override { events.emplace_back("stop"); }
|
||||
bool CanGoBack() const override { return false; }
|
||||
bool CanGoForward() const override { return false; }
|
||||
void GoBack() override {}
|
||||
void GoForward() override {}
|
||||
void ClearHistory() override { events.emplace_back("history"); }
|
||||
void EnableHistory(bool) override {}
|
||||
wxVector<wxSharedPtr<wxWebViewHistoryItem>> GetBackwardHistory() override { return {}; }
|
||||
wxVector<wxSharedPtr<wxWebViewHistoryItem>> GetForwardHistory() override { return {}; }
|
||||
void LoadHistoryItem(wxSharedPtr<wxWebViewHistoryItem>) override {}
|
||||
bool CanSetZoomType(wxWebViewZoomType) const override { return false; }
|
||||
float GetZoomFactor() const override { return 1.0f; }
|
||||
wxWebViewZoomType GetZoomType() const override { return wxWEBVIEW_ZOOM_TYPE_LAYOUT; }
|
||||
void SetZoomFactor(float) override {}
|
||||
void SetZoomType(wxWebViewZoomType) override {}
|
||||
bool CanUndo() const override { return false; }
|
||||
bool CanRedo() const override { return false; }
|
||||
void Undo() override {}
|
||||
void Redo() override {}
|
||||
void* GetNativeBackend() const override { return nullptr; }
|
||||
|
||||
bool RunScript(const wxString& javascript, wxString*) const override
|
||||
{
|
||||
events.emplace_back("script");
|
||||
script = javascript.ToStdString();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected:
|
||||
void DoSetPage(const wxString& html, const wxString& base_url) override
|
||||
{
|
||||
events.emplace_back("page");
|
||||
page = html.ToStdString();
|
||||
page_base = base_url.ToStdString();
|
||||
}
|
||||
|
||||
public:
|
||||
mutable std::vector<std::string> events;
|
||||
std::string page;
|
||||
std::string page_base;
|
||||
mutable std::string script;
|
||||
std::string loaded_url;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("Web media controller tears down a snapshot lifecycle", "[WebMediaController][integration]")
|
||||
{
|
||||
StubWebView view;
|
||||
WebMediaController controller(&view);
|
||||
|
||||
controller.set_mode(CameraStreamMode::http_snapshot);
|
||||
controller.Load(wxURI("http://camera.example/frame.jpg"));
|
||||
controller.Play();
|
||||
|
||||
REQUIRE(view.events.size() == 3);
|
||||
CHECK(view.events[0] == "background");
|
||||
CHECK(view.events[1] == "page");
|
||||
CHECK(view.events[2] == "page");
|
||||
CHECK(view.page.find("stopCameraRefresh") != std::string::npos);
|
||||
CHECK(view.page.find("http://camera.example/frame.jpg") != std::string::npos);
|
||||
|
||||
controller.Stop();
|
||||
|
||||
REQUIRE(view.events.size() == 7);
|
||||
CHECK(view.events[3] == "script");
|
||||
CHECK(view.events[4] == "stop");
|
||||
CHECK(view.events[5] == "page");
|
||||
CHECK(view.events[6] == "history");
|
||||
CHECK(view.script == "if(typeof stopCameraRefresh==='function') stopCameraRefresh();");
|
||||
CHECK(view.page.empty());
|
||||
CHECK(view.page_base == "about:blank");
|
||||
|
||||
controller.Play();
|
||||
CHECK(view.page.find("http://camera.example/frame.jpg") == std::string::npos);
|
||||
}
|
||||
Reference in New Issue
Block a user