// // HTMLImageEmbedderTests.swift // readeckTests // // Created by Ilyas Hallak on 30.11.25. // import Testing import Foundation import Kingfisher #if os(iOS) import UIKit #elseif os(macOS) import AppKit #endif @testable import readeck @Suite("HTMLImageEmbedder Tests") struct HTMLImageEmbedderTests { // MARK: - Test Data private let htmlWithImages = """ Image 1 """ private let htmlWithoutImages = """

Just text, no images here.

""" private let htmlWithDataURI = """ """ // MARK: - Helper Methods /// Creates a test image and caches it in Kingfisher for testing private func cacheTestImage(url: URL) async { // Create a simple 1x1 pixel red image for testing #if os(iOS) let size = CGSize(width: 1, height: 1) UIGraphicsBeginImageContextWithOptions(size, false, 1.0) UIColor.red.setFill() UIRectFill(CGRect(origin: .zero, size: size)) let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() #elseif os(macOS) let size = NSSize(width: 1, height: 1) let image = NSImage(size: size) image.lockFocus() NSColor.red.setFill() NSBezierPath.fill(NSRect(origin: .zero, size: size)) image.unlockFocus() #endif if let image = image { // Store both in memory and on disk for testing let options = KingfisherParsedOptionsInfo([ .cacheOriginalImage, .diskCacheExpiration(.never) ]) try? await ImageCache.default.store(image, forKey: url.cacheKey, options: options) // Small delay to ensure cache write completes try? await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds } } /// Clears all cached images after tests private func clearTestCache() async { // Clear both memory and disk cache await ImageCache.default.clearMemoryCache() await ImageCache.default.clearDiskCache() // Small delay to ensure cache clear completes try? await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds } // MARK: - Basic Functionality Tests @Test("Embed Base64 images converts URLs to data URIs") func testEmbedBase64ImagesConvertsURLs() async { // Clear cache first to ensure clean state await clearTestCache() let embedder = HTMLImageEmbedder() // Cache test images first let url1 = URL(string: "https://example.com/image1.jpg")! let url2 = URL(string: "https://example.com/image2.png")! await cacheTestImage(url: url1) await cacheTestImage(url: url2) let result = await embedder.embedBase64Images(in: htmlWithImages) // Verify images were embedded as Base64 #expect(result.contains("data:image/jpeg;base64,")) #expect(!result.contains("https://example.com/image1.jpg")) #expect(!result.contains("https://example.com/image2.png")) await clearTestCache() } @Test("Embed Base64 images skips images not in cache") func testEmbedBase64ImagesSkipsUncachedImages() async { // Clear cache first to ensure clean state await clearTestCache() let embedder = HTMLImageEmbedder() // Don't cache any images - all should be skipped let result = await embedder.embedBase64Images(in: htmlWithImages) // Original URLs should remain unchanged #expect(result.contains("https://example.com/image1.jpg")) #expect(result.contains("https://example.com/image2.png")) #expect(!result.contains("data:image/jpeg;base64,")) } @Test("Embed Base64 images increases HTML size") func testEmbedBase64ImagesIncreasesHTMLSize() async { // Clear cache first to ensure clean state await clearTestCache() let embedder = HTMLImageEmbedder() // Cache one test image let url1 = URL(string: "https://example.com/image1.jpg")! await cacheTestImage(url: url1) let originalSize = htmlWithImages.utf8.count let result = await embedder.embedBase64Images(in: htmlWithImages) let newSize = result.utf8.count // Base64 encoded images should make HTML larger #expect(newSize > originalSize) await clearTestCache() } @Test("Embed Base64 images uses JPEG format with quality 0.85") func testEmbedBase64ImagesUsesJPEGFormat() async { // Clear cache first to ensure clean state await clearTestCache() let embedder = HTMLImageEmbedder() let url = URL(string: "https://example.com/image1.jpg")! await cacheTestImage(url: url) let result = await embedder.embedBase64Images(in: htmlWithImages) // Verify data URI uses JPEG format #expect(result.contains("data:image/jpeg;base64,")) await clearTestCache() } // MARK: - Edge Case Tests @Test("Embed Base64 images handles empty HTML") func testEmbedBase64ImagesHandlesEmptyHTML() async { let embedder = HTMLImageEmbedder() let emptyHTML = "" let result = await embedder.embedBase64Images(in: emptyHTML) #expect(result.isEmpty) #expect(result == emptyHTML) } @Test("Embed Base64 images handles HTML without images") func testEmbedBase64ImagesHandlesHTMLWithoutImages() async { let embedder = HTMLImageEmbedder() let result = await embedder.embedBase64Images(in: htmlWithoutImages) // Should return unchanged HTML #expect(result == htmlWithoutImages) #expect(!result.contains("data:image")) } @Test("Embed Base64 images skips already embedded data URIs") func testEmbedBase64ImagesSkipsDataURIs() async { // Clear cache first to ensure clean state await clearTestCache() let embedder = HTMLImageEmbedder() // Cache the non-data URI image let url = URL(string: "https://example.com/new-image.jpg")! await cacheTestImage(url: url) let result = await embedder.embedBase64Images(in: htmlWithDataURI) // Original data URI should remain #expect(result.contains("data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2w==")) // New image should be embedded #expect(!result.contains("https://example.com/new-image.jpg")) await clearTestCache() } @Test("Embed Base64 images processes multiple images correctly") func testEmbedBase64ImagesProcessesMultipleImages() async { // Clear cache first to ensure clean state await clearTestCache() let embedder = HTMLImageEmbedder() let htmlMultiple = """ """ // Cache all three images for i in 1...3 { let url = URL(string: "https://example.com/img\(i).jpg")! await cacheTestImage(url: url) } let result = await embedder.embedBase64Images(in: htmlMultiple) // All three should be embedded let dataURICount = result.components(separatedBy: "data:image/jpeg;base64,").count - 1 #expect(dataURICount == 3) // None of the original URLs should remain #expect(!result.contains("https://example.com/img1.jpg")) #expect(!result.contains("https://example.com/img2.jpg")) #expect(!result.contains("https://example.com/img3.jpg")) await clearTestCache() } // MARK: - Statistics & Logging Tests @Test("Embed Base64 images tracks success and failure counts") func testEmbedBase64ImagesTracksStatistics() async { // Clear cache first to ensure clean state await clearTestCache() let embedder = HTMLImageEmbedder() let htmlMixed = """ """ // Cache only the first image let cachedURL = URL(string: "https://cached.com/image.jpg")! await cacheTestImage(url: cachedURL) let result = await embedder.embedBase64Images(in: htmlMixed) // First image should be embedded #expect(result.contains("data:image/jpeg;base64,")) #expect(!result.contains("https://cached.com/image.jpg")) // Second image should remain as URL #expect(result.contains("https://not-cached.com/image.jpg")) await clearTestCache() } @Test("Embed Base64 images handles invalid URLs gracefully") func testEmbedBase64ImagesHandlesInvalidURLs() async { // Clear cache first to ensure clean state await clearTestCache() let embedder = HTMLImageEmbedder() let htmlInvalid = """ """ let url = URL(string: "https://valid.com/image.jpg")! await cacheTestImage(url: url) let result = await embedder.embedBase64Images(in: htmlInvalid) // Invalid URL should remain unchanged #expect(result.contains("not a valid url")) // Valid URL should be embedded #expect(!result.contains("https://valid.com/image.jpg")) #expect(result.contains("data:image/jpeg;base64,")) await clearTestCache() } }