diff --git a/.github/workflows/ci.yml b/.github/workflows/build.yml similarity index 64% rename from .github/workflows/ci.yml rename to .github/workflows/build.yml index ec158d1..8b8222f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/build.yml @@ -2,24 +2,27 @@ name: Build on: push: - branches: - - master + branches: [ master ] paths: - 'src/**' + - 'tests/**' + - '.github/**' + - 'CMakeLists.txt' pull_request: - branches: - - master + branches: [ master ] paths: - 'src/**' + - 'tests/**' + - '.github/**' + - 'CMakeLists.txt' jobs: build: - runs-on: macos-12 + runs-on: macos-latest steps: - - name: Checkout repository - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: - submodules: 'recursive' + submodules: recursive - name: Install dependencies run: | diff --git a/.github/workflows/test-macos.yml b/.github/workflows/test-macos.yml new file mode 100644 index 0000000..1034c81 --- /dev/null +++ b/.github/workflows/test-macos.yml @@ -0,0 +1,39 @@ +name: MacOS Tests (mouse) + +on: + push: + branches: [ master ] + paths: + - 'src/**' + - 'tests/**' + - '.github/**' + - 'CMakeLists.txt' + pull_request: + branches: [ master ] + paths: + - 'src/**' + - 'tests/**' + - '.github/**' + - 'CMakeLists.txt' + +jobs: + test-macos: + runs-on: macos-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Install dependencies + run: brew install cmake sdl2 + + - name: Configure + run: cmake -B build -DBUILD_ROBOT_TESTS=ON + + - name: Build + run: cmake --build build + + - name: Test + run: | + # macOS can run GUI apps in headless mode + build/bin/RobotCPPSDLTest --gtest_filter=-*InteractiveMode --ci-mode true diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml new file mode 100644 index 0000000..97c8bbc --- /dev/null +++ b/.github/workflows/test-windows.yml @@ -0,0 +1,103 @@ +name: Windows Tests (mouse) + +on: + push: + branches: [ master ] + paths: + - 'src/**' + - 'tests/**' + - '.github/**' + - 'CMakeLists.txt' + pull_request: + branches: [ master ] + paths: + - 'src/**' + - 'tests/**' + - '.github/**' + - 'CMakeLists.txt' + +jobs: + test-windows: + runs-on: windows-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Install vcpkg and SDL2 + run: | + # Clone vcpkg + git clone https://github.com/Microsoft/vcpkg.git + cd vcpkg + + # Bootstrap vcpkg + .\bootstrap-vcpkg.bat + + # Install SDL2 + .\vcpkg install sdl2:x64-windows + + # Integrate with Visual Studio + .\vcpkg integrate install + + cd .. + + - name: Configure with vcpkg + run: | + cmake -B build -DCMAKE_TOOLCHAIN_FILE="$PWD/vcpkg/scripts/buildsystems/vcpkg.cmake" -DBUILD_ROBOT_TESTS=ON + + - name: Build + run: cmake --build build --config Release + + - name: Test + run: | + # Check and display directory structure + Write-Host "Checking directory structure..." + + # Check vcpkg directories + Write-Host "Vcpkg directories:" + Get-ChildItem -Path "vcpkg\installed\x64-windows\bin" -ErrorAction SilentlyContinue + + # Check build output directories + Write-Host "Build output directories:" + Get-ChildItem -Path "build\bin" -ErrorAction SilentlyContinue + Get-ChildItem -Path "build\bin\Release" -ErrorAction SilentlyContinue + + # Find SDL2.dll + Write-Host "Finding SDL2.dll..." + Get-ChildItem -Path "vcpkg" -Recurse -Filter "SDL2.dll" -ErrorAction SilentlyContinue | + ForEach-Object { Write-Host $_.FullName } + + # Create Release directory if it doesn't exist + if (-not (Test-Path "build\bin\Release")) { + Write-Host "Creating missing directory: build\bin\Release" + New-Item -Path "build\bin\Release" -ItemType Directory -Force + } + + # Try to find the executable + Write-Host "Finding test executable..." + Get-ChildItem -Path "build" -Recurse -Filter "*.exe" -ErrorAction SilentlyContinue | + ForEach-Object { Write-Host $_.FullName } + + # Try to run the executable wherever it is + $executable = Get-ChildItem -Path "build" -Recurse -Filter "RobotCPPSDLTest.exe" -ErrorAction SilentlyContinue | + Select-Object -First 1 + + if ($executable) { + Write-Host "Found executable at: $($executable.FullName)" + + # Try to find and copy SDL2.dll + $sdl2Dll = Get-ChildItem -Path "vcpkg" -Recurse -Filter "SDL2.dll" -ErrorAction SilentlyContinue | + Select-Object -First 1 + + if ($sdl2Dll) { + Write-Host "Found SDL2.dll at: $($sdl2Dll.FullName)" + Copy-Item -Path $sdl2Dll.FullName -Destination $executable.DirectoryName -Force + } + + # Run the executable + Write-Host "Running: $($executable.FullName) --gtest_filter=-*InteractiveMode --ci-mode true" + & $executable.FullName --gtest_filter=-*InteractiveMode --ci-mode true + } else { + Write-Host "Executable not found!" + exit 1 + } diff --git a/.gitignore b/.gitignore index 89e41d1..1ec2e2a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ # Build files /cmake-build-debug /example/cmake-build-debug + + +/build diff --git a/.gitmodules b/.gitmodules index bf4bc2d..5ae0458 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,9 @@ [submodule "externals/lodepng"] path = externals/lodepng url = https://github.com/lvandeve/lodepng +[submodule "cmake/sdl2"] + path = cmake/sdl2 + url = https://github.com/opeik/cmake-modern-findsdl2 +[submodule "externals/googletest"] + path = externals/googletest + url = https://github.com/google/googletest diff --git a/CMakeLists.txt b/CMakeLists.txt index 3ed6100..5eea677 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,8 +3,36 @@ cmake_minimum_required(VERSION 3.21) project(RobotCPP) set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +# Library name set(LIB_NAME RobotCPP) +# Option to build tests (OFF by default) +option(BUILD_ROBOT_TESTS "Build the RobotCPP tests" OFF) + +# Only find dependencies and add GoogleTest if tests are enabled +if(BUILD_ROBOT_TESTS) + # Add GoogleTest + add_subdirectory(externals/googletest) + enable_testing() + + # Find SDL2 for tests + set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/sdl2/") + find_package(SDL2 REQUIRED) +endif() + +# Compiler-specific options +if(MSVC) + # MSVC flags + add_compile_options(/W4 /MP) +else() + # GCC/Clang flags + add_compile_options(-Wall -Wextra -Wpedantic) +endif() + +# Common source files set(COMMON_SOURCES src/ActionRecorder.h src/types.h @@ -17,19 +45,32 @@ set(COMMON_SOURCES src/Screen.cpp src/Screen.h) +# External dependencies set(SOURCES_LODEPNG externals/lodepng/lodepng.cpp externals/lodepng/lodepng.h) -if (APPLE) +# Platform-specific components +if(APPLE) list(APPEND PLATFORM_SOURCES src/EventHookMacOS.h) find_library(CARBON_LIBRARY Carbon) mark_as_advanced(CARBON_LIBRARY) list(APPEND PLATFORM_LIBRARIES ${CARBON_LIBRARY}) -elseif (WIN32) +elseif(WIN32) list(APPEND PLATFORM_SOURCES src/EventHookWindows.h) -endif () +endif() +# Define the main library add_library(${LIB_NAME} STATIC ${COMMON_SOURCES} ${PLATFORM_SOURCES} ${SOURCES_LODEPNG}) target_include_directories(${LIB_NAME} PUBLIC src PRIVATE externals/lodepng) target_link_libraries(${LIB_NAME} ${PLATFORM_LIBRARIES}) + +# Set output directory for all targets +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_BINARY_DIR}/bin) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_BINARY_DIR}/bin) + +# Add the tests directory only if tests are enabled +if(BUILD_ROBOT_TESTS) + add_subdirectory(tests) +endif() diff --git a/README.md b/README.md index 1ddd72e..a82a827 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Robot CPP -![master](https://github.com/developer239/robot-cpp/actions/workflows/ci.yml/badge.svg) -![Windows](https://img.shields.io/badge/Windows-0078D6?style=for-the-badge&logo=windows&logoColor=white) -![macOS](https://img.shields.io/badge/mac%20os-000000?style=for-the-badge&logo=macos&logoColor=F0F0F0) +![Build](https://github.com/developer239/robot-cpp/actions/workflows/build.yml/badge.svg) +[![MacOS Tests](https://github.com/developer239/robot-cpp/actions/workflows/test-macos.yml/badge.svg)](https://github.com/developer239/robot-cpp/actions/workflows/test-macos.yml) +[![Windows Tests](https://github.com/developer239/robot-cpp/actions/workflows/test-windows.yml/badge.svg)](https://github.com/developer239/robot-cpp/actions/workflows/test-windows.yml) This library is inspired by older unmaintained libraries like [octalmage/robotjs](https://github.com/octalmage/robotjs) and [Robot/robot-js](https://github.com/Robot/robot-js). The goal is to provide cross-platform controls for various diff --git a/cmake/sdl2 b/cmake/sdl2 new file mode 160000 index 0000000..77f77c6 --- /dev/null +++ b/cmake/sdl2 @@ -0,0 +1 @@ +Subproject commit 77f77c6699946c0df609bfa04dba93f4cede3a06 diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt deleted file mode 100644 index 906b5a4..0000000 --- a/example/CMakeLists.txt +++ /dev/null @@ -1,11 +0,0 @@ -cmake_minimum_required(VERSION 3.21) - -project(RobotCPPExample) - -set(CMAKE_CXX_STANDARD 23) -set(APP_NAME RobotCPPExample) - -add_subdirectory(../ ${CMAKE_CURRENT_BINARY_DIR}/RobotCPP) -add_executable(MouseExample main.cpp) - -target_link_libraries(MouseExample PRIVATE RobotCPP) diff --git a/example/main.cpp b/example/main.cpp deleted file mode 100644 index 094b9c7..0000000 --- a/example/main.cpp +++ /dev/null @@ -1,39 +0,0 @@ -#include -#include -#include -// Comment out to test on MacOS -#include -// Uncomment to test on MacOS -// #include - -int main() { - int recordFor = 10; - - Robot::ActionRecorder recorder; - Robot::EventHook hook(recorder); - - std::cout << "Start recording actions in 3 seconds..." << std::endl; - std::this_thread::sleep_for(std::chrono::seconds(3)); - - // Start recording - std::cout << "Starting to record actions for " << recordFor << " seconds..." << std::endl; - std::thread recordingThread([&hook] { hook.StartRecording(); }); - - // Sleep for 10 seconds - std::this_thread::sleep_for(std::chrono::seconds(recordFor)); - - // Stop recording - std::cout << "Stopping recording..." << std::endl; - hook.StopRecording(); - recordingThread.join(); - - // Wait for 5 seconds before replaying - std::cout << "Replaying actions in 3 seconds..." << std::endl; - std::this_thread::sleep_for(std::chrono::seconds(3)); - - // Replay the recorded actions - std::cout << "Replaying actions..." << std::endl; - recorder.ReplayActions(); - - return 0; -} diff --git a/externals/googletest b/externals/googletest new file mode 160000 index 0000000..0bdccf4 --- /dev/null +++ b/externals/googletest @@ -0,0 +1 @@ +Subproject commit 0bdccf4aa2f5c67af967193caf31d42d5c49bde2 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..1433a97 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,45 @@ +set(SDL_TEST_NAME RobotCPPSDLTest) + +# Find test dependencies +find_package(GTest QUIET) +if(NOT GTest_FOUND) + # GTest is already included via add_subdirectory in the main CMakeLists.txt +endif() + +# Define test sources +set(SDL_TEST_SOURCES + sdl/SDLTestMain.cpp + sdl/MouseTests.cpp + sdl/TestElements.h + sdl/TestContext.h + sdl/TestConfig.h + sdl/RobotSDLTestFixture.h +) + +# Create test executable +add_executable(${SDL_TEST_NAME} ${SDL_TEST_SOURCES}) + +# Link dependencies +target_link_libraries(${SDL_TEST_NAME} PRIVATE + RobotCPP + SDL2::SDL2 + gtest + gtest_main +) + +# Set output directory for test targets +set_target_properties(${SDL_TEST_NAME} PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" + RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}/bin" + RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}/bin" +) + +# Add a custom command to build the SDL test executable +add_custom_target(build_sdl_tests ALL DEPENDS ${SDL_TEST_NAME}) + +# Add automated tests +add_test( + NAME SDLFunctionalTests + COMMAND ${SDL_TEST_NAME} --run-tests + WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/bin +) diff --git a/tests/sdl/MouseTests.cpp b/tests/sdl/MouseTests.cpp new file mode 100644 index 0000000..1a10282 --- /dev/null +++ b/tests/sdl/MouseTests.cpp @@ -0,0 +1,256 @@ +#include +#include "RobotSDLTestFixture.h" +#include +#include +#include "../../src/Utils.h" + +namespace RobotTest { + +class MouseTest : public RobotSDLTest { +protected: + SDL_Point GetElementCenter(const TestElement* element) const { + SDL_Rect rect = element->getRect(); + return {rect.x + rect.w / 2, rect.y + rect.h / 2}; + } + + void ExpectPositionNear(const SDL_Rect& actual, int expectedX, int expectedY, int tolerance = 0) { + if (tolerance == 0) { + tolerance = config_->positionTolerance; + } + + EXPECT_NEAR(actual.x, expectedX, tolerance) + << "Element X position should be near expected position"; + + EXPECT_NEAR(actual.y, expectedY, tolerance) + << "Element Y position should be near expected position"; + } +}; + +TEST_F(MouseTest, CanDragElementSmoothly) { + // Create drag element specifically for this test + auto dragElement = createDragElement( + 100, 200, 100, 100, Color::Yellow(), "Drag Test Element" + ); + + // Set up drag event handlers for this test + context_->addEventHandler([dragElement](const SDL_Event& event) { + if (event.type == SDL_MOUSEBUTTONDOWN) { + if (event.button.button == SDL_BUTTON_LEFT) { + int x = event.button.x; + int y = event.button.y; + if (dragElement->isInside(x, y)) { + dragElement->startDrag(); + } + } + } else if (event.type == SDL_MOUSEBUTTONUP) { + if (event.button.button == SDL_BUTTON_LEFT) { + dragElement->stopDrag(); + } + } else if (event.type == SDL_MOUSEMOTION) { + int x = event.motion.x; + int y = event.motion.y; + if (dragElement->isDragging()) { + dragElement->moveTo(x, y); + } + } + }); + + const SDL_Rect initialRect = dragElement->getRect(); + SDL_Point startPoint = GetElementCenter(dragElement); + SDL_Point endPoint = { + startPoint.x + 50, + startPoint.y + 30 + }; + + std::cout << "Starting smooth mouse drag test" << std::endl; + std::cout << " Initial element position: (" + << initialRect.x << ", " << initialRect.y << ")" << std::endl; + + processEventsFor(std::chrono::milliseconds(500)); + + Robot::Point startPos = windowToScreen(startPoint.x, startPoint.y); + Robot::Point endPos = windowToScreen(endPoint.x, endPoint.y); + + Robot::Mouse::MoveSmooth(startPos); + Robot::Mouse::ToggleButton(true, Robot::MouseButton::LEFT_BUTTON); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Robot::Mouse::MoveSmooth(endPos); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Robot::Mouse::ToggleButton(false, Robot::MouseButton::LEFT_BUTTON); + + processEventsFor(std::chrono::milliseconds(1000)); + + const SDL_Rect finalRect = dragElement->getRect(); + const int expectedX = initialRect.x + 50; + const int expectedY = initialRect.y + 30; + + ExpectPositionNear(finalRect, expectedX, expectedY); +} + +TEST_F(MouseTest, CanMoveAndClickAtPosition) { + auto clickButton = createTestButton( + 300, 150, 120, 60, Color::Blue(), "Click Test Button" + ); + + SDL_Point buttonCenter = { + clickButton->getRect().x + clickButton->getRect().w / 2, + clickButton->getRect().y + clickButton->getRect().h / 2 + }; + + std::cout << "Starting mouse move and click test" << std::endl; + std::cout << " Button position: (" + << clickButton->getRect().x << ", " << clickButton->getRect().y << ")" << std::endl; + std::cout << " Button center: (" + << buttonCenter.x << ", " << buttonCenter.y << ")" << std::endl; + + processEventsFor(std::chrono::milliseconds(500)); + + Robot::Point targetPos = windowToScreen(buttonCenter.x, buttonCenter.y); + Robot::Mouse::Move(targetPos); + + processEventsFor(std::chrono::milliseconds(500)); + Robot::Mouse::Click(Robot::MouseButton::LEFT_BUTTON); + processEventsFor(std::chrono::milliseconds(500)); + + EXPECT_TRUE(clickButton->wasClicked()) << "Button should have been clicked"; +} + +TEST_F(MouseTest, CanPerformPrecisionMovements) { + std::vector targetPoints = { + {50, 50}, // Top-left + {700, 50}, // Top-right + {50, 500}, // Bottom-left + {700, 500}, // Bottom-right + {400, 300} // Center + }; + + std::cout << "Starting precision mouse movement test" << std::endl; + + for (const auto& point : targetPoints) { + Robot::Point targetPos = windowToScreen(point.x, point.y); + Robot::Mouse::Move(targetPos); + + processEventsFor(std::chrono::milliseconds(300)); + + Robot::Point currentPos = Robot::Mouse::GetPosition(); + + int windowX, windowY; + SDL_GetWindowPosition(context_->getWindow(), &windowX, &windowY); + int localX = currentPos.x - windowX; + int localY = currentPos.y - windowY; + + std::cout << " Target: (" << point.x << ", " << point.y + << "), Actual: (" << localX << ", " << localY << ")" << std::endl; + + EXPECT_NEAR(localX, point.x, config_->positionTolerance) + << "Mouse X position should be near target"; + EXPECT_NEAR(localY, point.y, config_->positionTolerance) + << "Mouse Y position should be near target"; + } +} + +TEST_F(MouseTest, CanPerformDoubleClick) { + auto doubleClickButton = createDoubleClickButton( + 200, 300, 150, 80, Color::Green(), "Double-Click Button" + ); + + SDL_Point buttonCenter = { + doubleClickButton->getRect().x + doubleClickButton->getRect().w / 2, + doubleClickButton->getRect().y + doubleClickButton->getRect().h / 2 + }; + + std::cout << "Starting mouse double-click test" << std::endl; + processEventsFor(std::chrono::milliseconds(500)); + + Robot::Point targetPos = windowToScreen(buttonCenter.x, buttonCenter.y); + Robot::Mouse::Move(targetPos); + processEventsFor(std::chrono::milliseconds(500)); + + Robot::Mouse::DoubleClick(Robot::MouseButton::LEFT_BUTTON); + processEventsFor(std::chrono::milliseconds(500)); + + EXPECT_TRUE(doubleClickButton->wasDoubleClicked()) + << "Button should have registered a double-click"; +} + +TEST_F(MouseTest, CanPerformRightClick) { + auto rightClickButton = createRightClickButton( + 450, 250, 140, 70, Color::Orange(), "Right-Click Button" + ); + + SDL_Point buttonCenter = { + rightClickButton->getRect().x + rightClickButton->getRect().w / 2, + rightClickButton->getRect().y + rightClickButton->getRect().h / 2 + }; + + std::cout << "Starting mouse right-click test" << std::endl; + processEventsFor(std::chrono::milliseconds(500)); + + Robot::Point targetPos = windowToScreen(buttonCenter.x, buttonCenter.y); + Robot::Mouse::Move(targetPos); + processEventsFor(std::chrono::milliseconds(500)); + + Robot::Mouse::Click(Robot::MouseButton::RIGHT_BUTTON); + processEventsFor(std::chrono::milliseconds(500)); + + EXPECT_TRUE(rightClickButton->wasRightClicked()) + << "Button should have registered a right-click"; +} + +TEST_F(MouseTest, CanPerformScroll) { + auto scrollArea = createScrollArea( + 300, 200, 200, 150, Color::White(), "Scroll Test Area" + ); + + SDL_Point areaCenter = { + scrollArea->getRect().x + scrollArea->getRect().w / 2, + scrollArea->getRect().y + scrollArea->getRect().h / 2 + }; + + bool wheelEventReceived = false; + + context_->addEventHandler([&wheelEventReceived, scrollArea](const SDL_Event& event) { + if (event.type == SDL_MOUSEWHEEL) { + wheelEventReceived = true; + std::cout << " SDL wheel event detected! Amount: " << event.wheel.y << std::endl; + + int mouseX, mouseY; + SDL_GetMouseState(&mouseX, &mouseY); + std::cout << " Mouse position during wheel event: (" << mouseX << ", " << mouseY << ")" << std::endl; + + if (scrollArea->isInside(mouseX, mouseY)) { + std::cout << " Mouse is inside scroll area" << std::endl; + } else { + std::cout << " Mouse is outside scroll area" << std::endl; + } + } + }); + + int initialScrollY = scrollArea->getScrollY(); + + std::cout << "Starting mouse scroll test" << std::endl; + std::cout << " Initial scroll position: " << initialScrollY << std::endl; + + processEventsFor(std::chrono::milliseconds(500)); + + Robot::Point targetPos = windowToScreen(areaCenter.x, areaCenter.y); + Robot::Mouse::Move(targetPos); + processEventsFor(std::chrono::milliseconds(500)); + + std::cout << " Performing Robot::Mouse::ScrollBy(20)" << std::endl; + Robot::Mouse::ScrollBy(20); + processEventsFor(std::chrono::milliseconds(1000)); + + EXPECT_TRUE(wheelEventReceived) + << "Robot::Mouse::ScrollBy should generate a wheel event captured by SDL"; + + int newScrollY = scrollArea->getScrollY(); + std::cout << " New scroll position after scrolling: " << newScrollY << std::endl; + + if (wheelEventReceived) { + EXPECT_GT(newScrollY, initialScrollY) + << "When wheel events are captured, scroll position should increase"; + } +} + +} // namespace RobotTest diff --git a/tests/sdl/RobotSDLTestFixture.h b/tests/sdl/RobotSDLTestFixture.h new file mode 100644 index 0000000..f8d3480 --- /dev/null +++ b/tests/sdl/RobotSDLTestFixture.h @@ -0,0 +1,189 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "TestContext.h" +#include "TestConfig.h" +#include "TestElements.h" +#include "../../src/Mouse.h" + +namespace RobotTest { + +class RobotSDLTest : public ::testing::Test { +protected: + void SetUp() override { + config_ = std::make_unique(); + context_ = std::make_unique(*config_); + context_->prepareForTests(); + SDL_Delay(static_cast(config_->setupDelay.count())); + } + + void TearDown() override { + testElements_.clear(); + context_.reset(); + } + + DragElement* createDragElement(int x, int y, int width, int height, + Color color, const std::string& name) { + auto element = std::make_unique( + SDL_Rect{x, y, width, height}, color, name); + + auto* rawPtr = element.get(); + testElements_.push_back(std::move(element)); + return rawPtr; + } + + TestButton* createTestButton(int x, int y, int width, int height, + Color color, const std::string& name) { + auto button = std::make_unique( + SDL_Rect{x, y, width, height}, color, name); + + context_->addEventHandler([button = button.get()](const SDL_Event& event) { + if (event.type == SDL_MOUSEBUTTONDOWN && + event.button.button == SDL_BUTTON_LEFT) { + int x = event.button.x; + int y = event.button.y; + if (button->isInside(x, y)) { + button->handleClick(); + } + } + }); + + auto* rawPtr = button.get(); + testElements_.push_back(std::move(button)); + return rawPtr; + } + + DoubleClickButton* createDoubleClickButton(int x, int y, int width, int height, + Color color, const std::string& name) { + auto button = std::make_unique( + SDL_Rect{x, y, width, height}, color, name); + + context_->addEventHandler([button = button.get()](const SDL_Event& event) { + if (event.type == SDL_MOUSEBUTTONDOWN && + event.button.button == SDL_BUTTON_LEFT) { + int x = event.button.x; + int y = event.button.y; + if (button->isInside(x, y)) { + button->handleClick(); + } + } + }); + + auto* rawPtr = button.get(); + testElements_.push_back(std::move(button)); + return rawPtr; + } + + RightClickButton* createRightClickButton(int x, int y, int width, int height, + Color color, const std::string& name) { + auto button = std::make_unique( + SDL_Rect{x, y, width, height}, color, name); + + context_->addEventHandler([button = button.get()](const SDL_Event& event) { + if (event.type == SDL_MOUSEBUTTONDOWN && + event.button.button == SDL_BUTTON_RIGHT) { + int x = event.button.x; + int y = event.button.y; + if (button->isInside(x, y)) { + button->handleRightClick(); + } + } + }); + + auto* rawPtr = button.get(); + testElements_.push_back(std::move(button)); + return rawPtr; + } + + ScrollArea* createScrollArea(int x, int y, int width, int height, + Color color, const std::string& name) { + auto area = std::make_unique( + SDL_Rect{x, y, width, height}, color, name); + + context_->addEventHandler([area = area.get()](const SDL_Event& event) { + if (event.type == SDL_MOUSEWHEEL) { + int mouseX, mouseY; + SDL_GetMouseState(&mouseX, &mouseY); + if (area->isInside(mouseX, mouseY)) { + area->handleScroll(event.wheel.y); + } + } + }); + + auto* rawPtr = area.get(); + testElements_.push_back(std::move(area)); + return rawPtr; + } + + void processEventsFor(std::chrono::milliseconds duration) { + auto startTime = std::chrono::steady_clock::now(); + bool running = true; + + while (running && + (std::chrono::steady_clock::now() - startTime < duration)) { + context_->handleEvents(running); + context_->renderFrame([this](SDL_Renderer* renderer) { + renderTestElements(renderer); + }); + SDL_Delay(static_cast(config_->frameDelay.count())); + } + } + + Robot::Point windowToScreen(int x, int y) const { + int windowX, windowY; + SDL_GetWindowPosition(context_->getWindow(), &windowX, &windowY); + return {x + windowX, y + windowY}; + } + + SDL_Point screenToWindow(int x, int y) const { + int windowX, windowY; + SDL_GetWindowPosition(context_->getWindow(), &windowX, &windowY); + return {x - windowX, y - windowY}; + } + + void performMouseDrag(const SDL_Point& startPoint, const SDL_Point& endPoint) { + Robot::Point startPos = windowToScreen(startPoint.x, startPoint.y); + Robot::Point endPos = windowToScreen(endPoint.x, endPoint.y); + + std::cout << "Moving to start position: " << startPos.x << ", " << startPos.y << std::endl; + Robot::Mouse::MoveSmooth(startPos); + + std::cout << "Performing smooth drag to end position: " << endPos.x << ", " << endPos.y << std::endl; + Robot::Mouse::DragSmooth(endPos); + + processEventsFor(std::chrono::milliseconds(1000)); + } + + void renderTestElements(SDL_Renderer* renderer) { + for (const auto& element : testElements_) { + element->draw(renderer); + } + drawMousePosition(renderer); + } + + void drawMousePosition(SDL_Renderer* renderer) { + int windowX, windowY; + SDL_GetWindowPosition(context_->getWindow(), &windowX, &windowY); + + Robot::Point globalMousePos = Robot::Mouse::GetPosition(); + int localMouseX = globalMousePos.x - windowX; + int localMouseY = globalMousePos.y - windowY; + + SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255); + SDL_RenderDrawLine(renderer, localMouseX - 10, localMouseY, localMouseX + 10, localMouseY); + SDL_RenderDrawLine(renderer, localMouseX, localMouseY - 10, localMouseX, localMouseY + 10); + } + + std::unique_ptr config_; + std::unique_ptr context_; + std::vector> testElements_; +}; + +} // namespace RobotTest diff --git a/tests/sdl/SDLTestMain.cpp b/tests/sdl/SDLTestMain.cpp new file mode 100644 index 0000000..8ab7ffd --- /dev/null +++ b/tests/sdl/SDLTestMain.cpp @@ -0,0 +1,33 @@ +#include +#include +#include +#include + +#include "TestConfig.h" + +int main(int argc, char** argv) { + // Parse wait time + int waitTime = 2000; + for (int i = 1; i < argc; ++i) { + if (std::string(argv[i]) == "--wait-time" && i + 1 < argc) { + waitTime = std::stoi(argv[i + 1]); + for (int j = i; j < argc - 2; ++j) { + argv[j] = argv[j + 2]; + } + argc -= 2; + break; + } + else if (std::string(argv[i]) == "--run-tests") { + // Replace with gtest filter to exclude any specific tests if needed + argv[i] = const_cast("--gtest_filter=*"); + } + } + + ::testing::InitGoogleTest(&argc, argv); + + std::cout << "Running automated tests..." << std::endl; + std::cout << "Waiting " << waitTime / 1000.0 << " seconds before starting tests..." << std::endl; + + SDL_Delay(static_cast(waitTime)); + return RUN_ALL_TESTS(); +} diff --git a/tests/sdl/TestConfig.h b/tests/sdl/TestConfig.h new file mode 100644 index 0000000..cc2e8c0 --- /dev/null +++ b/tests/sdl/TestConfig.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include + +namespace RobotTest { + struct TestConfig { + // Window settings + int windowWidth = 800; + int windowHeight = 600; + std::string windowTitle = "Robot CPP Testing Framework"; + + // Test execution settings + bool runTests = false; + std::chrono::milliseconds initialWaitTime{6000}; + std::chrono::seconds testTimeout{60}; + + // Delay settings for animation and visualization + std::chrono::milliseconds frameDelay{16}; // ~60 FPS + std::chrono::milliseconds setupDelay{1500}; + std::chrono::milliseconds actionDelay{900}; + + // Window positioning + int windowX = 50; + int windowY = 50; + + // Mouse test settings + int dragOffsetX = 100; + int dragOffsetY = 50; + int positionTolerance = 20; // Pixels + + static TestConfig fromCommandLine(int argc, char **argv) { + TestConfig config; + + for (int i = 1; i < argc; i++) { + std::string arg = argv[i]; + + if (arg == "--run-tests") { + config.runTests = true; + } else if (arg == "--wait-time" && i + 1 < argc) { + config.initialWaitTime = std::chrono::milliseconds(std::stoi(argv[i + 1])); + i++; + } else if (arg == "--action-delay" && i + 1 < argc) { + config.actionDelay = std::chrono::milliseconds(std::stoi(argv[i + 1])); + i++; + } + } + + return config; + } + }; +} // namespace RobotTest diff --git a/tests/sdl/TestContext.h b/tests/sdl/TestContext.h new file mode 100644 index 0000000..a996ba8 --- /dev/null +++ b/tests/sdl/TestContext.h @@ -0,0 +1,143 @@ +#pragma once + +#include +#include +#include +#include +#include +#include "TestConfig.h" + +namespace RobotTest { + +class TestContext { +public: + explicit TestContext(const TestConfig& config) : config_(config) { + if (SDL_Init(SDL_INIT_VIDEO) < 0) { + throw std::runtime_error(std::string("SDL init error: ") + SDL_GetError()); + } + + initialized_ = true; + + window_ = SDL_CreateWindow( + config_.windowTitle.c_str(), + SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, + config_.windowWidth, config_.windowHeight, + SDL_WINDOW_SHOWN + ); + + if (!window_) { + throw std::runtime_error(std::string("Window creation error: ") + SDL_GetError()); + } + + renderer_ = SDL_CreateRenderer( + window_, -1, + SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC + ); + + if (!renderer_) { + throw std::runtime_error(std::string("Renderer creation error: ") + SDL_GetError()); + } + + SDL_RaiseWindow(window_); + SDL_SetWindowPosition(window_, config_.windowX, config_.windowY); + } + + ~TestContext() { + if (renderer_) SDL_DestroyRenderer(renderer_); + if (window_) SDL_DestroyWindow(window_); + if (initialized_) SDL_Quit(); + } + + // Prevent copying + TestContext(const TestContext&) = delete; + TestContext& operator=(const TestContext&) = delete; + + // Allow moving + TestContext(TestContext&& other) noexcept + : window_(other.window_), + renderer_(other.renderer_), + initialized_(other.initialized_), + config_(other.config_) { + other.window_ = nullptr; + other.renderer_ = nullptr; + other.initialized_ = false; + } + + TestContext& operator=(TestContext&& other) noexcept { + if (this != &other) { + if (renderer_) SDL_DestroyRenderer(renderer_); + if (window_) SDL_DestroyWindow(window_); + if (initialized_) SDL_Quit(); + + window_ = other.window_; + renderer_ = other.renderer_; + initialized_ = other.initialized_; + config_ = other.config_; + + other.window_ = nullptr; + other.renderer_ = nullptr; + other.initialized_ = false; + } + return *this; + } + + SDL_Renderer* getRenderer() const { return renderer_; } + SDL_Window* getWindow() const { return window_; } + const TestConfig& getConfig() const { return config_; } + + void prepareForTests() { + SDL_ShowWindow(window_); + SDL_SetWindowPosition(window_, config_.windowX, config_.windowY); + SDL_RaiseWindow(window_); + + for (int i = 0; i < 5; i++) { + renderFrame([](SDL_Renderer* renderer) { + SDL_SetRenderDrawColor(renderer, 40, 40, 40, 255); + SDL_RenderClear(renderer); + }); + SDL_Delay(100); + } + + SDL_Event event; + while (SDL_PollEvent(&event)) { /* Drain event queue */ } + + SDL_Delay(static_cast(config_.setupDelay.count())); + + int x, y; + SDL_GetWindowPosition(window_, &x, &y); + printf("Window position: (%d, %d)\n", x, y); + } + + void handleEvents(bool& running) { + SDL_Event event; + while (SDL_PollEvent(&event)) { + if (event.type == SDL_QUIT) { + running = false; + } + + for (const auto& handler : eventHandlers_) { + handler(event); + } + } + } + + void renderFrame(const std::function& renderFunction) { + SDL_SetRenderDrawColor(renderer_, 40, 40, 40, 255); + SDL_RenderClear(renderer_); + renderFunction(renderer_); + SDL_RenderPresent(renderer_); + } + + void addEventHandler(std::function handler) { + eventHandlers_.push_back(std::move(handler)); + } + +private: + SDL_Window* window_ = nullptr; + SDL_Renderer* renderer_ = nullptr; + bool initialized_ = false; + TestConfig config_; + std::vector> eventHandlers_; +}; + +} // namespace RobotTest diff --git a/tests/sdl/TestElements.h b/tests/sdl/TestElements.h new file mode 100644 index 0000000..c894a53 --- /dev/null +++ b/tests/sdl/TestElements.h @@ -0,0 +1,371 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace RobotTest { + +struct Color { + uint8_t r, g, b, a; + + static constexpr Color White() { return {255, 255, 255, 255}; } + static constexpr Color Black() { return {0, 0, 0, 255}; } + static constexpr Color Red() { return {255, 0, 0, 255}; } + static constexpr Color Green() { return {0, 255, 0, 255}; } + static constexpr Color Blue() { return {0, 0, 255, 255}; } + static constexpr Color Yellow() { return {255, 255, 0, 255}; } + static constexpr Color Orange() { return {255, 165, 0, 255}; } + + [[nodiscard]] constexpr Color darken(float factor) const noexcept { + const auto adjustment = [factor](uint8_t value) -> uint8_t { + return static_cast(static_cast(value) * (1.0f - factor)); + }; + return {adjustment(r), adjustment(g), adjustment(b), a}; + } + + [[nodiscard]] SDL_Color toSDL() const noexcept { + return {r, g, b, a}; + } +}; + +class TestElement { +public: + virtual ~TestElement() = default; + virtual void draw(SDL_Renderer* renderer) const = 0; + [[nodiscard]] virtual bool isInside(int x, int y) const = 0; + virtual void reset() = 0; + [[nodiscard]] virtual SDL_Rect getRect() const = 0; + [[nodiscard]] virtual std::string_view getName() const = 0; +}; + +class DragElement : public TestElement { +public: + DragElement(SDL_Rect rect, Color color, std::string name) + : rect_(rect), originalRect_(rect), color_(color), name_(std::move(name)), dragging_(false) {} + + void draw(SDL_Renderer* renderer) const override { + SDL_SetRenderDrawColor(renderer, color_.r, color_.g, color_.b, color_.a); + SDL_RenderFillRect(renderer, &rect_); + SDL_SetRenderDrawColor(renderer, Color::White().r, Color::White().g, Color::White().b, Color::White().a); + SDL_RenderDrawRect(renderer, &rect_); + } + + [[nodiscard]] bool isInside(int x, int y) const override { + return (x >= rect_.x && x < rect_.x + rect_.w && + y >= rect_.y && y < rect_.y + rect_.h); + } + + void startDrag() { dragging_ = true; } + void stopDrag() { dragging_ = false; } + + void moveTo(int x, int y) { + if (dragging_) { + rect_.x = x - rect_.w/2; + rect_.y = y - rect_.h/2; + } + } + + void reset() override { + rect_ = originalRect_; + dragging_ = false; + } + + [[nodiscard]] SDL_Rect getRect() const override { return rect_; } + [[nodiscard]] std::string_view getName() const override { return name_; } + [[nodiscard]] bool isDragging() const { return dragging_; } + +private: + SDL_Rect rect_; + SDL_Rect originalRect_; + Color color_; + std::string name_; + bool dragging_; +}; + +class TestButton : public TestElement { +public: + using ClickCallback = std::function; + + TestButton(SDL_Rect rect, Color color, std::string name, + std::optional callback = std::nullopt) + : rect_(rect), color_(color), name_(std::move(name)), + clicked_(false), callback_(std::move(callback)) {} + + void draw(SDL_Renderer* renderer) const override { + const Color drawColor = clicked_ ? color_ : color_.darken(0.5f); + SDL_SetRenderDrawColor(renderer, drawColor.r, drawColor.g, drawColor.b, drawColor.a); + SDL_RenderFillRect(renderer, &rect_); + SDL_SetRenderDrawColor(renderer, Color::White().r, Color::White().g, Color::White().b, Color::White().a); + SDL_RenderDrawRect(renderer, &rect_); + } + + [[nodiscard]] bool isInside(int x, int y) const override { + return (x >= rect_.x && x < rect_.x + rect_.w && + y >= rect_.y && y < rect_.y + rect_.h); + } + + void handleClick() { + clicked_ = !clicked_; + if (callback_ && clicked_) { + (*callback_)(); + } + } + + [[nodiscard]] bool wasClicked() const { return clicked_; } + void reset() override { clicked_ = false; } + [[nodiscard]] SDL_Rect getRect() const override { return rect_; } + [[nodiscard]] std::string_view getName() const override { return name_; } + +private: + SDL_Rect rect_; + Color color_; + std::string name_; + bool clicked_; + std::optional callback_; +}; + +class DoubleClickButton : public TestElement { +public: + DoubleClickButton(SDL_Rect rect, Color color, std::string name) + : rect_(rect), color_(color), name_(std::move(name)), + clicked_(false), doubleClicked_(false), + lastClickTime_(std::chrono::steady_clock::now() - std::chrono::seconds(10)) {} + + void draw(SDL_Renderer* renderer) const override { + Color drawColor = doubleClicked_ ? color_ : + clicked_ ? color_.darken(0.3f) : color_.darken(0.6f); + + SDL_SetRenderDrawColor(renderer, drawColor.r, drawColor.g, drawColor.b, drawColor.a); + SDL_RenderFillRect(renderer, &rect_); + + SDL_SetRenderDrawColor(renderer, Color::White().r, Color::White().g, Color::White().b, Color::White().a); + SDL_RenderDrawRect(renderer, &rect_); + } + + [[nodiscard]] bool isInside(int x, int y) const override { + return (x >= rect_.x && x < rect_.x + rect_.w && + y >= rect_.y && y < rect_.y + rect_.h); + } + + void handleClick() { + auto now = std::chrono::steady_clock::now(); + auto timeSinceLastClick = std::chrono::duration_cast( + now - lastClickTime_).count(); + + if (timeSinceLastClick < 300) { // Double-click threshold + doubleClicked_ = true; + } else { + clicked_ = true; + doubleClicked_ = false; + } + + lastClickTime_ = now; + } + + [[nodiscard]] bool wasClicked() const { return clicked_; } + [[nodiscard]] bool wasDoubleClicked() const { return doubleClicked_; } + + void reset() override { + clicked_ = false; + doubleClicked_ = false; + lastClickTime_ = std::chrono::steady_clock::now() - std::chrono::seconds(10); + } + + [[nodiscard]] SDL_Rect getRect() const override { return rect_; } + [[nodiscard]] std::string_view getName() const override { return name_; } + +private: + SDL_Rect rect_; + Color color_; + std::string name_; + bool clicked_; + bool doubleClicked_; + std::chrono::time_point lastClickTime_; +}; + +class RightClickButton : public TestElement { +public: + RightClickButton(SDL_Rect rect, Color color, std::string name) + : rect_(rect), color_(color), name_(std::move(name)), rightClicked_(false) {} + + void draw(SDL_Renderer* renderer) const override { + Color drawColor = rightClicked_ ? color_ : color_.darken(0.5f); + SDL_SetRenderDrawColor(renderer, drawColor.r, drawColor.g, drawColor.b, drawColor.a); + SDL_RenderFillRect(renderer, &rect_); + + SDL_SetRenderDrawColor(renderer, Color::White().r, Color::White().g, Color::White().b, Color::White().a); + SDL_RenderDrawRect(renderer, &rect_); + } + + [[nodiscard]] bool isInside(int x, int y) const override { + return (x >= rect_.x && x < rect_.x + rect_.w && + y >= rect_.y && y < rect_.y + rect_.h); + } + + void handleRightClick() { rightClicked_ = true; } + [[nodiscard]] bool wasRightClicked() const { return rightClicked_; } + void reset() override { rightClicked_ = false; } + [[nodiscard]] SDL_Rect getRect() const override { return rect_; } + [[nodiscard]] std::string_view getName() const override { return name_; } + +private: + SDL_Rect rect_; + Color color_; + std::string name_; + bool rightClicked_; +}; + +class ScrollArea : public TestElement { +public: + ScrollArea(SDL_Rect rect, Color color, std::string name) + : rect_(rect), color_(color), name_(std::move(name)), scrollY_(0), + contentHeight_(500) // Content is taller than visible area + {} + + void draw(SDL_Renderer* renderer) const override { + // Draw visible area background + SDL_SetRenderDrawColor(renderer, color_.r, color_.g, color_.b, color_.a); + SDL_RenderFillRect(renderer, &rect_); + + // Draw border + SDL_SetRenderDrawColor(renderer, Color::Black().r, Color::Black().g, Color::Black().b, Color::Black().a); + SDL_RenderDrawRect(renderer, &rect_); + + // Set up a clipping rectangle for the content area + SDL_Rect clipRect = rect_; + SDL_RenderSetClipRect(renderer, &clipRect); + + // Draw content (series of colored lines) + const int lineHeight = 20; + const int numLines = contentHeight_ / lineHeight; + + for (int i = 0; i < numLines; ++i) { + // Calculate line Y position with scroll offset + int lineY = rect_.y + (i * lineHeight) - scrollY_; + + // Skip if line is outside visible area + if (lineY + lineHeight < rect_.y || lineY > rect_.y + rect_.h) { + continue; + } + + // Alternate colors + Color lineColor = (i % 2 == 0) ? Color::Blue() : Color::Green(); + SDL_SetRenderDrawColor(renderer, lineColor.r, lineColor.g, lineColor.b, lineColor.a); + + SDL_Rect lineRect = {rect_.x + 2, lineY, rect_.w - 4, lineHeight}; + SDL_RenderFillRect(renderer, &lineRect); + } + + // Draw scrollbar track + SDL_Rect scrollTrack = { + rect_.x + rect_.w - 15, + rect_.y, + 15, + rect_.h + }; + SDL_SetRenderDrawColor(renderer, 50, 50, 50, 255); + SDL_RenderFillRect(renderer, &scrollTrack); + + // Draw scrollbar thumb + float visibleRatio = static_cast(rect_.h) / contentHeight_; + int thumbHeight = static_cast(rect_.h * visibleRatio); + int thumbY = rect_.y + static_cast((static_cast(scrollY_) / + (contentHeight_ - rect_.h)) * (rect_.h - thumbHeight)); + + SDL_Rect scrollThumb = { + rect_.x + rect_.w - 15, + thumbY, + 15, + thumbHeight + }; + SDL_SetRenderDrawColor(renderer, 150, 150, 150, 255); + SDL_RenderFillRect(renderer, &scrollThumb); + + // Reset clipping rectangle + SDL_RenderSetClipRect(renderer, nullptr); + } + + [[nodiscard]] bool isInside(int x, int y) const override { + return (x >= rect_.x && x < rect_.x + rect_.w && + y >= rect_.y && y < rect_.y + rect_.h); + } + + void handleScroll(int scrollAmount) { + scrollY_ += scrollAmount * 15; // Scale the scroll amount + + // Clamp scrollY to valid range + if (scrollY_ < 0) { + scrollY_ = 0; + } + + int maxScroll = contentHeight_ - rect_.h; + if (maxScroll < 0) { + maxScroll = 0; + } + + if (scrollY_ > maxScroll) { + scrollY_ = maxScroll; + } + } + + [[nodiscard]] int getScrollY() const { return scrollY_; } + void reset() override { scrollY_ = 0; } + [[nodiscard]] SDL_Rect getRect() const override { return rect_; } + [[nodiscard]] std::string_view getName() const override { return name_; } + +private: + SDL_Rect rect_; + Color color_; + std::string name_; + int scrollY_; + int contentHeight_; +}; + +// Factory functions +inline std::unique_ptr createDragElement( + int x, int y, int width, int height, Color color, std::string name) +{ + return std::make_unique( + SDL_Rect{x, y, width, height}, color, std::move(name) + ); +} + +inline std::unique_ptr createButton( + int x, int y, int width, int height, Color color, std::string name, + TestButton::ClickCallback callback = nullptr) +{ + return std::make_unique( + SDL_Rect{x, y, width, height}, color, std::move(name), callback + ); +} + +inline std::unique_ptr createDoubleClickButton( + int x, int y, int width, int height, Color color, std::string name) +{ + return std::make_unique( + SDL_Rect{x, y, width, height}, color, std::move(name) + ); +} + +inline std::unique_ptr createRightClickButton( + int x, int y, int width, int height, Color color, std::string name) +{ + return std::make_unique( + SDL_Rect{x, y, width, height}, color, std::move(name) + ); +} + +inline std::unique_ptr createScrollArea( + int x, int y, int width, int height, Color color, std::string name) +{ + return std::make_unique( + SDL_Rect{x, y, width, height}, color, std::move(name) + ); +} + +} // namespace RobotTest