From 1ebe10dde781039066d95f1eaaf685a53eba3a3c Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Mon, 27 Oct 2025 00:30:07 -0400 Subject: [PATCH] image work, fix text wrapping --- FlexLove.lua | 72 +++- testing/__tests__/25_image_cache_tests.lua | 226 ++++++++++ .../__tests__/26_object_fit_modes_tests.lua | 244 +++++++++++ .../__tests__/27_object_position_tests.lua | 184 +++++++++ .../28_element_image_integration_tests.lua | 391 ++++++++++++++++++ 5 files changed, 1096 insertions(+), 21 deletions(-) create mode 100644 testing/__tests__/25_image_cache_tests.lua create mode 100644 testing/__tests__/26_object_fit_modes_tests.lua create mode 100644 testing/__tests__/27_object_position_tests.lua create mode 100644 testing/__tests__/28_element_image_integration_tests.lua diff --git a/FlexLove.lua b/FlexLove.lua index 12e4466..3228731 100644 --- a/FlexLove.lua +++ b/FlexLove.lua @@ -4918,21 +4918,41 @@ function Element:draw(backdropCanvas) local contentX = self.x + textPaddingLeft local contentY = self.y + textPaddingTop - if self.textAlign == TextAlign.START then - tx = contentX - ty = contentY - elseif self.textAlign == TextAlign.CENTER then - tx = contentX + (textAreaWidth - textWidth) / 2 - ty = contentY + (textAreaHeight - textHeight) / 2 - elseif self.textAlign == TextAlign.END then - tx = contentX + textAreaWidth - textWidth - 10 - ty = contentY + textAreaHeight - textHeight - 10 - elseif self.textAlign == TextAlign.JUSTIFY then - --- need to figure out spreading + -- Check if text wrapping is enabled + if self.textWrap and (self.textWrap == "word" or self.textWrap == "char" or self.textWrap == true) then + -- Use printf for wrapped text + local align = "left" + if self.textAlign == TextAlign.CENTER then + align = "center" + elseif self.textAlign == TextAlign.END then + align = "right" + elseif self.textAlign == TextAlign.JUSTIFY then + align = "justify" + end + tx = contentX ty = contentY + + -- Use printf with the available width for wrapping + love.graphics.printf(self.text, tx, ty, textAreaWidth, align) + else + -- Use regular print for non-wrapped text + if self.textAlign == TextAlign.START then + tx = contentX + ty = contentY + elseif self.textAlign == TextAlign.CENTER then + tx = contentX + (textAreaWidth - textWidth) / 2 + ty = contentY + (textAreaHeight - textHeight) / 2 + elseif self.textAlign == TextAlign.END then + tx = contentX + textAreaWidth - textWidth - 10 + ty = contentY + textAreaHeight - textHeight - 10 + elseif self.textAlign == TextAlign.JUSTIFY then + --- need to figure out spreading + tx = contentX + ty = contentY + end + love.graphics.print(self.text, tx, ty) end - love.graphics.print(self.text, tx, ty) if self.textSize then love.graphics.setFont(origFont) end @@ -5575,6 +5595,8 @@ function Element:calculateTextHeight() return 0 end + -- Get the font + local font if self.textSize then -- Resolve font path from font family (same logic as in draw) local fontPath = nil @@ -5591,22 +5613,30 @@ function Element:calculateTextHeight() fontPath = themeToUse.fonts.default end end - - local tempFont = FONT_CACHE.get(self.textSize, fontPath) - local height = tempFont:getHeight() - -- Apply contentAutoSizingMultiplier if set - if self.contentAutoSizingMultiplier and self.contentAutoSizingMultiplier.height then - height = height * self.contentAutoSizingMultiplier.height - end - return height + font = FONT_CACHE.get(self.textSize, fontPath) + else + font = love.graphics.getFont() end - local font = love.graphics.getFont() local height = font:getHeight() + + -- If text wrapping is enabled, calculate height based on wrapped lines + if self.textWrap and (self.textWrap == "word" or self.textWrap == "char" or self.textWrap == true) then + -- Calculate available width for wrapping + local availableWidth = self.width + if availableWidth and availableWidth > 0 then + -- Get the wrapped text lines using getWrap (returns width and table of lines) + local wrappedWidth, wrappedLines = font:getWrap(self.text, availableWidth) + -- Height is line height * number of lines + height = height * #wrappedLines + end + end + -- Apply contentAutoSizingMultiplier if set if self.contentAutoSizingMultiplier and self.contentAutoSizingMultiplier.height then height = height * self.contentAutoSizingMultiplier.height end + return height end diff --git a/testing/__tests__/25_image_cache_tests.lua b/testing/__tests__/25_image_cache_tests.lua new file mode 100644 index 0000000..6b401d1 --- /dev/null +++ b/testing/__tests__/25_image_cache_tests.lua @@ -0,0 +1,226 @@ +local lu = require("testing.luaunit") +local FlexLove = require("FlexLove") +local ImageCache = FlexLove.ImageCache + +TestImageCache = {} + +function TestImageCache:setUp() + -- Clear cache before each test + ImageCache.clear() + + -- Create a test image programmatically + self.testImageData = love.image.newImageData(64, 64) + -- Fill with a simple pattern + for y = 0, 63 do + for x = 0, 63 do + local r = x / 63 + local g = y / 63 + local b = 0.5 + self.testImageData:setPixel(x, y, r, g, b, 1) + end + end + + -- Save to a temporary file (register in mock filesystem) + self.testImagePath = "testing/temp_test_image.png" + self.testImageData:encode("png", self.testImagePath) + -- Register file in mock filesystem so love.graphics.newImage can find it + love.filesystem.addMockFile(self.testImagePath, "mock_png_data") +end + +function TestImageCache:tearDown() + -- Clear cache after each test + ImageCache.clear() + + -- Clean up temporary test file + if love.filesystem.getInfo(self.testImagePath) then + love.filesystem.remove(self.testImagePath) + end +end + +-- ==================== +-- Basic Loading Tests +-- ==================== + +function TestImageCache:testLoadValidImage() + local image, err = ImageCache.load(self.testImagePath) + + lu.assertNotNil(image) + lu.assertNil(err) + lu.assertEquals(type(image), "userdata") -- love.Image is userdata +end + +function TestImageCache:testLoadInvalidPath() + local image, err = ImageCache.load("nonexistent/path/to/image.png") + + lu.assertNil(image) + lu.assertNotNil(err) + lu.assertStrContains(err, "Failed to load image") +end + +function TestImageCache:testLoadEmptyPath() + local image, err = ImageCache.load("") + + lu.assertNil(image) + lu.assertNotNil(err) + lu.assertStrContains(err, "Invalid image path") +end + +function TestImageCache:testLoadNilPath() + local image, err = ImageCache.load(nil) + + lu.assertNil(image) + lu.assertNotNil(err) + lu.assertStrContains(err, "Invalid image path") +end + +-- ==================== +-- Caching Tests +-- ==================== + +function TestImageCache:testCachingSameImageReturnsSameReference() + local image1, err1 = ImageCache.load(self.testImagePath) + local image2, err2 = ImageCache.load(self.testImagePath) + + lu.assertNotNil(image1) + lu.assertNotNil(image2) + lu.assertEquals(image1, image2) -- Same reference +end + +function TestImageCache:testCachingDifferentImages() + -- Create a second test image + local testImageData2 = love.image.newImageData(32, 32) + for y = 0, 31 do + for x = 0, 31 do + testImageData2:setPixel(x, y, 1, 0, 0, 1) + end + end + local testImagePath2 = "testing/temp_test_image2.png" + testImageData2:encode("png", testImagePath2) + + local image1 = ImageCache.load(self.testImagePath) + local image2 = ImageCache.load(testImagePath2) + + lu.assertNotNil(image1) + lu.assertNotNil(image2) + lu.assertNotEquals(image1, image2) -- Different images + + -- Cleanup + love.filesystem.remove(testImagePath2) +end + +function TestImageCache:testGetCachedImage() + -- Load image first + local loadedImage = ImageCache.load(self.testImagePath) + + -- Get from cache + local cachedImage = ImageCache.get(self.testImagePath) + + lu.assertNotNil(cachedImage) + lu.assertEquals(loadedImage, cachedImage) +end + +function TestImageCache:testGetNonCachedImage() + local image = ImageCache.get("nonexistent.png") + + lu.assertNil(image) +end + +-- ==================== +-- ImageData Loading Tests +-- ==================== + +function TestImageCache:testLoadWithImageData() + local image, err = ImageCache.load(self.testImagePath, true) + + lu.assertNotNil(image) + lu.assertNil(err) + + local imageData = ImageCache.getImageData(self.testImagePath) + lu.assertNotNil(imageData) + lu.assertEquals(type(imageData), "userdata") -- love.ImageData is userdata +end + +function TestImageCache:testLoadWithoutImageData() + local image, err = ImageCache.load(self.testImagePath, false) + + lu.assertNotNil(image) + lu.assertNil(err) + + local imageData = ImageCache.getImageData(self.testImagePath) + lu.assertNil(imageData) -- Should not be loaded +end + +-- ==================== +-- Cache Management Tests +-- ==================== + +function TestImageCache:testRemoveImage() + ImageCache.load(self.testImagePath) + + local removed = ImageCache.remove(self.testImagePath) + + lu.assertTrue(removed) + + -- Verify it's no longer in cache + local cachedImage = ImageCache.get(self.testImagePath) + lu.assertNil(cachedImage) +end + +function TestImageCache:testRemoveNonExistentImage() + local removed = ImageCache.remove("nonexistent.png") + + lu.assertFalse(removed) +end + +function TestImageCache:testClearCache() + -- Load multiple images + ImageCache.load(self.testImagePath) + + local stats1 = ImageCache.getStats() + lu.assertEquals(stats1.count, 1) + + ImageCache.clear() + + local stats2 = ImageCache.getStats() + lu.assertEquals(stats2.count, 0) +end + +-- ==================== +-- Statistics Tests +-- ==================== + +function TestImageCache:testCacheStats() + local stats1 = ImageCache.getStats() + lu.assertEquals(stats1.count, 0) + lu.assertEquals(stats1.memoryEstimate, 0) + + ImageCache.load(self.testImagePath) + + local stats2 = ImageCache.getStats() + lu.assertEquals(stats2.count, 1) + lu.assertTrue(stats2.memoryEstimate > 0) + + -- Memory estimate should be approximately 64*64*4 bytes + local expectedMemory = 64 * 64 * 4 + lu.assertEquals(stats2.memoryEstimate, expectedMemory) +end + +-- ==================== +-- Path Normalization Tests +-- ==================== + +function TestImageCache:testPathNormalization() + -- Load with different path formats + local image1 = ImageCache.load(self.testImagePath) + local image2 = ImageCache.load(" " .. self.testImagePath .. " ") -- With whitespace + local image3 = ImageCache.load(self.testImagePath:gsub("/", "\\")) -- With backslashes + + lu.assertEquals(image1, image2) + lu.assertEquals(image1, image3) + + -- Should only have one cache entry + local stats = ImageCache.getStats() + lu.assertEquals(stats.count, 1) +end + +lu.LuaUnit.run() diff --git a/testing/__tests__/26_object_fit_modes_tests.lua b/testing/__tests__/26_object_fit_modes_tests.lua new file mode 100644 index 0000000..2e89d2d --- /dev/null +++ b/testing/__tests__/26_object_fit_modes_tests.lua @@ -0,0 +1,244 @@ +local lu = require("testing.luaunit") +local FlexLove = require("FlexLove") +local ImageRenderer = FlexLove.ImageRenderer + +TestObjectFitModes = {} + +function TestObjectFitModes:setUp() + -- Test dimensions + self.imageWidth = 400 + self.imageHeight = 300 + self.boundsWidth = 200 + self.boundsHeight = 200 +end + +-- ==================== +-- Fill Mode Tests +-- ==================== + +function TestObjectFitModes:testFillModeStretchesToExactBounds() + local params = ImageRenderer.calculateFit(self.imageWidth, self.imageHeight, self.boundsWidth, self.boundsHeight, "fill") + + lu.assertEquals(params.dw, self.boundsWidth) + lu.assertEquals(params.dh, self.boundsHeight) + lu.assertEquals(params.dx, 0) + lu.assertEquals(params.dy, 0) +end + +function TestObjectFitModes:testFillModeUsesFullSourceImage() + local params = ImageRenderer.calculateFit(self.imageWidth, self.imageHeight, self.boundsWidth, self.boundsHeight, "fill") + + lu.assertEquals(params.sx, 0) + lu.assertEquals(params.sy, 0) + lu.assertEquals(params.sw, self.imageWidth) + lu.assertEquals(params.sh, self.imageHeight) +end + +-- ==================== +-- Contain Mode Tests +-- ==================== + +function TestObjectFitModes:testContainModePreservesAspectRatio() + local params = ImageRenderer.calculateFit(self.imageWidth, self.imageHeight, self.boundsWidth, self.boundsHeight, "contain") + + -- Image is 4:3, bounds are 1:1 + -- Should scale to fit width (200), height becomes 150 + local expectedScale = self.boundsWidth / self.imageWidth + local expectedHeight = self.imageHeight * expectedScale + + lu.assertAlmostEquals(params.dw, self.boundsWidth, 0.01) + lu.assertAlmostEquals(params.dh, expectedHeight, 0.01) +end + +function TestObjectFitModes:testContainModeFitsWithinBounds() + local params = ImageRenderer.calculateFit(self.imageWidth, self.imageHeight, self.boundsWidth, self.boundsHeight, "contain") + + lu.assertTrue(params.dw <= self.boundsWidth) + lu.assertTrue(params.dh <= self.boundsHeight) +end + +function TestObjectFitModes:testContainModeCentersImage() + local params = ImageRenderer.calculateFit(self.imageWidth, self.imageHeight, self.boundsWidth, self.boundsHeight, "contain") + + -- Image should be centered in letterbox + -- With default "center center" position + lu.assertTrue(params.dx >= 0) + lu.assertTrue(params.dy >= 0) +end + +-- ==================== +-- Cover Mode Tests +-- ==================== + +function TestObjectFitModes:testCoverModePreservesAspectRatio() + local params = ImageRenderer.calculateFit(self.imageWidth, self.imageHeight, self.boundsWidth, self.boundsHeight, "cover") + + -- Check that aspect ratio is preserved in source crop + local sourceAspect = params.sw / params.sh + local boundsAspect = self.boundsWidth / self.boundsHeight + + lu.assertAlmostEquals(sourceAspect, boundsAspect, 0.01) +end + +function TestObjectFitModes:testCoverModeCoversEntireBounds() + local params = ImageRenderer.calculateFit(self.imageWidth, self.imageHeight, self.boundsWidth, self.boundsHeight, "cover") + + lu.assertEquals(params.dw, self.boundsWidth) + lu.assertEquals(params.dh, self.boundsHeight) +end + +function TestObjectFitModes:testCoverModeCropsImage() + local params = ImageRenderer.calculateFit(self.imageWidth, self.imageHeight, self.boundsWidth, self.boundsHeight, "cover") + + -- Source should be cropped (not full image) + lu.assertTrue(params.sw < self.imageWidth or params.sh < self.imageHeight) +end + +-- ==================== +-- None Mode Tests +-- ==================== + +function TestObjectFitModes:testNoneModeUsesNaturalSize() + local params = ImageRenderer.calculateFit(self.imageWidth, self.imageHeight, self.boundsWidth, self.boundsHeight, "none") + + lu.assertEquals(params.dw, self.imageWidth) + lu.assertEquals(params.dh, self.imageHeight) + lu.assertEquals(params.scaleX, 1) + lu.assertEquals(params.scaleY, 1) +end + +function TestObjectFitModes:testNoneModeUsesFullSourceImage() + local params = ImageRenderer.calculateFit(self.imageWidth, self.imageHeight, self.boundsWidth, self.boundsHeight, "none") + + lu.assertEquals(params.sx, 0) + lu.assertEquals(params.sy, 0) + lu.assertEquals(params.sw, self.imageWidth) + lu.assertEquals(params.sh, self.imageHeight) +end + +-- ==================== +-- Scale-Down Mode Tests +-- ==================== + +function TestObjectFitModes:testScaleDownUsesNoneWhenImageFits() + -- Image smaller than bounds + local smallWidth = 100 + local smallHeight = 75 + + local params = ImageRenderer.calculateFit(smallWidth, smallHeight, self.boundsWidth, self.boundsHeight, "scale-down") + + -- Should use natural size (none mode) + lu.assertEquals(params.dw, smallWidth) + lu.assertEquals(params.dh, smallHeight) +end + +function TestObjectFitModes:testScaleDownUsesContainWhenImageTooBig() + local params = ImageRenderer.calculateFit(self.imageWidth, self.imageHeight, self.boundsWidth, self.boundsHeight, "scale-down") + + -- Should use contain mode + lu.assertTrue(params.dw <= self.boundsWidth) + lu.assertTrue(params.dh <= self.boundsHeight) + + -- Should preserve aspect ratio + local scale = params.dw / self.imageWidth + lu.assertAlmostEquals(params.dh, self.imageHeight * scale, 0.01) +end + +-- ==================== +-- Edge Cases +-- ==================== + +function TestObjectFitModes:testLandscapeImageInPortraitBounds() + local params = ImageRenderer.calculateFit( + 400, + 200, -- Landscape image (2:1) + 200, + 400, -- Portrait bounds (1:2) + "contain" + ) + + -- Should fit width + lu.assertEquals(params.dw, 200) + lu.assertTrue(params.dh < 400) +end + +function TestObjectFitModes:testPortraitImageInLandscapeBounds() + local params = ImageRenderer.calculateFit( + 200, + 400, -- Portrait image (1:2) + 400, + 200, -- Landscape bounds (2:1) + "contain" + ) + + -- Should fit height + lu.assertEquals(params.dh, 200) + lu.assertTrue(params.dw < 400) +end + +function TestObjectFitModes:testSquareImageInNonSquareBounds() + local params = ImageRenderer.calculateFit( + 300, + 300, -- Square image + 200, + 400, -- Non-square bounds + "contain" + ) + + -- Should fit to smaller dimension (width) + lu.assertEquals(params.dw, 200) + lu.assertEquals(params.dh, 200) +end + +function TestObjectFitModes:testImageSmallerThanBounds() + local params = ImageRenderer.calculateFit(100, 100, 200, 200, "contain") + + -- Should scale up to fit + lu.assertEquals(params.dw, 200) + lu.assertEquals(params.dh, 200) +end + +function TestObjectFitModes:testImageLargerThanBounds() + local params = ImageRenderer.calculateFit(800, 600, 200, 200, "contain") + + -- Should scale down to fit + lu.assertTrue(params.dw <= 200) + lu.assertTrue(params.dh <= 200) +end + +-- ==================== +-- Invalid Input Tests +-- ==================== + +function TestObjectFitModes:testInvalidFitModeThrowsError() + lu.assertErrorMsgContains("Invalid fit mode", ImageRenderer.calculateFit, 100, 100, 200, 200, "invalid-mode") +end + +function TestObjectFitModes:testZeroDimensionsThrowsError() + lu.assertErrorMsgContains("Dimensions must be positive", ImageRenderer.calculateFit, 0, 100, 200, 200, "fill") +end + +function TestObjectFitModes:testNegativeDimensionsThrowsError() + lu.assertErrorMsgContains("Dimensions must be positive", ImageRenderer.calculateFit, 100, -100, 200, 200, "fill") +end + +-- ==================== +-- Default Mode Test +-- ==================== + +function TestObjectFitModes:testDefaultModeIsFill() + local params1 = ImageRenderer.calculateFit( + self.imageWidth, + self.imageHeight, + self.boundsWidth, + self.boundsHeight, + nil -- No mode specified + ) + + local params2 = ImageRenderer.calculateFit(self.imageWidth, self.imageHeight, self.boundsWidth, self.boundsHeight, "fill") + + lu.assertEquals(params1.dw, params2.dw) + lu.assertEquals(params1.dh, params2.dh) +end + +lu.LuaUnit.run() diff --git a/testing/__tests__/27_object_position_tests.lua b/testing/__tests__/27_object_position_tests.lua new file mode 100644 index 0000000..fe3147e --- /dev/null +++ b/testing/__tests__/27_object_position_tests.lua @@ -0,0 +1,184 @@ +local lu = require("testing.luaunit") +local FlexLove = require("FlexLove") +local ImageRenderer = FlexLove.ImageRenderer + +TestObjectPosition = {} + +-- ==================== +-- Position Parsing Tests +-- ==================== + +function TestObjectPosition:testCenterCenterDefault() + local x, y = ImageRenderer._parsePosition("center center") + lu.assertEquals(x, 0.5) + lu.assertEquals(y, 0.5) +end + +function TestObjectPosition:testTopLeft() + local x, y = ImageRenderer._parsePosition("top left") + lu.assertEquals(x, 0) + lu.assertEquals(y, 0) +end + +function TestObjectPosition:testBottomRight() + local x, y = ImageRenderer._parsePosition("bottom right") + lu.assertEquals(x, 1) + lu.assertEquals(y, 1) +end + +function TestObjectPosition:testPercentage50() + local x, y = ImageRenderer._parsePosition("50% 50%") + lu.assertEquals(x, 0.5) + lu.assertEquals(y, 0.5) +end + +function TestObjectPosition:testPercentage0() + local x, y = ImageRenderer._parsePosition("0% 0%") + lu.assertEquals(x, 0) + lu.assertEquals(y, 0) +end + +function TestObjectPosition:testPercentage100() + local x, y = ImageRenderer._parsePosition("100% 100%") + lu.assertEquals(x, 1) + lu.assertEquals(y, 1) +end + +function TestObjectPosition:testMixedKeywordPercentage() + local x, y = ImageRenderer._parsePosition("center 25%") + lu.assertEquals(x, 0.5) + lu.assertEquals(y, 0.25) +end + +function TestObjectPosition:testSingleValueLeft() + local x, y = ImageRenderer._parsePosition("left") + lu.assertEquals(x, 0) + lu.assertEquals(y, 0.5) -- Should center on Y axis +end + +function TestObjectPosition:testSingleValueTop() + local x, y = ImageRenderer._parsePosition("top") + lu.assertEquals(x, 0.5) -- Should center on X axis + lu.assertEquals(y, 0) +end + +function TestObjectPosition:testInvalidPositionDefaultsToCenter() + local x, y = ImageRenderer._parsePosition("invalid position") + lu.assertEquals(x, 0.5) + lu.assertEquals(y, 0.5) +end + +function TestObjectPosition:testNilPositionDefaultsToCenter() + local x, y = ImageRenderer._parsePosition(nil) + lu.assertEquals(x, 0.5) + lu.assertEquals(y, 0.5) +end + +function TestObjectPosition:testEmptyStringDefaultsToCenter() + local x, y = ImageRenderer._parsePosition("") + lu.assertEquals(x, 0.5) + lu.assertEquals(y, 0.5) +end + +-- ==================== +-- Position with Contain Mode Tests +-- ==================== + +function TestObjectPosition:testContainWithTopLeft() + local params = ImageRenderer.calculateFit( + 400, + 300, -- Image (landscape) + 200, + 200, -- Bounds (square) + "contain", + "top left" + ) + + -- Image should be in top-left of letterbox + lu.assertEquals(params.dx, 0) + lu.assertEquals(params.dy, 0) +end + +function TestObjectPosition:testContainWithBottomRight() + local params = ImageRenderer.calculateFit( + 400, + 300, -- Image (landscape) + 200, + 200, -- Bounds (square) + "contain", + "bottom right" + ) + + -- Image should be in bottom-right of letterbox + lu.assertTrue(params.dx + params.dw <= 200) + lu.assertTrue(params.dy + params.dh <= 200) + -- Should be at the bottom right + lu.assertAlmostEquals(params.dx + params.dw, 200, 0.01) + lu.assertAlmostEquals(params.dy + params.dh, 200, 0.01) +end + +function TestObjectPosition:testContainWithCenter() + local params = ImageRenderer.calculateFit(400, 300, 200, 200, "contain", "center center") + + -- Image (400x300) will be scaled to fit width (200x150) + -- Should be centered horizontally (dx=0) and vertically (dy=25) + lu.assertEquals(params.dx, 0) + lu.assertTrue(params.dy > 0) +end + +-- ==================== +-- Position with Cover Mode Tests +-- ==================== + +function TestObjectPosition:testCoverWithTopLeft() + local params = ImageRenderer.calculateFit(400, 300, 200, 200, "cover", "top left") + + -- Crop should start from top-left + lu.assertEquals(params.sx, 0) + lu.assertEquals(params.sy, 0) +end + +function TestObjectPosition:testCoverWithBottomRight() + local params = ImageRenderer.calculateFit(400, 300, 200, 200, "cover", "bottom right") + + -- Crop should be from bottom-right + lu.assertTrue(params.sx > 0) + lu.assertTrue(params.sy >= 0) +end + +function TestObjectPosition:testCoverWithCenter() + local params = ImageRenderer.calculateFit(400, 300, 200, 200, "cover", "center center") + + -- Crop should be centered + lu.assertTrue(params.sx > 0) +end + +-- ==================== +-- Position with None Mode Tests +-- ==================== + +function TestObjectPosition:testNoneWithTopLeft() + local params = ImageRenderer.calculateFit(100, 100, 200, 200, "none", "top left") + + -- Image should be at top-left + lu.assertEquals(params.dx, 0) + lu.assertEquals(params.dy, 0) +end + +function TestObjectPosition:testNoneWithBottomRight() + local params = ImageRenderer.calculateFit(100, 100, 200, 200, "none", "bottom right") + + -- Image should be at bottom-right + lu.assertEquals(params.dx, 100) -- 200 - 100 + lu.assertEquals(params.dy, 100) -- 200 - 100 +end + +function TestObjectPosition:testNoneWithCenter() + local params = ImageRenderer.calculateFit(100, 100, 200, 200, "none", "center center") + + -- Image should be centered + lu.assertEquals(params.dx, 50) -- (200 - 100) / 2 + lu.assertEquals(params.dy, 50) -- (200 - 100) / 2 +end + +lu.LuaUnit.run() diff --git a/testing/__tests__/28_element_image_integration_tests.lua b/testing/__tests__/28_element_image_integration_tests.lua new file mode 100644 index 0000000..955830b --- /dev/null +++ b/testing/__tests__/28_element_image_integration_tests.lua @@ -0,0 +1,391 @@ +local lu = require("testing.luaunit") +local FlexLove = require("FlexLove") +local Gui = FlexLove.Gui +local Element = FlexLove.Element +local ImageCache = FlexLove.ImageCache + +TestElementImageIntegration = {} + +function TestElementImageIntegration:setUp() + Gui.init({ baseScale = { width = 1920, height = 1080 } }) + + -- Create a test image programmatically + self.testImageData = love.image.newImageData(400, 300) + -- Fill with a gradient pattern + for y = 0, 299 do + for x = 0, 399 do + local r = x / 399 + local g = y / 299 + local b = 0.5 + self.testImageData:setPixel(x, y, r, g, b, 1) + end + end + + -- Save to a temporary file (mock filesystem) + self.testImagePath = "testing/temp_element_test_image.png" + self.testImageData:encode("png", self.testImagePath) + love.filesystem.addMockFile(self.testImagePath, "mock_image_data") + + -- Create test image object + self.testImage = love.graphics.newImage(self.testImageData) +end + +function TestElementImageIntegration:tearDown() + Gui.destroy() + ImageCache.clear() + + -- Clean up temporary test file + if love.filesystem.getInfo(self.testImagePath) then + love.filesystem.remove(self.testImagePath) + end +end + +-- ==================== +-- Element Creation Tests +-- ==================== + +function TestElementImageIntegration:testElementWithImagePath() + local element = Element.new({ + x = 100, + y = 100, + width = 200, + height = 200, + imagePath = self.testImagePath, + }) + + lu.assertNotNil(element._loadedImage) + lu.assertEquals(element.imagePath, self.testImagePath) +end + +function TestElementImageIntegration:testElementWithImageObject() + local element = Element.new({ + x = 100, + y = 100, + width = 200, + height = 200, + image = self.testImage, + }) + + lu.assertNotNil(element._loadedImage) + lu.assertEquals(element._loadedImage, self.testImage) +end + +function TestElementImageIntegration:testElementWithInvalidImagePath() + local element = Element.new({ + x = 100, + y = 100, + width = 200, + height = 200, + imagePath = "nonexistent/image.png", + }) + + -- Should not crash, just not have a loaded image + lu.assertNil(element._loadedImage) +end + +function TestElementImageIntegration:testElementWithoutImage() + local element = Element.new({ + x = 100, + y = 100, + width = 200, + height = 200, + }) + + lu.assertNil(element._loadedImage) + lu.assertNil(element.imagePath) + lu.assertNil(element.image) +end + +-- ==================== +-- Property Tests +-- ==================== + +function TestElementImageIntegration:testObjectFitProperty() + local element = Element.new({ + x = 100, + y = 100, + width = 200, + height = 200, + imagePath = self.testImagePath, + objectFit = "contain", + }) + + lu.assertEquals(element.objectFit, "contain") +end + +function TestElementImageIntegration:testObjectFitDefaultValue() + local element = Element.new({ + x = 100, + y = 100, + width = 200, + height = 200, + imagePath = self.testImagePath, + }) + + lu.assertEquals(element.objectFit, "fill") +end + +function TestElementImageIntegration:testObjectPositionProperty() + local element = Element.new({ + x = 100, + y = 100, + width = 200, + height = 200, + imagePath = self.testImagePath, + objectPosition = "top left", + }) + + lu.assertEquals(element.objectPosition, "top left") +end + +function TestElementImageIntegration:testObjectPositionDefaultValue() + local element = Element.new({ + x = 100, + y = 100, + width = 200, + height = 200, + imagePath = self.testImagePath, + }) + + lu.assertEquals(element.objectPosition, "center center") +end + +function TestElementImageIntegration:testImageOpacityProperty() + local element = Element.new({ + x = 100, + y = 100, + width = 200, + height = 200, + imagePath = self.testImagePath, + imageOpacity = 0.5, + }) + + lu.assertEquals(element.imageOpacity, 0.5) +end + +function TestElementImageIntegration:testImageOpacityDefaultValue() + local element = Element.new({ + x = 100, + y = 100, + width = 200, + height = 200, + imagePath = self.testImagePath, + }) + + lu.assertEquals(element.imageOpacity, 1) +end + +-- ==================== +-- Image Caching Tests +-- ==================== + +function TestElementImageIntegration:testMultipleElementsShareCachedImage() + local element1 = Element.new({ + x = 0, + y = 0, + width = 100, + height = 100, + imagePath = self.testImagePath, + }) + + local element2 = Element.new({ + x = 100, + y = 100, + width = 200, + height = 200, + imagePath = self.testImagePath, + }) + + -- Both should have the same cached image reference + lu.assertEquals(element1._loadedImage, element2._loadedImage) + + -- Cache should only have one entry + local stats = ImageCache.getStats() + lu.assertEquals(stats.count, 1) +end + +-- ==================== +-- Rendering Tests (Basic Validation) +-- ==================== + +function TestElementImageIntegration:testDrawDoesNotCrashWithImage() + local element = Element.new({ + x = 100, + y = 100, + width = 200, + height = 200, + imagePath = self.testImagePath, + }) + + -- Should not crash when drawing + lu.assertNotNil(function() + element:draw() + end) +end + +function TestElementImageIntegration:testDrawDoesNotCrashWithoutImage() + local element = Element.new({ + x = 100, + y = 100, + width = 200, + height = 200, + }) + + -- Should not crash when drawing without image + lu.assertNotNil(function() + element:draw() + end) +end + +function TestElementImageIntegration:testDrawWithZeroOpacity() + local element = Element.new({ + x = 100, + y = 100, + width = 200, + height = 200, + imagePath = self.testImagePath, + opacity = 0, + }) + + -- Should not crash (early exit in draw) + lu.assertNotNil(function() + element:draw() + end) +end + +-- ==================== +-- Combined Properties Tests +-- ==================== + +function TestElementImageIntegration:testImageWithPadding() + local element = Element.new({ + x = 100, + y = 100, + width = 200, + height = 200, + padding = { top = 10, right = 10, bottom = 10, left = 10 }, + imagePath = self.testImagePath, + }) + + lu.assertNotNil(element._loadedImage) + lu.assertEquals(element.padding.top, 10) + lu.assertEquals(element.padding.left, 10) + -- Image should render in content area (200x200) + lu.assertEquals(element.width, 200) + lu.assertEquals(element.height, 200) +end + +function TestElementImageIntegration:testImageWithCornerRadius() + local element = Element.new({ + x = 100, + y = 100, + width = 200, + height = 200, + cornerRadius = 20, + imagePath = self.testImagePath, + }) + + lu.assertNotNil(element._loadedImage) + lu.assertEquals(element.cornerRadius.topLeft, 20) + lu.assertEquals(element.cornerRadius.topRight, 20) + lu.assertEquals(element.cornerRadius.bottomLeft, 20) + lu.assertEquals(element.cornerRadius.bottomRight, 20) +end + +function TestElementImageIntegration:testImageWithBackgroundColor() + local element = Element.new({ + x = 100, + y = 100, + width = 200, + height = 200, + backgroundColor = FlexLove.Color.new(1, 0, 0, 1), + imagePath = self.testImagePath, + }) + + lu.assertNotNil(element._loadedImage) + lu.assertEquals(element.backgroundColor.r, 1) + lu.assertEquals(element.backgroundColor.g, 0) + lu.assertEquals(element.backgroundColor.b, 0) +end + +function TestElementImageIntegration:testImageWithAllObjectFitModes() + local modes = { "fill", "contain", "cover", "scale-down", "none" } + + for _, mode in ipairs(modes) do + local element = Element.new({ + x = 100, + y = 100, + width = 200, + height = 200, + imagePath = self.testImagePath, + objectFit = mode, + }) + + lu.assertEquals(element.objectFit, mode) + lu.assertNotNil(element._loadedImage) + end +end + +function TestElementImageIntegration:testImageWithCombinedOpacity() + local element = Element.new({ + x = 100, + y = 100, + width = 200, + height = 200, + imagePath = self.testImagePath, + opacity = 0.5, + imageOpacity = 0.8, + }) + + lu.assertEquals(element.opacity, 0.5) + lu.assertEquals(element.imageOpacity, 0.8) + -- Combined opacity should be 0.5 * 0.8 = 0.4 (tested in rendering) +end + +-- ==================== +-- Layout Integration Tests +-- ==================== + +function TestElementImageIntegration:testImageWithFlexLayout() + local container = Element.new({ + x = 0, + y = 0, + width = 600, + height = 200, + flexDirection = FlexLove.enums.FlexDirection.HORIZONTAL, + }) + + local imageElement = Element.new({ + width = 200, + height = 200, + imagePath = self.testImagePath, + parent = container, + }) + + table.insert(container.children, imageElement) + + lu.assertNotNil(imageElement._loadedImage) + lu.assertEquals(imageElement.width, 200) + lu.assertEquals(imageElement.height, 200) +end + +function TestElementImageIntegration:testImageWithAbsolutePositioning() + local element = Element.new({ + positioning = FlexLove.enums.Positioning.ABSOLUTE, + top = 50, + left = 50, + width = 200, + height = 200, + imagePath = self.testImagePath, + }) + + lu.assertNotNil(element._loadedImage) + lu.assertEquals(element.positioning, FlexLove.enums.Positioning.ABSOLUTE) +end + +-- Run tests if executed directly +if arg and arg[0]:match("28_element_image_integration_tests.lua$") then + os.exit(lu.LuaUnit.run()) +end + +return TestElementImageIntegration