backend-infra-engineer: Release v0.3.2 snapshot
This commit is contained in:
477
test/unit/gui/canvas_automation_api_test.cc
Normal file
477
test/unit/gui/canvas_automation_api_test.cc
Normal file
@@ -0,0 +1,477 @@
|
||||
#include "app/gui/canvas/canvas_automation_api.h"
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "app/gui/canvas/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<gui::Canvas>("TestCanvas", ImVec2(512, 512),
|
||||
gui::CanvasGridSize::k16x16);
|
||||
api_ = canvas_->GetAutomationAPI();
|
||||
ASSERT_NE(api_, nullptr);
|
||||
}
|
||||
|
||||
std::unique_ptr<gui::Canvas> 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<float>(i));
|
||||
EXPECT_FLOAT_EQ(tile_pos.y, static_cast<float>(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<std::tuple<int, int, int>> 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<std::tuple<int, int, int>> painted_tiles;
|
||||
api_->SetTilePaintCallback([&](int x, int y, int tile_id) {
|
||||
painted_tiles.push_back({x, y, tile_id});
|
||||
return true;
|
||||
});
|
||||
|
||||
std::vector<std::tuple<int, int, int>> 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<std::tuple<int, int, int>> painted;
|
||||
api_->SetTilePaintCallback([&](int x, int y, int tile_id) {
|
||||
painted.push_back({x, y, tile_id});
|
||||
return true;
|
||||
});
|
||||
|
||||
std::vector<std::tuple<int, int, int>> 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<gui::Canvas>(
|
||||
"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<gui::Canvas>(
|
||||
"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
|
||||
|
||||
297
test/unit/gui/canvas_coordinate_sync_test.cc
Normal file
297
test/unit/gui/canvas_coordinate_sync_test.cc
Normal file
@@ -0,0 +1,297 @@
|
||||
#include "app/gui/canvas/canvas.h"
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "testing.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
using ::testing::Eq;
|
||||
using ::testing::FloatEq;
|
||||
using ::testing::Ne;
|
||||
|
||||
/**
|
||||
* @brief Tests for canvas coordinate synchronization
|
||||
*
|
||||
* These tests verify that the canvas coordinate system properly tracks
|
||||
* mouse position for both hover and paint operations, fixing the regression
|
||||
* where CheckForCurrentMap() in OverworldEditor was using raw ImGui mouse
|
||||
* position instead of canvas-local coordinates.
|
||||
*
|
||||
* Regression: overworld_editor.cc:1041 was using ImGui::GetIO().MousePos
|
||||
* instead of canvas hover position, causing map highlighting to break.
|
||||
*/
|
||||
class CanvasCoordinateSyncTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
// Create a test canvas with known dimensions (4096x4096 for overworld)
|
||||
canvas_ = std::make_unique<gui::Canvas>("OverworldCanvas", ImVec2(4096, 4096),
|
||||
gui::CanvasGridSize::k16x16);
|
||||
canvas_->set_global_scale(1.0f);
|
||||
}
|
||||
|
||||
std::unique_ptr<gui::Canvas> canvas_;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Hover Position Tests (hover_mouse_pos)
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(CanvasCoordinateSyncTest, HoverMousePos_InitialState) {
|
||||
// Hover position should start at (0,0) or invalid state
|
||||
auto hover_pos = canvas_->hover_mouse_pos();
|
||||
|
||||
// Initial state may be (0,0) - this is valid
|
||||
EXPECT_GE(hover_pos.x, 0.0f);
|
||||
EXPECT_GE(hover_pos.y, 0.0f);
|
||||
}
|
||||
|
||||
TEST_F(CanvasCoordinateSyncTest, HoverMousePos_IndependentFromDrawnPos) {
|
||||
// Hover position and drawn tile position are independent
|
||||
// hover_mouse_pos() tracks continuous mouse movement
|
||||
// drawn_tile_position() only updates during painting
|
||||
|
||||
auto hover_pos = canvas_->hover_mouse_pos();
|
||||
auto drawn_pos = canvas_->drawn_tile_position();
|
||||
|
||||
// These may differ - hover tracks all movement, drawn only tracks paint
|
||||
// We just verify both are valid (non-negative or expected sentinel values)
|
||||
EXPECT_TRUE(hover_pos.x >= 0.0f || hover_pos.x == -1.0f);
|
||||
EXPECT_TRUE(drawn_pos.x >= 0.0f || drawn_pos.x == -1.0f);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Coordinate Space Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(CanvasCoordinateSyncTest, CoordinateSpace_WorldNotScreen) {
|
||||
// REGRESSION TEST: Verify hover_mouse_pos() returns world coordinates
|
||||
// not screen coordinates. The bug was using ImGui::GetIO().MousePos
|
||||
// which is in screen space and doesn't account for scrolling/canvas offset.
|
||||
|
||||
// Simulate scrolling the canvas
|
||||
canvas_->set_scrolling(ImVec2(100, 100));
|
||||
|
||||
// The hover position should be in canvas/world space, not affected by
|
||||
// the canvas's screen position. This is tested by ensuring the method
|
||||
// exists and returns a coordinate that could be used for map calculations.
|
||||
auto hover_pos = canvas_->hover_mouse_pos();
|
||||
|
||||
// Valid world coordinates should be usable for map index calculations
|
||||
// For a 512x512 map size (kOverworldMapSize = 512):
|
||||
// map_x = hover_pos.x / 512
|
||||
// map_y = hover_pos.y / 512
|
||||
|
||||
int map_x = static_cast<int>(hover_pos.x) / 512;
|
||||
int map_y = static_cast<int>(hover_pos.y) / 512;
|
||||
|
||||
// Map indices should be within valid range for 8x8 overworld grid
|
||||
EXPECT_GE(map_x, 0);
|
||||
EXPECT_GE(map_y, 0);
|
||||
EXPECT_LT(map_x, 64); // 8x8 grid = 64 maps max
|
||||
EXPECT_LT(map_y, 64);
|
||||
}
|
||||
|
||||
TEST_F(CanvasCoordinateSyncTest, MapCalculation_SmallMaps) {
|
||||
// Test map index calculation for standard 512x512 maps
|
||||
const int kOverworldMapSize = 512;
|
||||
|
||||
// Simulate hover at different world positions
|
||||
std::vector<ImVec2> test_positions = {
|
||||
ImVec2(0, 0), // Map (0, 0)
|
||||
ImVec2(512, 0), // Map (1, 0)
|
||||
ImVec2(0, 512), // Map (0, 1)
|
||||
ImVec2(512, 512), // Map (1, 1)
|
||||
ImVec2(1536, 1024), // Map (3, 2)
|
||||
};
|
||||
|
||||
std::vector<std::pair<int, int>> expected_maps = {
|
||||
{0, 0}, {1, 0}, {0, 1}, {1, 1}, {3, 2}
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < test_positions.size(); ++i) {
|
||||
ImVec2 pos = test_positions[i];
|
||||
int map_x = pos.x / kOverworldMapSize;
|
||||
int map_y = pos.y / kOverworldMapSize;
|
||||
|
||||
EXPECT_EQ(map_x, expected_maps[i].first);
|
||||
EXPECT_EQ(map_y, expected_maps[i].second);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(CanvasCoordinateSyncTest, MapCalculation_LargeMaps) {
|
||||
// Test map index calculation for ZSCustomOverworld v3 large maps (1024x1024)
|
||||
const int kLargeMapSize = 1024;
|
||||
|
||||
// Large maps should span multiple standard map coordinates
|
||||
std::vector<ImVec2> test_positions = {
|
||||
ImVec2(0, 0), // Large map (0, 0)
|
||||
ImVec2(1024, 0), // Large map (1, 0)
|
||||
ImVec2(0, 1024), // Large map (0, 1)
|
||||
ImVec2(2048, 2048), // Large map (2, 2)
|
||||
};
|
||||
|
||||
std::vector<std::pair<int, int>> expected_large_maps = {
|
||||
{0, 0}, {1, 0}, {0, 1}, {2, 2}
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < test_positions.size(); ++i) {
|
||||
ImVec2 pos = test_positions[i];
|
||||
int map_x = pos.x / kLargeMapSize;
|
||||
int map_y = pos.y / kLargeMapSize;
|
||||
|
||||
EXPECT_EQ(map_x, expected_large_maps[i].first);
|
||||
EXPECT_EQ(map_y, expected_large_maps[i].second);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Scale Invariance Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(CanvasCoordinateSyncTest, HoverPosition_ScaleInvariant) {
|
||||
// REGRESSION TEST: Hover position should be in world space regardless of scale
|
||||
// The bug was scale-dependent because it used screen coordinates
|
||||
|
||||
auto test_hover_at_scale = [&](float scale) {
|
||||
canvas_->set_global_scale(scale);
|
||||
auto hover_pos = canvas_->hover_mouse_pos();
|
||||
|
||||
// Hover position should be in world coordinates, not affected by scale
|
||||
// World coordinates are always in the range [0, canvas_size)
|
||||
EXPECT_GE(hover_pos.x, 0.0f);
|
||||
EXPECT_GE(hover_pos.y, 0.0f);
|
||||
EXPECT_LE(hover_pos.x, 4096.0f);
|
||||
EXPECT_LE(hover_pos.y, 4096.0f);
|
||||
};
|
||||
|
||||
test_hover_at_scale(0.25f);
|
||||
test_hover_at_scale(0.5f);
|
||||
test_hover_at_scale(1.0f);
|
||||
test_hover_at_scale(2.0f);
|
||||
test_hover_at_scale(4.0f);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Overworld Editor Integration Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(CanvasCoordinateSyncTest, OverworldMapHighlight_UsesHoverNotDrawn) {
|
||||
// CRITICAL REGRESSION TEST
|
||||
// This verifies the fix for overworld_editor.cc:1041
|
||||
// CheckForCurrentMap() must use hover_mouse_pos() not ImGui::GetIO().MousePos
|
||||
|
||||
// The pattern used in DrawOverworldEdits (line 664) for painting:
|
||||
auto drawn_pos = canvas_->drawn_tile_position();
|
||||
|
||||
// The pattern that SHOULD be used in CheckForCurrentMap (line 1041) for highlighting:
|
||||
auto hover_pos = canvas_->hover_mouse_pos();
|
||||
|
||||
// These are different methods for different purposes:
|
||||
// - drawn_tile_position(): Only updates during active painting (mouse drag)
|
||||
// - hover_mouse_pos(): Updates continuously during hover
|
||||
|
||||
// Verify both methods exist and return valid (or sentinel) values
|
||||
EXPECT_TRUE(drawn_pos.x >= 0.0f || drawn_pos.x == -1.0f);
|
||||
EXPECT_TRUE(hover_pos.x >= 0.0f || hover_pos.x == -1.0f);
|
||||
}
|
||||
|
||||
TEST_F(CanvasCoordinateSyncTest, OverworldMapIndex_From8x8Grid) {
|
||||
// Simulate the exact calculation from OverworldEditor::CheckForCurrentMap
|
||||
const int kOverworldMapSize = 512;
|
||||
|
||||
// Test all three worlds (Light, Dark, Special)
|
||||
struct TestCase {
|
||||
ImVec2 hover_pos;
|
||||
int current_world; // 0=Light, 1=Dark, 2=Special
|
||||
int expected_map_index;
|
||||
};
|
||||
|
||||
std::vector<TestCase> test_cases = {
|
||||
// Light World (0x00 - 0x3F)
|
||||
{ImVec2(0, 0), 0, 0}, // Map 0 (Light World)
|
||||
{ImVec2(512, 0), 0, 1}, // Map 1
|
||||
{ImVec2(1024, 512), 0, 10}, // Map 10 = 2 + 1*8
|
||||
|
||||
// Dark World (0x40 - 0x7F)
|
||||
{ImVec2(0, 0), 1, 0x40}, // Map 0x40 (Dark World)
|
||||
{ImVec2(512, 0), 1, 0x41}, // Map 0x41
|
||||
{ImVec2(1024, 512), 1, 0x4A}, // Map 0x4A = 0x40 + 10
|
||||
|
||||
// Special World (0x80+)
|
||||
{ImVec2(0, 0), 2, 0x80}, // Map 0x80 (Special World)
|
||||
{ImVec2(512, 512), 2, 0x89}, // Map 0x89 = 0x80 + 9
|
||||
};
|
||||
|
||||
for (const auto& tc : test_cases) {
|
||||
int map_x = tc.hover_pos.x / kOverworldMapSize;
|
||||
int map_y = tc.hover_pos.y / kOverworldMapSize;
|
||||
int hovered_map = map_x + map_y * 8;
|
||||
|
||||
if (tc.current_world == 1) {
|
||||
hovered_map += 0x40;
|
||||
} else if (tc.current_world == 2) {
|
||||
hovered_map += 0x80;
|
||||
}
|
||||
|
||||
EXPECT_EQ(hovered_map, tc.expected_map_index)
|
||||
<< "Failed for world " << tc.current_world
|
||||
<< " at position (" << tc.hover_pos.x << ", " << tc.hover_pos.y << ")";
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Boundary Condition Tests
|
||||
// ============================================================================
|
||||
|
||||
TEST_F(CanvasCoordinateSyncTest, MapBoundaries_512x512) {
|
||||
// Test coordinates exactly at map boundaries
|
||||
const int kOverworldMapSize = 512;
|
||||
|
||||
// Boundary coordinates (edges of maps)
|
||||
std::vector<ImVec2> boundary_positions = {
|
||||
ImVec2(511, 0), // Right edge of map 0
|
||||
ImVec2(512, 0), // Left edge of map 1
|
||||
ImVec2(0, 511), // Bottom edge of map 0
|
||||
ImVec2(0, 512), // Top edge of map 8
|
||||
ImVec2(511, 511), // Corner of map 0
|
||||
ImVec2(512, 512), // Corner of map 9
|
||||
};
|
||||
|
||||
for (const auto& pos : boundary_positions) {
|
||||
int map_x = pos.x / kOverworldMapSize;
|
||||
int map_y = pos.y / kOverworldMapSize;
|
||||
int map_index = map_x + map_y * 8;
|
||||
|
||||
// Verify map indices are within valid range
|
||||
EXPECT_GE(map_index, 0);
|
||||
EXPECT_LT(map_index, 64); // 8x8 grid = 64 maps
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(CanvasCoordinateSyncTest, MapBoundaries_1024x1024) {
|
||||
// Test large map boundaries (ZSCustomOverworld v3)
|
||||
const int kLargeMapSize = 1024;
|
||||
|
||||
std::vector<ImVec2> boundary_positions = {
|
||||
ImVec2(1023, 0), // Right edge of large map 0
|
||||
ImVec2(1024, 0), // Left edge of large map 1
|
||||
ImVec2(0, 1023), // Bottom edge of large map 0
|
||||
ImVec2(0, 1024), // Top edge of large map 4 (0,1 in 4x4 grid)
|
||||
};
|
||||
|
||||
for (const auto& pos : boundary_positions) {
|
||||
int map_x = pos.x / kLargeMapSize;
|
||||
int map_y = pos.y / kLargeMapSize;
|
||||
int map_index = map_x + map_y * 4; // 4x4 grid for large maps
|
||||
|
||||
// Verify map indices are within valid range for large maps
|
||||
EXPECT_GE(map_index, 0);
|
||||
EXPECT_LT(map_index, 16); // 4x4 grid = 16 large maps
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace yaze
|
||||
194
test/unit/gui/tile_selector_widget_test.cc
Normal file
194
test/unit/gui/tile_selector_widget_test.cc
Normal file
@@ -0,0 +1,194 @@
|
||||
#include "app/gui/widgets/tile_selector_widget.h"
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "app/gfx/core/bitmap.h"
|
||||
#include "app/gui/canvas/canvas.h"
|
||||
#include "testing.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
|
||||
using ::testing::Eq;
|
||||
using ::testing::NotNull;
|
||||
|
||||
class TileSelectorWidgetTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
// Create a test canvas
|
||||
canvas_ = std::make_unique<gui::Canvas>("TestCanvas", ImVec2(512, 512),
|
||||
gui::CanvasGridSize::k16x16);
|
||||
|
||||
// Create a test config
|
||||
config_.tile_size = 16;
|
||||
config_.display_scale = 2.0f;
|
||||
config_.tiles_per_row = 8;
|
||||
config_.total_tiles = 64; // 8x8 grid
|
||||
config_.draw_offset = {2.0f, 0.0f};
|
||||
config_.show_tile_ids = false;
|
||||
config_.highlight_color = {1.0f, 0.85f, 0.35f, 1.0f};
|
||||
}
|
||||
|
||||
std::unique_ptr<gui::Canvas> canvas_;
|
||||
gui::TileSelectorWidget::Config config_;
|
||||
};
|
||||
|
||||
// Test basic construction
|
||||
TEST_F(TileSelectorWidgetTest, Construction) {
|
||||
gui::TileSelectorWidget widget("test_widget");
|
||||
EXPECT_EQ(widget.GetSelectedTileID(), 0);
|
||||
}
|
||||
|
||||
// Test construction with config
|
||||
TEST_F(TileSelectorWidgetTest, ConstructionWithConfig) {
|
||||
gui::TileSelectorWidget widget("test_widget", config_);
|
||||
EXPECT_EQ(widget.GetSelectedTileID(), 0);
|
||||
}
|
||||
|
||||
// Test canvas attachment
|
||||
TEST_F(TileSelectorWidgetTest, AttachCanvas) {
|
||||
gui::TileSelectorWidget widget("test_widget");
|
||||
widget.AttachCanvas(canvas_.get());
|
||||
// No crash means success
|
||||
}
|
||||
|
||||
// Test tile count setting
|
||||
TEST_F(TileSelectorWidgetTest, SetTileCount) {
|
||||
gui::TileSelectorWidget widget("test_widget", config_);
|
||||
widget.SetTileCount(128);
|
||||
// Verify selection is clamped when tile count changes
|
||||
widget.SetSelectedTile(100);
|
||||
EXPECT_EQ(widget.GetSelectedTileID(), 100);
|
||||
|
||||
// Setting tile count lower should clamp selection
|
||||
widget.SetTileCount(50);
|
||||
EXPECT_EQ(widget.GetSelectedTileID(), 0); // Should reset to 0
|
||||
}
|
||||
|
||||
// Test selected tile setting
|
||||
TEST_F(TileSelectorWidgetTest, SetSelectedTile) {
|
||||
gui::TileSelectorWidget widget("test_widget", config_);
|
||||
widget.SetTileCount(64);
|
||||
|
||||
widget.SetSelectedTile(10);
|
||||
EXPECT_EQ(widget.GetSelectedTileID(), 10);
|
||||
|
||||
widget.SetSelectedTile(63);
|
||||
EXPECT_EQ(widget.GetSelectedTileID(), 63);
|
||||
|
||||
// Out of bounds should be ignored
|
||||
widget.SetSelectedTile(64);
|
||||
EXPECT_EQ(widget.GetSelectedTileID(), 63); // Should remain unchanged
|
||||
|
||||
widget.SetSelectedTile(-1);
|
||||
EXPECT_EQ(widget.GetSelectedTileID(), 63); // Should remain unchanged
|
||||
}
|
||||
|
||||
// Test tile origin calculation
|
||||
TEST_F(TileSelectorWidgetTest, TileOrigin) {
|
||||
gui::TileSelectorWidget widget("test_widget", config_);
|
||||
widget.SetTileCount(64);
|
||||
|
||||
// Test first tile (0,0)
|
||||
auto origin = widget.TileOrigin(0);
|
||||
EXPECT_FLOAT_EQ(origin.x, config_.draw_offset.x);
|
||||
EXPECT_FLOAT_EQ(origin.y, config_.draw_offset.y);
|
||||
|
||||
// Test tile at (1,0)
|
||||
origin = widget.TileOrigin(1);
|
||||
float expected_x = config_.draw_offset.x +
|
||||
(config_.tile_size * config_.display_scale);
|
||||
EXPECT_FLOAT_EQ(origin.x, expected_x);
|
||||
EXPECT_FLOAT_EQ(origin.y, config_.draw_offset.y);
|
||||
|
||||
// Test tile at (0,1) - first tile of second row
|
||||
origin = widget.TileOrigin(8);
|
||||
expected_x = config_.draw_offset.x;
|
||||
float expected_y = config_.draw_offset.y +
|
||||
(config_.tile_size * config_.display_scale);
|
||||
EXPECT_FLOAT_EQ(origin.x, expected_x);
|
||||
EXPECT_FLOAT_EQ(origin.y, expected_y);
|
||||
|
||||
// Test invalid tile ID
|
||||
origin = widget.TileOrigin(64);
|
||||
EXPECT_FLOAT_EQ(origin.x, -1.0f);
|
||||
EXPECT_FLOAT_EQ(origin.y, -1.0f);
|
||||
}
|
||||
|
||||
// Test render without atlas (should not crash)
|
||||
TEST_F(TileSelectorWidgetTest, RenderWithoutAtlas) {
|
||||
gui::TileSelectorWidget widget("test_widget", config_);
|
||||
widget.AttachCanvas(canvas_.get());
|
||||
|
||||
gfx::Bitmap atlas;
|
||||
auto result = widget.Render(atlas, false);
|
||||
|
||||
EXPECT_FALSE(result.tile_clicked);
|
||||
EXPECT_FALSE(result.tile_double_clicked);
|
||||
EXPECT_FALSE(result.selection_changed);
|
||||
EXPECT_EQ(result.selected_tile, -1);
|
||||
}
|
||||
|
||||
// Test programmatic selection for AI/automation
|
||||
TEST_F(TileSelectorWidgetTest, ProgrammaticSelection) {
|
||||
gui::TileSelectorWidget widget("test_widget", config_);
|
||||
widget.AttachCanvas(canvas_.get());
|
||||
widget.SetTileCount(64);
|
||||
|
||||
// Simulate AI/automation selecting tiles programmatically
|
||||
for (int i = 0; i < 64; ++i) {
|
||||
widget.SetSelectedTile(i);
|
||||
EXPECT_EQ(widget.GetSelectedTileID(), i);
|
||||
|
||||
auto origin = widget.TileOrigin(i);
|
||||
int expected_col = i % config_.tiles_per_row;
|
||||
int expected_row = i / config_.tiles_per_row;
|
||||
float expected_x = config_.draw_offset.x +
|
||||
expected_col * config_.tile_size * config_.display_scale;
|
||||
float expected_y = config_.draw_offset.y +
|
||||
expected_row * config_.tile_size * config_.display_scale;
|
||||
|
||||
EXPECT_FLOAT_EQ(origin.x, expected_x);
|
||||
EXPECT_FLOAT_EQ(origin.y, expected_y);
|
||||
}
|
||||
}
|
||||
|
||||
// Test scroll to tile
|
||||
TEST_F(TileSelectorWidgetTest, ScrollToTile) {
|
||||
gui::TileSelectorWidget widget("test_widget", config_);
|
||||
widget.AttachCanvas(canvas_.get());
|
||||
widget.SetTileCount(64);
|
||||
|
||||
// Scroll to various tiles (should not crash)
|
||||
widget.ScrollToTile(0);
|
||||
widget.ScrollToTile(10);
|
||||
widget.ScrollToTile(63);
|
||||
|
||||
// Invalid tile should not crash
|
||||
widget.ScrollToTile(-1);
|
||||
widget.ScrollToTile(64);
|
||||
}
|
||||
|
||||
// Test different configs
|
||||
TEST_F(TileSelectorWidgetTest, DifferentConfigs) {
|
||||
// Test with 16x16 grid
|
||||
gui::TileSelectorWidget::Config large_config;
|
||||
large_config.tile_size = 8;
|
||||
large_config.display_scale = 1.0f;
|
||||
large_config.tiles_per_row = 16;
|
||||
large_config.total_tiles = 256;
|
||||
large_config.draw_offset = {0.0f, 0.0f};
|
||||
|
||||
gui::TileSelectorWidget large_widget("large_widget", large_config);
|
||||
large_widget.SetTileCount(256);
|
||||
|
||||
for (int i = 0; i < 256; ++i) {
|
||||
large_widget.SetSelectedTile(i);
|
||||
EXPECT_EQ(large_widget.GetSelectedTileID(), i);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace yaze
|
||||
|
||||
Reference in New Issue
Block a user