Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions Demo/SmartAsyncImageDemo/SmartAsyncImageDemo/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
import SmartAsyncImage
import SwiftUI

/// Helper to safely create picsum.photos URLs without force unwrapping
private func picsumURL(id: Int, width: Int, height: Int) -> URL {
URL(string: "https://picsum.photos/id/\(id)/\(width)/\(height)") ?? URL(filePath: "/")
}

struct ContentView: View {
private let sampleImages = [
(id: 10, title: "Forest"),
Expand All @@ -19,7 +24,7 @@ struct ContentView: View {
VStack(spacing: 24) {
// Simple usage with default placeholder
Section {
SmartAsyncImage(url: URL(string: "https://picsum.photos/id/1/400/300")!)
SmartAsyncImage(url: picsumURL(id: 1, width: 400, height: 300))
.aspectRatio(4/3, contentMode: .fit)
.clipShape(RoundedRectangle(cornerRadius: 12))
} header: {
Expand All @@ -28,7 +33,7 @@ struct ContentView: View {

// Custom content with phase handling
Section {
SmartAsyncImage(url: URL(string: "https://picsum.photos/id/15/400/300")!) { phase in
SmartAsyncImage(url: picsumURL(id: 15, width: 400, height: 300)) { phase in
switch phase {
case .empty, .loading:
ProgressView()
Expand Down Expand Up @@ -58,7 +63,7 @@ struct ContentView: View {
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 12) {
ForEach(sampleImages, id: \.id) { item in
VStack {
SmartAsyncImage(url: URL(string: "https://picsum.photos/id/\(item.id)/200/200")!)
SmartAsyncImage(url: picsumURL(id: item.id, width: 200, height: 200))
.aspectRatio(1, contentMode: .fit)
.clipShape(RoundedRectangle(cornerRadius: 8))
Text(item.title)
Expand Down
63 changes: 35 additions & 28 deletions Tests/SmartAsyncImageTests/SmartAsyncImageTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ struct SmartAsyncImageEncoderTests {
let encoder = SmartAsyncImageEncoder()

@Test("Encode URL to safe filename string")
func encodeURLToSafeString() {
let url = URL(string: "https://example.com/image.png")!
func encodeURLToSafeString() throws {
let url = try #require(URL(string: "https://example.com/image.png"))
let encoded = encoder.encode(url)

#expect(!encoded.contains("/"))
Expand All @@ -23,26 +23,26 @@ struct SmartAsyncImageEncoderTests {
}

@Test("Decode encoded string back to URL")
func decodeStringToURL() {
let originalURL = URL(string: "https://example.com/image.png")!
func decodeStringToURL() throws {
let originalURL = try #require(URL(string: "https://example.com/image.png"))
let encoded = encoder.encode(originalURL)
let decoded = encoder.decode(encoded)

#expect(decoded == originalURL)
}

@Test("Encode and decode URL with query parameters")
func encodeDecodeWithQueryParams() {
let url = URL(string: "https://example.com/image.png?size=large&format=webp")!
func encodeDecodeWithQueryParams() throws {
let url = try #require(URL(string: "https://example.com/image.png?size=large&format=webp"))
let encoded = encoder.encode(url)
let decoded = encoder.decode(encoded)

#expect(decoded == url)
}

@Test("Encode and decode URL with special characters")
func encodeDecodeWithSpecialChars() {
let url = URL(string: "https://example.com/path/to/image%20file.png")!
func encodeDecodeWithSpecialChars() throws {
let url = try #require(URL(string: "https://example.com/path/to/image%20file.png"))
let encoded = encoder.encode(url)
let decoded = encoder.decode(encoded)

Expand Down Expand Up @@ -80,7 +80,7 @@ struct SmartAsyncImageDiskCacheTests {
let diskCache = SmartAsyncImageDiskCache(fileManager: fileManager, folder: testFolder)

let testImage = createTestImage(color: .red, size: CGSize(width: 100, height: 100))
let testURL = URL(string: "https://example.com/test-image.png")!
let testURL = try #require(URL(string: "https://example.com/test-image.png"))

try await diskCache.save(testImage, key: testURL)
let loadedImage = try await diskCache.load(key: testURL)
Expand All @@ -104,7 +104,7 @@ struct SmartAsyncImageDiskCacheTests {
let testFolder = "TestSmartAsyncImageCache_\(UUID().uuidString)"
let diskCache = SmartAsyncImageDiskCache(fileManager: fileManager, folder: testFolder)

let testURL = URL(string: "https://example.com/non-existent.png")!
let testURL = try #require(URL(string: "https://example.com/non-existent.png"))
let loadedImage = try await diskCache.load(key: testURL)

#expect(loadedImage == nil)
Expand All @@ -122,8 +122,8 @@ struct SmartAsyncImageDiskCacheTests {
let image1 = createTestImage(color: .red, size: CGSize(width: 50, height: 50))
let image2 = createTestImage(color: .blue, size: CGSize(width: 100, height: 100))

let url1 = URL(string: "https://example.com/image1.png")!
let url2 = URL(string: "https://example.com/image2.png")!
let url1 = try #require(URL(string: "https://example.com/image1.png"))
let url2 = try #require(URL(string: "https://example.com/image2.png"))

try await diskCache.save(image1, key: url1)
try await diskCache.save(image2, key: url2)
Expand Down Expand Up @@ -152,7 +152,7 @@ struct SmartAsyncImageMemoryCacheMockTests {
@Test("Mock Cache returns same image for same URL")
func mockCacheReturnsSameImage() async throws {
let mockCache = MockMemoryCache()
let testURL = URL(string: "https://example.com/test.png")!
let testURL = try #require(URL(string: "https://example.com/test.png"))
let testImage = createTestImage(color: .purple, size: CGSize(width: 64, height: 64))

await mockCache.setImage(testImage, for: testURL)
Expand All @@ -162,9 +162,9 @@ struct SmartAsyncImageMemoryCacheMockTests {
}

@Test("Mock Cache miss returns nil from mock")
func mockCacheMissReturnsNil() async {
func mockCacheMissReturnsNil() async throws {
let mockCache = MockMemoryCache()
let testURL = URL(string: "https://example.com/nonexistent.png")!
let testURL = try #require(URL(string: "https://example.com/nonexistent.png"))

let result = await mockCache.getImage(for: testURL)
#expect(result == nil)
Expand All @@ -177,9 +177,16 @@ struct SmartAsyncImageMemoryCacheMockTests {
struct SmartAsyncImageMemoryCacheIntegrationTests {

// Use stable, small image URLs for testing
static let testImageURL = URL(string: "https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png")!
static let smallImageURL = URL(string: "https://www.google.com/favicon.ico")!
static let anotherImageURL = URL(string: "https://www.apple.com/favicon.ico")!
// Using computed properties with fallbacks to avoid force unwraps
static var testImageURL: URL {
URL(string: "https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png") ?? URL(filePath: "/")
}
static var smallImageURL: URL {
URL(string: "https://www.google.com/favicon.ico") ?? URL(filePath: "/")
}
static var anotherImageURL: URL {
URL(string: "https://www.apple.com/favicon.ico") ?? URL(filePath: "/")
}

/// Creates an isolated cache instance with its own disk cache folder
func createIsolatedCache() -> (cache: SmartAsyncImageMemoryCache, diskCache: SmartAsyncImageDiskCache, folder: String) {
Expand Down Expand Up @@ -320,7 +327,7 @@ struct SmartAsyncImageMemoryCacheIntegrationTests {
defer { cleanup(folder: folder) }

// This URL returns a 404
let invalidURL = URL(string: "https://httpstat.us/404")!
let invalidURL = try #require(URL(string: "https://httpstat.us/404"))

do {
_ = try await cache.image(for: invalidURL)
Expand All @@ -337,7 +344,7 @@ struct SmartAsyncImageMemoryCacheIntegrationTests {
defer { cleanup(folder: folder) }

// This returns JSON, not an image
let jsonURL = URL(string: "https://httpbin.org/json")!
let jsonURL = try #require(URL(string: "https://httpbin.org/json"))

do {
_ = try await cache.image(for: jsonURL)
Expand Down Expand Up @@ -398,8 +405,8 @@ struct SmartAsyncImageMemoryCacheIntegrationTests {
struct SmartAsyncImageViewModelTests {

@Test("Initial phase is empty")
func initialPhaseIsEmpty() {
let url = URL(string: "https://example.com/image.png")!
func initialPhaseIsEmpty() throws {
let url = try #require(URL(string: "https://example.com/image.png"))
let mockCache = MockMemoryCache()
let viewModel = SmartAsyncImageViewModel(url: url, cache: mockCache)

Expand All @@ -411,8 +418,8 @@ struct SmartAsyncImageViewModelTests {
}

@Test("Load transitions to loading phase")
func loadTransitionsToLoading() async {
let url = URL(string: "https://example.com/image.png")!
func loadTransitionsToLoading() async throws {
let url = try #require(URL(string: "https://example.com/image.png"))
let mockCache = MockMemoryCache()
await mockCache.setDelay(1.0) // Add delay to catch loading state

Expand All @@ -433,7 +440,7 @@ struct SmartAsyncImageViewModelTests {

@Test("Load succeeds with cached image")
func loadSucceedsWithCachedImage() async throws {
let url = URL(string: "https://example.com/image.png")!
let url = try #require(URL(string: "https://example.com/image.png"))
let mockCache = MockMemoryCache()
let testImage = createTestImage(color: .cyan, size: CGSize(width: 32, height: 32))
await mockCache.setImage(testImage, for: url)
Expand All @@ -453,7 +460,7 @@ struct SmartAsyncImageViewModelTests {

@Test("Load fails with error")
func loadFailsWithError() async throws {
let url = URL(string: "https://example.com/image.png")!
let url = try #require(URL(string: "https://example.com/image.png"))
let mockCache = MockMemoryCache()
await mockCache.setShouldFail(true)

Expand All @@ -472,7 +479,7 @@ struct SmartAsyncImageViewModelTests {

@Test("Cancel resets phase to empty")
func cancelResetsPhaseToEmpty() async throws {
let url = URL(string: "https://example.com/image.png")!
let url = try #require(URL(string: "https://example.com/image.png"))
let mockCache = MockMemoryCache()
await mockCache.setDelay(5.0) // Long delay

Expand All @@ -492,7 +499,7 @@ struct SmartAsyncImageViewModelTests {

@Test("Multiple loads are ignored if not empty")
func multipleLoadsIgnored() async throws {
let url = URL(string: "https://example.com/image.png")!
let url = try #require(URL(string: "https://example.com/image.png"))
let mockCache = MockMemoryCache()
await mockCache.setDelay(0.5)

Expand Down
Loading