
Sport vor der Arbeit fördert die Produktivität. (Foto: Jacob Lund / Shutterstock)
Zu viele von uns sind zu sehr verplant, übermäßig vernetzt und übermäßig stimuliert von all dem Lärm, den Unterbrechungen und der Komplexität der heutigen Gesellschaft. Der Preis dieses Lebensstils? Du erreichst die letzte Stunden deines letzten Tages und merkst, dass du dein größtes Potenzial für deine sinnlosesten Aktivitäten vergeudet hast.
+Herausragende Performer, Spitzenleute und Welt-Erschaffer spielen in einer anderen Liga. Die Elon Musks, Mark Zuckerbergs, große Künstler und Top-Wissenschaftler planen ihre Tage alle mit komplett anderer Geisteshaltung und Ritualen als jene, die im Hamsterrad gefangen sind.
+Als privater Coach von vielen der erfolgreichsten Unternehmer auf diesem Planeten und Gründer von The Titan Summit, das jeden Dezember in Toronto Ultra-Performer von mehr als 43 Nationen vier Tage für ein Elite-Training für Produktivität, Business-Acceleration und Lifestyle-Optimierung zusammenbringt, habe ich mit eigenen Augen beobachtet, wie Menschen, die in einer Woche mehr schaffen als die meisten in einem Vierteljahr, ihre Ergebnisse erzielen. Ich habe außerdem eine komplette Methodik für exponentiale Produktivität entwickelt, die ich den Teilnehmern bei diesem Event beibringe.
+Hier sind vier der wegweisenden Elemente meines Ansatzes:
++
Die Art, wie du deinen Tag kraftvoll startest, bestimmt, wie produktiv du ihn gestaltest. Reserviere die ersten 60 Minuten für persönliche Vorbereitung. Wie die spartanischen Krieger sagten: „Schwitze mehr beim Trainieren, dann wirst du im Krieg weniger bluten.“ Verbringe deine ersten 20 Minuten mit intensivem Sport. Das Schwitzen setzt BDNF frei, einen Botenstoff, der tatsächlich neurale Verbindungen wachsen lässt. Ein Workout produziert auch Dopamin (der Neurotransmitter für Motivation) und Serotonin, welches gute Laune macht.
+Schau dir im nächsten 20-Minuten-Slot deinen Jahresplan an und denk gründlich über deine Ziele für dieses Quartal nach. Klarheit geht der Meisterschaft voraus, und diese Übung wird den Tag über deine Konzentration verstärken.
+Investiere die letzten 20 Minuten dieses Morgens für regelmäßiges Lernen. Lies zum Beispiel Autobiografien großer Persönlichkeiten, hör dir einen Podcast über das Führen an oder lade dir die Lektionen des Vortages in dein Journal herunter.
++
Allein diese Angewohnheit hat meinen Kunden einen gewaltigen Mehrwert verschafft. Kurz gesagt: Widme dich an den nächsten 90 Tagen die ersten 90 Minuten deines Arbeitstages deiner einzigen, wichtigsten Chance, der einen Sache, die, wenn du sie fehlerlos durchführst, alles aufgehen lässt.
+Durchschnittliche Mitarbeiter kommen zur Arbeit und checken ihre Mails oder surfen im Netz. Für den echten Anführer ist die Ankunft im Büro der Beginn der Showtime. Sie begreifen, dass das Entwickeln einer Besessenheit für ihre entscheidenden paar Prioritäten legendäre Resultate freisetzt.
++
Gute Studien belegen, dass die besten Athleten der Welt das nicht dafür waren, was sie in ihrem Sport leisteten, sondern wie effektiv sie sich erholten. Es waren beispielsweise die Rituale, welche die Star-Tennisspieler zwischen ihren Punkten vollzogen, die sie zu Stars machten. Was die russischen Gewichtheber so unschlagbar machte, war ihr Verhältnis von Arbeit zu Erholungspausen.
+Also, stell dir einen Wecker auf 60 Minuten und schalte in dieser Zeitspanne alle technischen Geräte aus, schließ deine Tür und tauche mit voller Wucht in ein Projekt ein, das wichtig ist. Erhol dich dann mit einer echten Pause wie Walking, Musikhören oder Lesen. Probiere diese Vorgehensweise mal für einen Monat aus und spüre den Nutzen.
++
Verhaltensforscher haben das Phänomen des „emotionalen Ansteckungseffekts“ entdeckt. Dieses beschreibt, dass wir unbewusst die Glaubenssätze, Gefühle und Verhalten der Leute übernehmen, mit denen wir die meiste Zeit verbringen.
+Du möchtest ultra-fit werden? Einer der besten Wege dahin ist es, dich einer Laufgruppe anzuschließen oder dich mit Athleten anzufreunden. Du möchtest glücklicher sein? Dann streich die Energieräuber und Meckerer aus deinem Leben. Du bist darauf aus, eine echte Weltklassefirma aufzubauen? Dann fang an, viel mehr Zeit mit denen zu verbringen, die das bereits getan haben. Ihre Geisteshaltung und Lebensart wird dich mit der Zeit automatisch beeinflussen. Und mit Leuten Umgang zu haben, deren Leben du gern führen würdest, zeigt dir, was alles möglich ist. Und wenn du erstmal mehr weißt, kannst du auch mehr erreichen.
+Schlussendlich möchte ich dich noch an die Endlichkeit des Lebens erinnern. Sogar das längste ist ein relativ kurzer Ritt. Du schuldest es dem Talent, mit dem du geboren wurdest, dem Team, das du führst, der Familie, die du liebst und der Welt, die danach verlangt, dass du deine Größe zeigst, um das zu Nötige zu tun, exponentielle Produktivität zu erlangen.
+Hoffentlich nimmst du diese von mir geteilte Methodik an, um deinem Führungspotenzial gerecht zu werden.
+Wie immer hoffe ich, dass diese Zeilen dir echten Mehrwert geliefert haben und dich in die Lage versetzen, den Rest dieses Jahres „in der kostbaren Luft vollständiger Meisterschaft“ zu vollenden.
+Deine Partner sind Führungsqualität und Erfolg,
+Dieser Artikel erschien zuerst auf Medium.com. Übersetzung: Anja Braun.
+
+
+
+
+
+
+ """
+
+ // We need to test the private method indirectly via cacheBookmarkWithMetadata
+ // For now, we'll test the regex pattern separately
+ let pattern = #"No images here
" + + let pattern = #"
+
+
+ """
+
+ let pattern = #"Text mit fett.
" - let expected = "Text mit fett." - - XCTAssertEqual(html.stripHTMLSimple, expected) - } - - func testStripHTMLSimple_HTMLEntities() { - let html = "Text mit Leerzeichen, & Zeichen und "Anführungszeichen".
" - let expected = "Text mit Leerzeichen, & Zeichen und \"Anführungszeichen\"." - - XCTAssertEqual(html.stripHTMLSimple, expected) - } - - func testStripHTMLSimple_MoreEntities() { - let html = "<Tag> und 'Apostroph'
" - let expected = "Absatz mit kursiv und fett.
Text mit Whitespace
" - let expected = "Text mit Whitespace" - - XCTAssertEqual(html.stripHTMLSimple, expected) - } - - // MARK: - Performance Tests - - func testStripHTML_Performance() { - let largeHTML = String(repeating: "Dies ist ein Test mit vielen HTML Tags.
", count: 1000) - - measure { - _ = largeHTML.stripHTML - } - } - - func testStripHTMLSimple_Performance() { - let largeHTML = String(repeating: "Dies ist ein Test mit vielen HTML Tags.
", count: 1000) - - measure { - _ = largeHTML.stripHTMLSimple - } - } - // MARK: - Edge Cases func testStripHTML_MalformedHTML() { diff --git a/readeckTests/Utils/HTMLImageEmbedderTests.swift b/readeckTests/Utils/HTMLImageEmbedderTests.swift new file mode 100644 index 0000000..e67dde8 --- /dev/null +++ b/readeckTests/Utils/HTMLImageEmbedderTests.swift @@ -0,0 +1,306 @@ +// +// 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 = """ + + +
+
+
+
+ """
+
+ 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()
+ }
+}
diff --git a/readeckTests/Utils/HTMLImageExtractorTests.swift b/readeckTests/Utils/HTMLImageExtractorTests.swift
new file mode 100644
index 0000000..adb42d2
--- /dev/null
+++ b/readeckTests/Utils/HTMLImageExtractorTests.swift
@@ -0,0 +1,194 @@
+//
+// HTMLImageExtractorTests.swift
+// readeckTests
+//
+// Created by Ilyas Hallak on 30.11.25.
+//
+
+import Testing
+import Foundation
+@testable import readeck
+
+@Suite("HTMLImageExtractor Tests")
+struct HTMLImageExtractorTests {
+
+ // MARK: - Test Data
+
+ private let htmlWithImages = """
+
+
+
+
+
+
+
+ """
+
+ private let htmlWithMixedURLs = """
+
+
+
+
+
+
+
+ """
+
+ private let htmlWithoutImages = """
+
+
+ This is just text content with no images.
+
+
+
+ """
+
+ let extractor = HTMLImageExtractor()
+ let imageURLs = extractor.extract(from: htmlWithRelative)
+
+ #expect(imageURLs.count == 1)
+ #expect(imageURLs.first == "https://valid.com/image.jpg")
+ }
+
+ @Test("Extract handles empty HTML string")
+ func testExtractHandlesEmptyHTML() {
+ let extractor = HTMLImageExtractor()
+ let imageURLs = extractor.extract(from: htmlEmpty)
+
+ #expect(imageURLs.isEmpty)
+ }
+
+ @Test("Extract ignores data URI images")
+ func testExtractIgnoresDataURIs() {
+ let htmlWithDataURI = """
+
+ """
+
+ let extractor = HTMLImageExtractor()
+ let imageURLs = extractor.extract(from: htmlWithDataURI)
+
+ #expect(imageURLs.count == 1)
+ #expect(imageURLs.first == "https://example.com/real-image.jpg")
+
+ // Verify no data URIs are included
+ for url in imageURLs {
+ #expect(!url.hasPrefix("data:"))
+ }
+ }
+
+ // MARK: - Hero/Thumbnail Tests
+
+ @Test("Extract with hero image prepends it to array")
+ func testExtractWithHeroImagePrependsToArray() {
+ let extractor = HTMLImageExtractor()
+ let heroURL = "https://example.com/hero.jpg"
+
+ let imageURLs = extractor.extract(
+ from: htmlWithImages,
+ heroImageURL: heroURL,
+ thumbnailURL: nil
+ )
+
+ #expect(imageURLs.count == 4) // 3 from HTML + 1 hero
+ #expect(imageURLs.first == heroURL) // Hero should be at position 0
+ #expect(imageURLs.contains("https://example.com/image1.jpg"))
+ }
+
+ @Test("Extract with thumbnail prepends it when no hero image")
+ func testExtractWithThumbnailPrependsWhenNoHero() {
+ let extractor = HTMLImageExtractor()
+ let thumbnailURL = "https://example.com/thumbnail.jpg"
+
+ let imageURLs = extractor.extract(
+ from: htmlWithImages,
+ heroImageURL: nil,
+ thumbnailURL: thumbnailURL
+ )
+
+ #expect(imageURLs.count == 4) // 3 from HTML + 1 thumbnail
+ #expect(imageURLs.first == thumbnailURL) // Thumbnail should be at position 0
+ }
+
+ @Test("Extract prefers hero image over thumbnail when both provided")
+ func testExtractPrefersHeroOverThumbnail() {
+ let extractor = HTMLImageExtractor()
+ let heroURL = "https://example.com/hero.jpg"
+ let thumbnailURL = "https://example.com/thumbnail.jpg"
+
+ let imageURLs = extractor.extract(
+ from: htmlWithImages,
+ heroImageURL: heroURL,
+ thumbnailURL: thumbnailURL
+ )
+
+ #expect(imageURLs.count == 4) // 3 from HTML + 1 hero (thumbnail ignored)
+ #expect(imageURLs.first == heroURL) // Hero takes precedence
+ #expect(!imageURLs.contains(thumbnailURL)) // Thumbnail should NOT be added
+ }
+
+ @Test("Extract with hero and thumbnail but no HTML images")
+ func testExtractWithHeroAndNoHTMLImages() {
+ let extractor = HTMLImageExtractor()
+ let heroURL = "https://example.com/hero.jpg"
+
+ let imageURLs = extractor.extract(
+ from: htmlWithoutImages,
+ heroImageURL: heroURL,
+ thumbnailURL: nil
+ )
+
+ #expect(imageURLs.count == 1)
+ #expect(imageURLs.first == heroURL)
+ }
+}
diff --git a/readeckTests/Utils/KingfisherImagePrefetcherTests.swift b/readeckTests/Utils/KingfisherImagePrefetcherTests.swift
new file mode 100644
index 0000000..0889aa6
--- /dev/null
+++ b/readeckTests/Utils/KingfisherImagePrefetcherTests.swift
@@ -0,0 +1,239 @@
+//
+// KingfisherImagePrefetcherTests.swift
+// readeckTests
+//
+// Created by Ilyas Hallak on 30.11.25.
+//
+
+import Testing
+import Foundation
+import Kingfisher
+@testable import readeck
+import UIKit
+
+@Suite("KingfisherImagePrefetcher Tests")
+struct KingfisherImagePrefetcherTests {
+
+ // MARK: - Test Setup & Helpers
+
+ /// Mock server URL for test images
+ private let testImageURL1 = URL(string: "https://via.placeholder.com/150/FF0000/FFFFFF?text=Test1")!
+ private let testImageURL2 = URL(string: "https://via.placeholder.com/150/00FF00/FFFFFF?text=Test2")!
+ private let testImageURL3 = URL(string: "https://via.placeholder.com/150/0000FF/FFFFFF?text=Test3")!
+
+ /// Creates a simple test image for caching
+ private func createTestImage() -> KFCrossPlatformImage {
+ #if os(iOS)
+ let size = CGSize(width: 10, height: 10)
+ UIGraphicsBeginImageContextWithOptions(size, false, 1.0)
+ UIColor.blue.setFill()
+ UIRectFill(CGRect(origin: .zero, size: size))
+ let image = UIGraphicsGetImageFromCurrentImageContext()!
+ UIGraphicsEndImageContext()
+ return image
+ #elseif os(macOS)
+ let size = NSSize(width: 10, height: 10)
+ let image = NSImage(size: size)
+ image.lockFocus()
+ NSColor.blue.setFill()
+ NSBezierPath.fill(NSRect(origin: .zero, size: size))
+ image.unlockFocus()
+ return image
+ #endif
+ }
+
+ /// Clears Kingfisher cache after tests
+ private func clearCache() async {
+ await ImageCache.default.clearCache()
+ }
+
+ /// Checks if an image is cached
+ private func isImageCached(forKey key: String) async -> Bool {
+ await withCheckedContinuation { continuation in
+ ImageCache.default.retrieveImage(forKey: key) { result in
+ switch result {
+ case .success(let cacheResult):
+ continuation.resume(returning: cacheResult.image != nil)
+ case .failure:
+ continuation.resume(returning: false)
+ }
+ }
+ }
+ }
+
+ // MARK: - Prefetch Tests
+
+ @Test("Prefetch images handles empty URL array")
+ func testPrefetchImagesHandlesEmptyArray() async {
+ let prefetcher = KingfisherImagePrefetcher()
+ let emptyURLs: [URL] = []
+
+ // Should complete without errors
+ await prefetcher.prefetchImages(urls: emptyURLs)
+
+ // No assertions needed - just verify it doesn't crash
+ #expect(emptyURLs.isEmpty)
+ }
+
+ @Test("Prefetch images uses never expiration for disk cache")
+ func testPrefetchImagesUsesNeverExpiration() async {
+ // This test verifies the configuration is set correctly
+ // The actual implementation uses .diskCacheExpiration(.never)
+ let prefetcher = KingfisherImagePrefetcher()
+
+ // Pre-cache a test image to verify it persists
+ let testURL = URL(string: "https://example.com/test.jpg")!
+ let testImage = createTestImage()
+
+ try? await ImageCache.default.store(
+ testImage,
+ forKey: testURL.cacheKey,
+ options: KingfisherParsedOptionsInfo([.diskCacheExpiration(.never)])
+ )
+
+ let isCached = await isImageCached(forKey: testURL.cacheKey)
+ #expect(isCached == true)
+
+ await clearCache()
+ }
+
+ @Test("Verify prefetched images confirms cache status")
+ func testVerifyPrefetchedImagesConfirmsCacheStatus() async {
+ let prefetcher = KingfisherImagePrefetcher()
+
+ // Manually cache some test images
+ let url1 = URL(string: "https://example.com/cached1.jpg")!
+ let url2 = URL(string: "https://example.com/cached2.jpg")!
+ let url3 = URL(string: "https://example.com/not-cached.jpg")!
+
+ let testImage = createTestImage()
+ try? await ImageCache.default.store(testImage, forKey: url1.cacheKey)
+ try? await ImageCache.default.store(testImage, forKey: url2.cacheKey)
+
+ // Verify the cached ones
+ await prefetcher.verifyPrefetchedImages([url1, url2, url3])
+
+ // Check that first two are cached
+ let isCached1 = await isImageCached(forKey: url1.cacheKey)
+ let isCached2 = await isImageCached(forKey: url2.cacheKey)
+ let isCached3 = await isImageCached(forKey: url3.cacheKey)
+
+ #expect(isCached1 == true)
+ #expect(isCached2 == true)
+ #expect(isCached3 == false)
+
+ await clearCache()
+ }
+
+ // MARK: - Custom Cache Key Tests
+
+ @Test("Cache image with custom key stores correctly")
+ func testCacheImageWithCustomKeyStoresCorrectly() async {
+ let prefetcher = KingfisherImagePrefetcher()
+ let customKey = "bookmark-123-hero"
+
+ // Pre-cache a test image with URL key so it can be "downloaded"
+ let sourceURL = URL(string: "https://example.com/hero.jpg")!
+ let testImage = createTestImage()
+ try? await ImageCache.default.store(testImage, forKey: sourceURL.cacheKey)
+
+ // Now use the prefetcher to cache with custom key
+ await prefetcher.cacheImageWithCustomKey(url: sourceURL, key: customKey)
+
+ // Verify it's cached with custom key
+ let isCached = await isImageCached(forKey: customKey)
+ #expect(isCached == true)
+
+ await clearCache()
+ }
+
+ @Test("Cache image with custom key skips if already cached")
+ func testCacheImageWithCustomKeySkipsIfAlreadyCached() async {
+ let prefetcher = KingfisherImagePrefetcher()
+ let customKey = "bookmark-456-hero"
+ let sourceURL = URL(string: "https://example.com/hero2.jpg")!
+
+ // Pre-cache with custom key
+ let testImage = createTestImage()
+ try? await ImageCache.default.store(testImage, forKey: customKey)
+
+ // Call again - should skip (verify by checking it doesn't fail)
+ await prefetcher.cacheImageWithCustomKey(url: sourceURL, key: customKey)
+
+ // Should still be cached
+ let isCached = await isImageCached(forKey: customKey)
+ #expect(isCached == true)
+
+ await clearCache()
+ }
+
+ // MARK: - Clear Cache Tests
+
+ @Test("Clear cached images removes all specified URLs")
+ func testClearCachedImagesRemovesAllURLs() async {
+ let prefetcher = KingfisherImagePrefetcher()
+
+ // Cache some test images
+ let url1 = URL(string: "https://example.com/clear1.jpg")!
+ let url2 = URL(string: "https://example.com/clear2.jpg")!
+ let testImage = createTestImage()
+
+ try? await ImageCache.default.store(testImage, forKey: url1.cacheKey)
+ try? await ImageCache.default.store(testImage, forKey: url2.cacheKey)
+
+ // Verify they are cached
+ var isCached1 = await isImageCached(forKey: url1.cacheKey)
+ var isCached2 = await isImageCached(forKey: url2.cacheKey)
+ #expect(isCached1 == true)
+ #expect(isCached2 == true)
+
+ // Clear them
+ await prefetcher.clearCachedImages(urls: [url1, url2])
+
+ // Verify they are removed
+ isCached1 = await isImageCached(forKey: url1.cacheKey)
+ isCached2 = await isImageCached(forKey: url2.cacheKey)
+ #expect(isCached1 == false)
+ #expect(isCached2 == false)
+ }
+
+ @Test("Clear cached images handles empty array")
+ func testClearCachedImagesHandlesEmptyArray() async {
+ let prefetcher = KingfisherImagePrefetcher()
+ let emptyURLs: [URL] = []
+
+ // Should complete without errors
+ await prefetcher.clearCachedImages(urls: emptyURLs)
+
+ // No assertions needed - just verify it doesn't crash
+ #expect(emptyURLs.isEmpty)
+ }
+
+ // MARK: - Integration Tests
+
+ @Test("Prefetch and verify workflow")
+ func testPrefetchAndVerifyWorkflow() async {
+ let prefetcher = KingfisherImagePrefetcher()
+
+ // Pre-populate cache with test images
+ let urls = [
+ URL(string: "https://example.com/workflow1.jpg")!,
+ URL(string: "https://example.com/workflow2.jpg")!
+ ]
+
+ let testImage = createTestImage()
+ for url in urls {
+ try? await ImageCache.default.store(testImage, forKey: url.cacheKey)
+ }
+
+ // Verify they were cached
+ await prefetcher.verifyPrefetchedImages(urls)
+
+ for url in urls {
+ let isCached = await isImageCached(forKey: url.cacheKey)
+ #expect(isCached == true)
+ }
+
+ await clearCache()
+ }
+}
diff --git a/readeckUITests/readeckUITests.swift b/readeckUITests/readeckUITests.swift
index f8c0422..0df2b42 100644
--- a/readeckUITests/readeckUITests.swift
+++ b/readeckUITests/readeckUITests.swift
@@ -6,7 +6,6 @@
//
import XCTest
-import SnapshotHelper
final class readeckUITests: XCTestCase {
@@ -16,7 +15,7 @@ final class readeckUITests: XCTestCase {
// In UI tests it is usually best to stop immediately when a failure occurs.
continueAfterFailure = false
- // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
+ // In UI tests it's important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
}
override func tearDownWithError() throws {
@@ -27,9 +26,7 @@ final class readeckUITests: XCTestCase {
func testExample() throws {
// UI tests must launch the application that they test.
let app = XCUIApplication()
- setupSnapshot(app)
app.launch()
- snapshot("01LaunchScreen")
// Use XCTAssert and related functions to verify your tests produce the correct results.
}