diff --git a/res/modules/internal/test.lua b/res/modules/internal/test.lua new file mode 100644 index 000000000..a6ca7d096 --- /dev/null +++ b/res/modules/internal/test.lua @@ -0,0 +1,136 @@ +local __app = __vc_app + +local function get_node_center(node) + local pos = node.wpos + local size = node.size + return pos[1] + size[1] / 2, pos[2] + size[2] / 2 +end + +local function types_check_enum(value, enum) + if value == nil then + return true, nil + end + if type(value) ~= "string" then + return false, string.format("string expected [%s]", table.concat(table.keys(enum), "|")) + end + return true, nil +end + +local function types_check_number(value) + if value == nil then + return true, nil + end + local value_type = type(value) + if value_type ~= "number" then + return false, string.format("number expected, got %s", value_type) + end +end + +local function check_option(t, key, func, ...) + local value = t[key] + if value == nil then + return nil + end + local success, message = func(value, ...) + if not success then + error(string.format("invalid option '%s': %s", key, message)) + end +end + +local buttons_enum = { + left = 0, + right = 1, + middle = 2, +} +local no_options = {} + +local _tick = __app.tick +local _set_button_pressed = test.set_button_pressed +local _set_key_pressed = test.set_key_pressed +local _enter_text = test.enter_text +test.set_button_pressed = nil +test.set_key_pressed = nil +test.enter_text = nil + +function test.click(node, options) + options = options or no_options + + check_option(options, 'button', types_check_enum, buttons_enum) + check_option(options, 'timeout', types_check_number) + + local button = buttons_enum[options.button or 'left'] + local timeout = options.timeout or 1000 + + local tm = time.precise_time() + while (time.precise_time() - tm) * 1000 < timeout do + if node.exists then + break + end + _tick() + end + if not node.exists then + error(string.format("timeout %s ms exceeded: element %s does not exists", + math.floor(timeout), string.escape(node.id))) + end + + local center_x, center_y = get_node_center(node) + _set_button_pressed(button, center_x, center_y, true) + _tick() + _set_button_pressed(button, center_x, center_y, false) + _tick() +end + +-- TODO: replace with resolving in C++ side +function test.find_by_attr(node, attr, value) + if node[attr] == value then + return node + end + local i = 1 + while true do + local subnode = node[i] + if subnode == nil then + break + end + local found = test.find_by_attr(subnode, attr, value) + if found then + return found + end + i = i + 1 + end +end + +function test.find_by_text(node, text) + return test.find_by_attr(node, "text", text) +end + +function test.find_by_attr_presence(node, attr) + if node[attr] then + return node + end + local i = 1 + while true do + local subnode = node[i] + if subnode == nil then + break + end + local found = test.find_by_attr_presence(subnode, attr) + if found then + return found + end + i = i + 1 + end +end + +function test.fill(node, text, options) + test.click(node, options) + __app.tick() + _enter_text(text) + test.press("enter") +end + +function test.press(key) + _set_key_pressed(key, true) + __app.tick() + _set_key_pressed(key, false) + __app.tick() +end diff --git a/res/scripts/stdlib.lua b/res/scripts/stdlib.lua index 093501d75..a1949c1e6 100644 --- a/res/scripts/stdlib.lua +++ b/res/scripts/stdlib.lua @@ -127,6 +127,10 @@ require "core:internal/extensions/inventory" asserts = require "core:internal/asserts" events = require "core:internal/events" +if test then + require "core:internal/test" +end + function pack.unload(prefix) events.remove_by_prefix(prefix) end diff --git a/src/logic/scripting/lua/libs/api_lua.hpp b/src/logic/scripting/lua/libs/api_lua.hpp index e304d6dc1..123e1e532 100644 --- a/src/logic/scripting/lua/libs/api_lua.hpp +++ b/src/logic/scripting/lua/libs/api_lua.hpp @@ -44,6 +44,7 @@ extern const luaL_Reg posteffectslib[]; // gfx.posteffects extern const luaL_Reg quatlib[]; extern const luaL_Reg randomlib[]; extern const luaL_Reg compressionlib[]; +extern const luaL_Reg testlib[]; extern const luaL_Reg text3dlib[]; // gfx.text3d extern const luaL_Reg timelib[]; extern const luaL_Reg tomllib[]; diff --git a/src/logic/scripting/lua/libs/libtest.cpp b/src/logic/scripting/lua/libs/libtest.cpp new file mode 100644 index 000000000..d6675deeb --- /dev/null +++ b/src/logic/scripting/lua/libs/libtest.cpp @@ -0,0 +1,45 @@ +#include "api_lua.hpp" +#include "engine/Engine.hpp" +#include "window/input.hpp" + +#include + +using namespace scripting; + +static int l_set_button_pressed(lua::State* L) { + int button = lua::touinteger(L, 1); + int x = lua::tointeger(L, 2); + int y = lua::tointeger(L, 3); + bool pressed = lua::toboolean(L, 4); + + auto& input = engine->getInput(); + input.simulateCursorPos(x, y); + input.simulateClick(button, pressed); + return 0; +} + +static int l_set_key_pressed(lua::State* L) { + auto key = lua::require_string(L, 1); + bool pressed = lua::toboolean(L, 2); + + auto& input = engine->getInput(); + input.simulateKey(input_util::keycode_from(key), pressed); + return 0; +} + +static int l_enter_text(lua::State* L) { + auto string = lua::require_wstring(L, 1); + + auto& input = engine->getInput(); + for (auto chr : string) { + input.simulateCodepoint(chr); + } + return 0; +} + +const luaL_Reg testlib[] = { + {"set_button_pressed", lua::wrap}, + {"set_key_pressed", lua::wrap}, + {"enter_text", lua::wrap}, + {nullptr, nullptr} +}; diff --git a/src/logic/scripting/lua/lua_engine.cpp b/src/logic/scripting/lua/lua_engine.cpp index afb2f42eb..38b6a4b83 100644 --- a/src/logic/scripting/lua/lua_engine.cpp +++ b/src/logic/scripting/lua/lua_engine.cpp @@ -19,6 +19,7 @@ namespace { debug::Logger logger("lua-state"); lua::State* main_thread = nullptr; bool headless_mode = false; + bool test_mode = false; const std::unordered_map* project_args; } @@ -69,12 +70,12 @@ static void create_libs(State* L, StateType stateType) { openlib(L, "yaml", yamllib); openlib(L, "__vc_app", applib); - lua::getglobal(L, "__vc_app"); - lua::setregistry(L, "app"); + getglobal(L, "__vc_app"); + setregistry(L, "app"); if (stateType == StateType::SCRIPT) { - lua::getregistry(L, "app"); - lua::setglobal(L, "app"); + getregistry(L, "app"); + setglobal(L, "app"); } if (stateType == StateType::BASE || stateType == StateType::SCRIPT) { openlib(L, "assets", assetslib); @@ -99,6 +100,10 @@ static void create_libs(State* L, StateType stateType) { openlib(L, "__transform", transformlib); } + if (::test_mode) { + openlib(L, "test", testlib); + } + addfunc(L, "print", lua::wrap); addfunc(L, "crc32", lua::wrap); } @@ -191,6 +196,7 @@ void lua::initialize(const EnginePaths& paths, const CoreParameters& params) { logger.info() << LUAJIT_VERSION; headless_mode = params.headless; + test_mode = params.testMode; project_args = ¶ms.projectArgs; main_thread = create_state( paths, params.headless ? StateType::SCRIPT : StateType::BASE diff --git a/src/window/detail/GLFWWindow.cpp b/src/window/detail/GLFWWindow.cpp index db3d67241..6f42258d8 100644 --- a/src/window/detail/GLFWWindow.cpp +++ b/src/window/detail/GLFWWindow.cpp @@ -165,17 +165,127 @@ inline constexpr short MOUSE_KEYS_OFFSET = 1024; static GLFWcursor* standard_cursors[static_cast(CursorShape::LAST) + 1] = {}; -class GLFWInput : public Input { +class BaseInput : public Input { public: + std::vector codepoints; int scroll = 0; + + void onKeyCallback(int key, bool pressed) { + bool prevPressed = keys[key]; + keys[key] = pressed; + frames[key] = currentFrame; + if (pressed && !prevPressed) { + const auto& callbacks = keyCallbacks.find(static_cast(key)); + if (callbacks != keyCallbacks.end()) { + callbacks->second.notify(); + } + } + if (pressed && key < MOUSE_KEYS_OFFSET) { + pressedKeys.push_back(static_cast(key)); + } + } + + void onMouseCallback(int button, bool pressed) { + int key = button + MOUSE_KEYS_OFFSET; + onKeyCallback(key, pressed); + } + + bool isCursorLocked() const override { + return cursorLocked; + } + + void setCursorPosition(double xpos, double ypos) { + if (cursorDrag) { + delta.x += xpos - cursor.x; + delta.y += ypos - cursor.y; + } else { + cursorDrag = true; + } + cursor.x = xpos; + cursor.y = ypos; + } + + Bindings& getBindings() override { + return bindings; + } + + const Bindings& getBindings() const override { + return bindings; + } + + ObserverHandler addKeyCallback(Keycode key, KeyCallback callback) override { + return keyCallbacks[key].add(std::move(callback)); + } + + const std::vector& getPressedKeys() const override { + return pressedKeys; + } + + const std::vector& getCodepoints() const override { + return codepoints; + } + + CursorState getCursor() const override { + return {isCursorLocked(), cursor, delta}; + } + + int getScroll() override { + return scroll; + } + + bool pressed(Keycode key) const override { + int keycode = static_cast(key); + if (keycode < 0 || keycode >= KEYS_BUFFER_SIZE) { + return false; + } + return keys[keycode]; + } + bool jpressed(Keycode keycode) const override { + return pressed(keycode) && + frames[static_cast(keycode)] == currentFrame; + } + + bool clicked(Mousecode code) const override { + return pressed( + static_cast(MOUSE_KEYS_OFFSET + static_cast(code)) + ); + } + bool jclicked(Mousecode code) const override { + return clicked(code) && + frames[static_cast(code) + MOUSE_KEYS_OFFSET] == + currentFrame; + } + + void simulateKey(Keycode key, bool pressed) override { + onKeyCallback(static_cast(key), pressed); + } + + void simulateClick(int button, bool pressed) override { + onMouseCallback(static_cast(button), pressed); + } + + void simulateCursorPos(double xpos, double ypos) override { + setCursorPosition(xpos, ypos); + } + + void simulateCodepoint(uint codepoint) override { + codepoints.push_back(codepoint); + } +protected: uint currentFrame = 0; uint frames[KEYS_BUFFER_SIZE] {}; - std::vector codepoints; std::vector pressedKeys; Bindings bindings; bool keys[KEYS_BUFFER_SIZE] {}; std::unordered_map> keyCallbacks; + bool cursorLocked = false; + bool cursorDrag = false; + glm::vec2 delta {}; + glm::vec2 cursor {}; +}; +class GLFWInput : public BaseInput { +public: GLFWInput(GLFWwindow* window) : window(window) { } @@ -225,26 +335,6 @@ public: } } - void onKeyCallback(int key, bool pressed) { - bool prevPressed = keys[key]; - keys[key] = pressed; - frames[key] = currentFrame; - if (pressed && !prevPressed) { - const auto& callbacks = keyCallbacks.find(static_cast(key)); - if (callbacks != keyCallbacks.end()) { - callbacks->second.notify(); - } - } - if (pressed && key < MOUSE_KEYS_OFFSET) { - pressedKeys.push_back(static_cast(key)); - } - } - - void onMouseCallback(int button, bool pressed) { - int key = button + MOUSE_KEYS_OFFSET; - onKeyCallback(key, pressed); - } - const char* getClipboardText() const override { return glfwGetClipboardString(window); } @@ -253,41 +343,6 @@ public: glfwSetClipboardString(window, text); } - int getScroll() override { - return scroll; - } - - bool pressed(Keycode key) const override { - int keycode = static_cast(key); - if (keycode < 0 || keycode >= KEYS_BUFFER_SIZE) { - return false; - } - return keys[keycode]; - } - bool jpressed(Keycode keycode) const override { - return pressed(keycode) && - frames[static_cast(keycode)] == currentFrame; - } - - bool clicked(Mousecode code) const override { - return pressed( - static_cast(MOUSE_KEYS_OFFSET + static_cast(code)) - ); - } - bool jclicked(Mousecode code) const override { - return clicked(code) && - frames[static_cast(code) + MOUSE_KEYS_OFFSET] == - currentFrame; - } - - CursorState getCursor() const override { - return {isCursorLocked(), cursor, delta}; - } - - bool isCursorLocked() const override { - return cursorLocked; - } - void toggleCursor() override { cursorDrag = false; if (cursorLocked) { @@ -298,43 +353,8 @@ public: } cursorLocked = !cursorLocked; } - - void setCursorPosition(double xpos, double ypos) { - if (cursorDrag) { - delta.x += xpos - cursor.x; - delta.y += ypos - cursor.y; - } else { - cursorDrag = true; - } - cursor.x = xpos; - cursor.y = ypos; - } - - Bindings& getBindings() override { - return bindings; - } - - const Bindings& getBindings() const override { - return bindings; - } - - ObserverHandler addKeyCallback(Keycode key, KeyCallback callback) override { - return keyCallbacks[key].add(std::move(callback)); - } - - const std::vector& getPressedKeys() const override { - return pressedKeys; - } - - const std::vector& getCodepoints() const override { - return codepoints; - } private: GLFWwindow* window; - bool cursorLocked = false; - bool cursorDrag = false; - glm::vec2 delta {}; - glm::vec2 cursor {}; }; static_assert(!std::is_abstract()); diff --git a/src/window/input.hpp b/src/window/input.hpp index 84c120409..59d663672 100644 --- a/src/window/input.hpp +++ b/src/window/input.hpp @@ -274,6 +274,11 @@ public: virtual bool clicked(Mousecode mousecode) const = 0; virtual bool jclicked(Mousecode mousecode) const = 0; + virtual void simulateKey(Keycode key, bool pressed) = 0; + virtual void simulateClick(int button, bool pressed) = 0; + virtual void simulateCursorPos(double xpos, double ypos) = 0; + virtual void simulateCodepoint(uint codepoint) = 0; + virtual CursorState getCursor() const = 0; virtual bool isCursorLocked() const = 0;