From f6ad6e971f2572010b3787247093eeb534944e9c Mon Sep 17 00:00:00 2001 From: scawful Date: Sun, 5 Oct 2025 23:44:38 -0400 Subject: [PATCH] refactor: Update CMake Configuration and Implement Canvas Automation Tests - Updated CMakeLists.txt to include additional proto files for canvas automation, enhancing the build process for gRPC services. - Refactored file_util.cc to use the core namespace for feature flags, improving code clarity and consistency. - Added new unit tests for CanvasAutomationAPI, covering various functionalities such as coordinate conversion, tile operations, and selection management, ensuring robust testing for canvas automation features. --- src/CMakeLists.txt | 9 +- src/util/file_util.cc | 12 +- test/CMakeLists.txt | 2 + test/test.cmake | 1 + test/unit/gui/canvas_automation_api_test.cc | 477 ++++++++++++++++++++ 5 files changed, 492 insertions(+), 9 deletions(-) create mode 100644 test/unit/gui/canvas_automation_api_test.cc diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index cd9ceeed..0cbd2ac8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -308,7 +308,8 @@ if (YAZE_BUILD_APP) if(NOT YAZE_USE_MODULAR_BUILD) target_add_protobuf(yaze - ${CMAKE_SOURCE_DIR}/src/app/core/proto/imgui_test_harness.proto) + ${CMAKE_SOURCE_DIR}/src/protos/imgui_test_harness.proto + ${CMAKE_SOURCE_DIR}/src/protos/canvas_automation.proto) target_sources(yaze PRIVATE ${CMAKE_SOURCE_DIR}/src/app/core/service/imgui_test_harness_service.cc @@ -579,7 +580,8 @@ if (YAZE_BUILD_LIB) if(YAZE_WITH_GRPC) target_add_protobuf(yaze_core - ${CMAKE_SOURCE_DIR}/src/app/core/proto/imgui_test_harness.proto) + ${CMAKE_SOURCE_DIR}/src/protos/imgui_test_harness.proto + ${CMAKE_SOURCE_DIR}/src/protos/canvas_automation.proto) target_link_libraries(yaze_core PRIVATE grpc++ @@ -645,7 +647,8 @@ if (YAZE_BUILD_LIB) if(YAZE_WITH_GRPC) target_add_protobuf(yaze_c - ${CMAKE_SOURCE_DIR}/src/app/core/proto/imgui_test_harness.proto) + ${CMAKE_SOURCE_DIR}/src/protos/imgui_test_harness.proto + ${CMAKE_SOURCE_DIR}/src/protos/canvas_automation.proto) target_link_libraries(yaze_c PRIVATE grpc++ diff --git a/src/util/file_util.cc b/src/util/file_util.cc index 5ac92028..9fefeab5 100644 --- a/src/util/file_util.cc +++ b/src/util/file_util.cc @@ -191,7 +191,7 @@ std::string GetConfigDirectory() { std::string FileDialogWrapper::ShowOpenFileDialog() { // Use global feature flag to choose implementation - if (FeatureFlags::get().kUseNativeFileDialog) { + if (core::FeatureFlags::get().kUseNativeFileDialog) { return ShowOpenFileDialogNFD(); } else { return ShowOpenFileDialogBespoke(); @@ -282,7 +282,7 @@ std::string FileDialogWrapper::ShowOpenFileDialogBespoke() { std::string FileDialogWrapper::ShowSaveFileDialog(const std::string& default_name, const std::string& default_extension) { // Use global feature flag to choose implementation - if (FeatureFlags::get().kUseNativeFileDialog) { + if (core::FeatureFlags::get().kUseNativeFileDialog) { return ShowSaveFileDialogNFD(default_name, default_extension); } else { return ShowSaveFileDialogBespoke(default_name, default_extension); @@ -415,7 +415,7 @@ std::string FileDialogWrapper::ShowSaveFileDialogBespoke(const std::string& defa std::string FileDialogWrapper::ShowOpenFolderDialog() { // Use global feature flag to choose implementation - if (FeatureFlags::get().kUseNativeFileDialog) { + if (core::FeatureFlags::get().kUseNativeFileDialog) { return ShowOpenFolderDialogNFD(); } else { return ShowOpenFolderDialogBespoke(); @@ -542,7 +542,7 @@ std::vector FileDialogWrapper::GetFilesInFolder( std::string FileDialogWrapper::ShowOpenFileDialog() { // Use global feature flag to choose implementation - if (FeatureFlags::get().kUseNativeFileDialog) { + if (core::FeatureFlags::get().kUseNativeFileDialog) { return ShowOpenFileDialogNFD(); } else { return ShowOpenFileDialogBespoke(); @@ -643,7 +643,7 @@ std::string FileDialogWrapper::ShowSaveFileDialogBespoke(const std::string& defa std::string FileDialogWrapper::ShowSaveFileDialog(const std::string& default_name, const std::string& default_extension) { // Use global feature flag to choose implementation - if (FeatureFlags::get().kUseNativeFileDialog) { + if (core::FeatureFlags::get().kUseNativeFileDialog) { return ShowSaveFileDialogNFD(default_name, default_extension); } else { return ShowSaveFileDialogBespoke(default_name, default_extension); @@ -652,7 +652,7 @@ std::string FileDialogWrapper::ShowSaveFileDialog(const std::string& default_nam std::string FileDialogWrapper::ShowOpenFolderDialog() { // Use global feature flag to choose implementation - if (FeatureFlags::get().kUseNativeFileDialog) { + if (core::FeatureFlags::get().kUseNativeFileDialog) { return ShowOpenFolderDialogNFD(); } else { return ShowOpenFolderDialogBespoke(); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index d51dad5f..2fba771b 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -35,6 +35,7 @@ if(YAZE_BUILD_TESTS AND NOT YAZE_BUILD_TESTS STREQUAL "OFF") unit/gfx/compression_test.cc unit/gfx/snes_palette_test.cc unit/gui/tile_selector_widget_test.cc + unit/gui/canvas_automation_api_test.cc unit/zelda3/message_test.cc unit/zelda3/overworld_test.cc unit/zelda3/object_parser_test.cc @@ -99,6 +100,7 @@ if(YAZE_BUILD_TESTS AND NOT YAZE_BUILD_TESTS STREQUAL "OFF") unit/gfx/compression_test.cc unit/gfx/snes_palette_test.cc unit/gui/tile_selector_widget_test.cc + unit/gui/canvas_automation_api_test.cc unit/zelda3/message_test.cc unit/zelda3/overworld_test.cc unit/zelda3/object_parser_test.cc diff --git a/test/test.cmake b/test/test.cmake index 0959df07..9c1074b4 100644 --- a/test/test.cmake +++ b/test/test.cmake @@ -80,6 +80,7 @@ source_group("Tests\\Unit" FILES unit/gfx/compression_test.cc unit/gfx/snes_palette_test.cc unit/gui/tile_selector_widget_test.cc + unit/gui/canvas_automation_api_test.cc unit/zelda3/message_test.cc unit/zelda3/overworld_test.cc unit/zelda3/object_parser_test.cc diff --git a/test/unit/gui/canvas_automation_api_test.cc b/test/unit/gui/canvas_automation_api_test.cc new file mode 100644 index 00000000..ea647a5c --- /dev/null +++ b/test/unit/gui/canvas_automation_api_test.cc @@ -0,0 +1,477 @@ +#include "app/gui/canvas/canvas_automation_api.h" + +#include +#include + +#include "app/gui/canvas.h" +#include "testing.h" + +namespace yaze { +namespace test { + +using ::testing::Eq; +using ::testing::Ge; +using ::testing::Le; + +class CanvasAutomationAPITest : public ::testing::Test { + protected: + void SetUp() override { + // Create a test canvas with known dimensions + canvas_ = std::make_unique("TestCanvas", ImVec2(512, 512), + gui::CanvasGridSize::k16x16); + api_ = canvas_->GetAutomationAPI(); + ASSERT_NE(api_, nullptr); + } + + std::unique_ptr canvas_; + gui::CanvasAutomationAPI* api_; +}; + +// ============================================================================ +// Coordinate Conversion Tests +// ============================================================================ + +TEST_F(CanvasAutomationAPITest, TileToCanvas_BasicConversion) { + // At 1.0x zoom, tile (0,0) should be at canvas (0,0) + canvas_->set_global_scale(1.0f); + + ImVec2 pos = api_->TileToCanvas(0, 0); + EXPECT_FLOAT_EQ(pos.x, 0.0f); + EXPECT_FLOAT_EQ(pos.y, 0.0f); + + // Tile (1,0) should be at (16,0) for 16x16 grid + pos = api_->TileToCanvas(1, 0); + EXPECT_FLOAT_EQ(pos.x, 16.0f); + EXPECT_FLOAT_EQ(pos.y, 0.0f); + + // Tile (0,1) should be at (0,16) + pos = api_->TileToCanvas(0, 1); + EXPECT_FLOAT_EQ(pos.x, 0.0f); + EXPECT_FLOAT_EQ(pos.y, 16.0f); + + // Tile (10,10) should be at (160,160) + pos = api_->TileToCanvas(10, 10); + EXPECT_FLOAT_EQ(pos.x, 160.0f); + EXPECT_FLOAT_EQ(pos.y, 160.0f); +} + +TEST_F(CanvasAutomationAPITest, TileToCanvas_WithZoom) { + // At 2.0x zoom, tile coordinates should scale + canvas_->set_global_scale(2.0f); + + ImVec2 pos = api_->TileToCanvas(1, 1); + EXPECT_FLOAT_EQ(pos.x, 32.0f); // 1 * 16 * 2.0 + EXPECT_FLOAT_EQ(pos.y, 32.0f); + + // At 0.5x zoom, tile coordinates should scale down + canvas_->set_global_scale(0.5f); + pos = api_->TileToCanvas(10, 10); + EXPECT_FLOAT_EQ(pos.x, 80.0f); // 10 * 16 * 0.5 + EXPECT_FLOAT_EQ(pos.y, 80.0f); +} + +TEST_F(CanvasAutomationAPITest, CanvasToTile_BasicConversion) { + canvas_->set_global_scale(1.0f); + + // Canvas (0,0) should be tile (0,0) + ImVec2 tile = api_->CanvasToTile(ImVec2(0, 0)); + EXPECT_FLOAT_EQ(tile.x, 0.0f); + EXPECT_FLOAT_EQ(tile.y, 0.0f); + + // Canvas (16,16) should be tile (1,1) + tile = api_->CanvasToTile(ImVec2(16, 16)); + EXPECT_FLOAT_EQ(tile.x, 1.0f); + EXPECT_FLOAT_EQ(tile.y, 1.0f); + + // Canvas (160,160) should be tile (10,10) + tile = api_->CanvasToTile(ImVec2(160, 160)); + EXPECT_FLOAT_EQ(tile.x, 10.0f); + EXPECT_FLOAT_EQ(tile.y, 10.0f); +} + +TEST_F(CanvasAutomationAPITest, CanvasToTile_WithZoom) { + // At 2.0x zoom + canvas_->set_global_scale(2.0f); + + ImVec2 tile = api_->CanvasToTile(ImVec2(32, 32)); + EXPECT_FLOAT_EQ(tile.x, 1.0f); // 32 / (16 * 2.0) + EXPECT_FLOAT_EQ(tile.y, 1.0f); + + // At 0.5x zoom + canvas_->set_global_scale(0.5f); + tile = api_->CanvasToTile(ImVec2(8, 8)); + EXPECT_FLOAT_EQ(tile.x, 1.0f); // 8 / (16 * 0.5) + EXPECT_FLOAT_EQ(tile.y, 1.0f); +} + +TEST_F(CanvasAutomationAPITest, CoordinateRoundTrip) { + canvas_->set_global_scale(1.0f); + + // Test round-trip conversion + for (int i = 0; i < 32; ++i) { + ImVec2 canvas_pos = api_->TileToCanvas(i, i); + ImVec2 tile_pos = api_->CanvasToTile(canvas_pos); + + EXPECT_FLOAT_EQ(tile_pos.x, static_cast(i)); + EXPECT_FLOAT_EQ(tile_pos.y, static_cast(i)); + } +} + +// ============================================================================ +// Bounds Checking Tests +// ============================================================================ + +TEST_F(CanvasAutomationAPITest, IsInBounds_ValidCoordinates) { + EXPECT_TRUE(api_->IsInBounds(0, 0)); + EXPECT_TRUE(api_->IsInBounds(10, 10)); + EXPECT_TRUE(api_->IsInBounds(31, 31)); // 512/16 = 32 tiles, so 31 is max +} + +TEST_F(CanvasAutomationAPITest, IsInBounds_InvalidCoordinates) { + EXPECT_FALSE(api_->IsInBounds(-1, 0)); + EXPECT_FALSE(api_->IsInBounds(0, -1)); + EXPECT_FALSE(api_->IsInBounds(-1, -1)); + EXPECT_FALSE(api_->IsInBounds(32, 0)); // Out of bounds + EXPECT_FALSE(api_->IsInBounds(0, 32)); + EXPECT_FALSE(api_->IsInBounds(100, 100)); +} + +// ============================================================================ +// Tile Operations Tests +// ============================================================================ + +TEST_F(CanvasAutomationAPITest, SetTileAt_WithCallback) { + // Set up a tile paint callback + std::vector> painted_tiles; + api_->SetTilePaintCallback([&](int x, int y, int tile_id) { + painted_tiles.push_back({x, y, tile_id}); + return true; + }); + + // Paint some tiles + EXPECT_TRUE(api_->SetTileAt(5, 5, 42)); + EXPECT_TRUE(api_->SetTileAt(10, 10, 100)); + + ASSERT_EQ(painted_tiles.size(), 2); + EXPECT_EQ(painted_tiles[0], std::make_tuple(5, 5, 42)); + EXPECT_EQ(painted_tiles[1], std::make_tuple(10, 10, 100)); +} + +TEST_F(CanvasAutomationAPITest, SetTileAt_OutOfBounds) { + bool callback_called = false; + api_->SetTilePaintCallback([&](int x, int y, int tile_id) { + callback_called = true; + return true; + }); + + // Out of bounds tiles should return false without calling callback + EXPECT_FALSE(api_->SetTileAt(-1, 0, 42)); + EXPECT_FALSE(api_->SetTileAt(0, -1, 42)); + EXPECT_FALSE(api_->SetTileAt(100, 100, 42)); + + EXPECT_FALSE(callback_called); +} + +TEST_F(CanvasAutomationAPITest, GetTileAt_WithCallback) { + // Set up a tile query callback + api_->SetTileQueryCallback([](int x, int y) { + return x * 100 + y; // Simple deterministic value + }); + + EXPECT_EQ(api_->GetTileAt(0, 0), 0); + EXPECT_EQ(api_->GetTileAt(1, 0), 100); + EXPECT_EQ(api_->GetTileAt(0, 1), 1); + EXPECT_EQ(api_->GetTileAt(5, 7), 507); +} + +TEST_F(CanvasAutomationAPITest, GetTileAt_OutOfBounds) { + api_->SetTileQueryCallback([](int x, int y) { return 42; }); + + EXPECT_EQ(api_->GetTileAt(-1, 0), -1); + EXPECT_EQ(api_->GetTileAt(0, -1), -1); + EXPECT_EQ(api_->GetTileAt(100, 100), -1); +} + +TEST_F(CanvasAutomationAPITest, SetTiles_BatchOperation) { + std::vector> painted_tiles; + api_->SetTilePaintCallback([&](int x, int y, int tile_id) { + painted_tiles.push_back({x, y, tile_id}); + return true; + }); + + std::vector> tiles_to_paint = { + {0, 0, 10}, + {1, 0, 11}, + {2, 0, 12}, + {0, 1, 20}, + {1, 1, 21} + }; + + EXPECT_TRUE(api_->SetTiles(tiles_to_paint)); + EXPECT_EQ(painted_tiles.size(), 5); +} + +// ============================================================================ +// Selection Tests +// ============================================================================ + +TEST_F(CanvasAutomationAPITest, SelectTile) { + api_->SelectTile(5, 5); + + auto selection = api_->GetSelection(); + EXPECT_TRUE(selection.has_selection); + EXPECT_EQ(selection.selected_tiles.size(), 1); +} + +TEST_F(CanvasAutomationAPITest, SelectTileRect) { + api_->SelectTileRect(5, 5, 9, 9); + + auto selection = api_->GetSelection(); + EXPECT_TRUE(selection.has_selection); + + // 5x5 rectangle = 25 tiles + EXPECT_EQ(selection.selected_tiles.size(), 25); + + // Check first and last tiles + EXPECT_FLOAT_EQ(selection.selected_tiles[0].x, 5.0f); + EXPECT_FLOAT_EQ(selection.selected_tiles[0].y, 5.0f); + EXPECT_FLOAT_EQ(selection.selected_tiles[24].x, 9.0f); + EXPECT_FLOAT_EQ(selection.selected_tiles[24].y, 9.0f); +} + +TEST_F(CanvasAutomationAPITest, SelectTileRect_SwappedCoordinates) { + // Should handle coordinates in any order + api_->SelectTileRect(9, 9, 5, 5); // Reversed + + auto selection = api_->GetSelection(); + EXPECT_TRUE(selection.has_selection); + EXPECT_EQ(selection.selected_tiles.size(), 25); +} + +TEST_F(CanvasAutomationAPITest, ClearSelection) { + api_->SelectTileRect(5, 5, 10, 10); + + auto selection = api_->GetSelection(); + EXPECT_TRUE(selection.has_selection); + + api_->ClearSelection(); + + selection = api_->GetSelection(); + EXPECT_FALSE(selection.has_selection); + EXPECT_EQ(selection.selected_tiles.size(), 0); +} + +TEST_F(CanvasAutomationAPITest, SelectTile_OutOfBounds) { + api_->SelectTile(-1, 0); + auto selection = api_->GetSelection(); + EXPECT_FALSE(selection.has_selection); + + api_->SelectTile(100, 100); + selection = api_->GetSelection(); + EXPECT_FALSE(selection.has_selection); +} + +// ============================================================================ +// View Operations Tests +// ============================================================================ + +TEST_F(CanvasAutomationAPITest, SetZoom_ValidRange) { + api_->SetZoom(1.0f); + EXPECT_FLOAT_EQ(api_->GetZoom(), 1.0f); + + api_->SetZoom(2.0f); + EXPECT_FLOAT_EQ(api_->GetZoom(), 2.0f); + + api_->SetZoom(0.5f); + EXPECT_FLOAT_EQ(api_->GetZoom(), 0.5f); +} + +TEST_F(CanvasAutomationAPITest, SetZoom_Clamping) { + // Should clamp to 0.25 - 4.0 range + api_->SetZoom(10.0f); + EXPECT_LE(api_->GetZoom(), 4.0f); + + api_->SetZoom(0.1f); + EXPECT_GE(api_->GetZoom(), 0.25f); + + api_->SetZoom(-1.0f); + EXPECT_GE(api_->GetZoom(), 0.25f); +} + +TEST_F(CanvasAutomationAPITest, ScrollToTile_ValidTile) { + // Should not crash when scrolling to valid tiles + api_->ScrollToTile(0, 0, true); + api_->ScrollToTile(10, 10, false); + api_->ScrollToTile(15, 15, true); + + // Just verify no crash - actual scroll behavior depends on ImGui state +} + +TEST_F(CanvasAutomationAPITest, ScrollToTile_OutOfBounds) { + // Should handle out of bounds gracefully + api_->ScrollToTile(-1, 0, true); + api_->ScrollToTile(100, 100, true); + + // Should not crash +} + +TEST_F(CanvasAutomationAPITest, CenterOn_ValidTile) { + // Should not crash when centering on valid tiles + api_->CenterOn(10, 10); + api_->CenterOn(0, 0); + api_->CenterOn(20, 20); + + // Verify scroll position changed (should be non-zero after centering on non-origin) + ImVec2 scroll = canvas_->scrolling(); + // Scroll values will depend on canvas size, just verify they're set +} + +TEST_F(CanvasAutomationAPITest, CenterOn_OutOfBounds) { + api_->CenterOn(-1, 0); + api_->CenterOn(100, 100); + + // Should not crash +} + +// ============================================================================ +// Query Operations Tests +// ============================================================================ + +TEST_F(CanvasAutomationAPITest, GetDimensions) { + canvas_->set_global_scale(1.0f); + + auto dims = api_->GetDimensions(); + EXPECT_EQ(dims.tile_size, 16); // 16x16 grid + EXPECT_EQ(dims.width_tiles, 32); // 512 / 16 + EXPECT_EQ(dims.height_tiles, 32); +} + +TEST_F(CanvasAutomationAPITest, GetDimensions_WithZoom) { + canvas_->set_global_scale(2.0f); + + auto dims = api_->GetDimensions(); + EXPECT_EQ(dims.tile_size, 16); + EXPECT_EQ(dims.width_tiles, 16); // 512 / (16 * 2.0) + EXPECT_EQ(dims.height_tiles, 16); +} + +TEST_F(CanvasAutomationAPITest, GetVisibleRegion) { + canvas_->set_global_scale(1.0f); + canvas_->set_scrolling(ImVec2(0, 0)); + + auto region = api_->GetVisibleRegion(); + + // At origin with no scroll, should start at (0,0) + EXPECT_GE(region.min_x, 0); + EXPECT_GE(region.min_y, 0); + + // Should have valid bounds + EXPECT_GE(region.max_x, region.min_x); + EXPECT_GE(region.max_y, region.min_y); +} + +TEST_F(CanvasAutomationAPITest, IsTileVisible_AtOrigin) { + canvas_->set_global_scale(1.0f); + canvas_->set_scrolling(ImVec2(0, 0)); + + // Tiles at origin should be visible + EXPECT_TRUE(api_->IsTileVisible(0, 0)); + EXPECT_TRUE(api_->IsTileVisible(1, 1)); + + // Tiles far away might not be visible (depends on canvas size) + // We just verify the method doesn't crash + api_->IsTileVisible(50, 50); +} + +TEST_F(CanvasAutomationAPITest, IsTileVisible_OutOfBounds) { + // Out of bounds tiles should return false + EXPECT_FALSE(api_->IsTileVisible(-1, 0)); + EXPECT_FALSE(api_->IsTileVisible(0, -1)); + EXPECT_FALSE(api_->IsTileVisible(100, 100)); +} + +// ============================================================================ +// Integration Tests +// ============================================================================ + +TEST_F(CanvasAutomationAPITest, CompleteWorkflow) { + // Simulate a complete automation workflow + + // 1. Set zoom level + api_->SetZoom(1.0f); + EXPECT_FLOAT_EQ(api_->GetZoom(), 1.0f); + + // 2. Select a tile region + api_->SelectTileRect(0, 0, 4, 4); + auto selection = api_->GetSelection(); + EXPECT_EQ(selection.selected_tiles.size(), 25); + + // 3. Query tile data with callback + api_->SetTileQueryCallback([](int x, int y) { + return x + y * 100; + }); + + EXPECT_EQ(api_->GetTileAt(2, 3), 302); + + // 4. Paint tiles with callback + std::vector> painted; + api_->SetTilePaintCallback([&](int x, int y, int tile_id) { + painted.push_back({x, y, tile_id}); + return true; + }); + + std::vector> tiles = { + {0, 0, 10}, {1, 0, 11}, {2, 0, 12} + }; + EXPECT_TRUE(api_->SetTiles(tiles)); + EXPECT_EQ(painted.size(), 3); + + // 5. Clear selection + api_->ClearSelection(); + selection = api_->GetSelection(); + EXPECT_FALSE(selection.has_selection); +} + +TEST_F(CanvasAutomationAPITest, DifferentGridSizes) { + // Test with 8x8 grid + auto canvas_8x8 = std::make_unique( + "Test8x8", ImVec2(512, 512), gui::CanvasGridSize::k8x8); + auto api_8x8 = canvas_8x8->GetAutomationAPI(); + + auto dims = api_8x8->GetDimensions(); + EXPECT_EQ(dims.tile_size, 8); + EXPECT_EQ(dims.width_tiles, 64); // 512 / 8 + + // Test with 32x32 grid + auto canvas_32x32 = std::make_unique( + "Test32x32", ImVec2(512, 512), gui::CanvasGridSize::k32x32); + auto api_32x32 = canvas_32x32->GetAutomationAPI(); + + dims = api_32x32->GetDimensions(); + EXPECT_EQ(dims.tile_size, 32); + EXPECT_EQ(dims.width_tiles, 16); // 512 / 32 +} + +TEST_F(CanvasAutomationAPITest, MultipleZoomLevels) { + float zoom_levels[] = {0.25f, 0.5f, 1.0f, 1.5f, 2.0f, 3.0f, 4.0f}; + + for (float zoom : zoom_levels) { + api_->SetZoom(zoom); + float actual_zoom = api_->GetZoom(); + + // Should be clamped to valid range + EXPECT_GE(actual_zoom, 0.25f); + EXPECT_LE(actual_zoom, 4.0f); + + // Coordinate conversion should still work + ImVec2 canvas_pos = api_->TileToCanvas(10, 10); + ImVec2 tile_pos = api_->CanvasToTile(canvas_pos); + + EXPECT_FLOAT_EQ(tile_pos.x, 10.0f); + EXPECT_FLOAT_EQ(tile_pos.y, 10.0f); + } +} + +} // namespace test +} // namespace yaze +