add 'test' library for e2e tests purposes

This commit is contained in:
MihailRis 2026-06-05 23:25:10 +03:00
parent d2f272e403
commit 3429435ff5
7 changed files with 313 additions and 96 deletions

View file

@ -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

View file

@ -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

View file

@ -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[];

View file

@ -0,0 +1,45 @@
#include "api_lua.hpp"
#include "engine/Engine.hpp"
#include "window/input.hpp"
#include <iostream>
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<l_set_button_pressed>},
{"set_key_pressed", lua::wrap<l_set_key_pressed>},
{"enter_text", lua::wrap<l_enter_text>},
{nullptr, nullptr}
};

View file

@ -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<std::string, std::string>* 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<l_print>);
addfunc(L, "crc32", lua::wrap<l_crc32>);
}
@ -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 = &params.projectArgs;
main_thread = create_state(
paths, params.headless ? StateType::SCRIPT : StateType::BASE

View file

@ -165,17 +165,127 @@ inline constexpr short MOUSE_KEYS_OFFSET = 1024;
static GLFWcursor* standard_cursors[static_cast<int>(CursorShape::LAST) + 1] = {};
class GLFWInput : public Input {
class BaseInput : public Input {
public:
std::vector<uint> 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<Keycode>(key));
if (callbacks != keyCallbacks.end()) {
callbacks->second.notify();
}
}
if (pressed && key < MOUSE_KEYS_OFFSET) {
pressedKeys.push_back(static_cast<Keycode>(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<Keycode>& getPressedKeys() const override {
return pressedKeys;
}
const std::vector<uint>& 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<int>(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<int>(keycode)] == currentFrame;
}
bool clicked(Mousecode code) const override {
return pressed(
static_cast<Keycode>(MOUSE_KEYS_OFFSET + static_cast<int>(code))
);
}
bool jclicked(Mousecode code) const override {
return clicked(code) &&
frames[static_cast<int>(code) + MOUSE_KEYS_OFFSET] ==
currentFrame;
}
void simulateKey(Keycode key, bool pressed) override {
onKeyCallback(static_cast<int>(key), pressed);
}
void simulateClick(int button, bool pressed) override {
onMouseCallback(static_cast<int>(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<uint> codepoints;
std::vector<Keycode> pressedKeys;
Bindings bindings;
bool keys[KEYS_BUFFER_SIZE] {};
std::unordered_map<Keycode, util::HandlersList<>> 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<Keycode>(key));
if (callbacks != keyCallbacks.end()) {
callbacks->second.notify();
}
}
if (pressed && key < MOUSE_KEYS_OFFSET) {
pressedKeys.push_back(static_cast<Keycode>(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<int>(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<int>(keycode)] == currentFrame;
}
bool clicked(Mousecode code) const override {
return pressed(
static_cast<Keycode>(MOUSE_KEYS_OFFSET + static_cast<int>(code))
);
}
bool jclicked(Mousecode code) const override {
return clicked(code) &&
frames[static_cast<int>(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<Keycode>& getPressedKeys() const override {
return pressedKeys;
}
const std::vector<uint>& 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<GLFWInput>());

View file

@ -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;