import XCTest import UIKit @testable import MangaReader /// Tests de integración para el flujo completo: scraper -> storage -> view /// Pruebas de descarga de capítulos y navegación entre vistas @MainActor final class IntegrationTests: XCTestCase { var scraper: ManhwaWebScraper! var storageService: StorageService! // MARK: - Setup & Teardown override func setUp() async throws { try await super.setUp() // Limpiar estado antes de cada test UserDefaults.standard.removeObject(forKey: "favoriteMangas") UserDefaults.standard.removeObject(forKey: "readingProgress") scraper = ManhwaWebScraper.shared storageService = StorageService.shared storageService.clearAllDownloads() // Esperar un momento para asegurar limpieza completa try await Task.sleep(nanoseconds: 100_000_000) // 0.1 segundos } override func tearDown() async throws { // Limpiar después de los tests storageService.clearAllDownloads() UserDefaults.standard.removeObject(forKey: "favoriteMangas") UserDefaults.standard.removeObject(forKey: "readingProgress") scraper = nil storageService = nil try await super.tearDown() } // MARK: - Complete Flow Tests: Scraper -> Storage func testCompleteScrapingAndStorageFlow() async throws { // Este test simula el flujo completo: scraper -> storage // 1. Simular datos del scraper (capítulos) let mockChapters = [ Chapter(number: 10, title: "Chapter 10", url: "url10", slug: "slug10"), Chapter(number: 9, title: "Chapter 9", url: "url9", slug: "slug9"), Chapter(number: 8, title: "Chapter 8", url: "url8", slug: "slug8") ] // 2. Guardar progreso de lectura simulado let progress = ReadingProgress( mangaSlug: "test-manga", chapterNumber: 9, pageNumber: 5, timestamp: Date() ) storageService.saveReadingProgress(progress) // 3. Verificar que el progreso se guardó let retrievedProgress = storageService.getReadingProgress( mangaSlug: "test-manga", chapterNumber: 9 ) XCTAssertNotNil(retrievedProgress) XCTAssertEqual(retrievedProgress?.pageNumber, 5) // 4. Marcar manga como favorito storageService.saveFavorite(mangaSlug: "test-manga") // 5. Verificar favoritos let favorites = storageService.getFavorites() XCTAssertTrue(favorites.contains("test-manga")) // 6. Simular guardado de capítulo descargado let pages = mockChapters[0].number == 10 ? [ MangaPage(url: "page1.jpg", index: 0), MangaPage(url: "page2.jpg", index: 1) ] : [] let downloadedChapter = DownloadedChapter( mangaSlug: "test-manga", mangaTitle: "Test Manga", chapterNumber: 10, pages: pages, downloadedAt: Date() ) storageService.saveDownloadedChapter(downloadedChapter) // 7. Verificar capítulo descargado XCTAssertTrue(storageService.isChapterDownloaded( mangaSlug: "test-manga", chapterNumber: 10 )) // 8. Obtener todos los datos relacionados let allProgress = storageService.getAllReadingProgress() let lastRead = storageService.getLastReadChapter(mangaSlug: "test-manga") let downloadedChapters = storageService.getDownloadedChapters() XCTAssertEqual(allProgress.count, 1) XCTAssertNotNil(lastRead) XCTAssertEqual(downloadedChapters.count, 1) } func testChapterDownloadFlow() async throws { // Simular el flujo completo de descarga de un capítulo // 1. Crear imágenes de prueba let image1 = createTestImage(color: .red, size: CGSize(width: 800, height: 1200)) let image2 = createTestImage(color: .blue, size: CGSize(width: 800, height: 1200)) let image3 = createTestImage(color: .green, size: CGSize(width: 800, height: 1200)) // 2. Guardar imágenes let url1 = try await storageService.saveImage( image1, mangaSlug: "test-manga", chapterNumber: 1, pageIndex: 0 ) let url2 = try await storageService.saveImage( image2, mangaSlug: "test-manga", chapterNumber: 1, pageIndex: 1 ) let url3 = try await storageService.saveImage( image3, mangaSlug: "test-manga", chapterNumber: 1, pageIndex: 2 ) // 3. Verificar que las imágenes se guardaron XCTAssertTrue(FileManager.default.fileExists(atPath: url1.path)) XCTAssertTrue(FileManager.default.fileExists(atPath: url2.path)) XCTAssertTrue(FileManager.default.fileExists(atPath: url3.path)) // 4. Crear objeto de capítulo descargado let pages = [ MangaPage(url: url1.absoluteString, index: 0, isCached: true), MangaPage(url: url2.absoluteString, index: 1, isCached: true), MangaPage(url: url3.absoluteString, index: 2, isCached: true) ] let downloadedChapter = DownloadedChapter( mangaSlug: "test-manga", mangaTitle: "Test Manga", chapterNumber: 1, pages: pages, downloadedAt: Date() ) // 5. Guardar metadatos del capítulo storageService.saveDownloadedChapter(downloadedChapter) // 6. Verificar que el capítulo está marcado como descargado XCTAssertTrue(storageService.isChapterDownloaded( mangaSlug: "test-manga", chapterNumber: 1 )) // 7. Recuperar el capítulo descargado let retrieved = storageService.getDownloadedChapter( mangaSlug: "test-manga", chapterNumber: 1 ) XCTAssertNotNil(retrieved) XCTAssertEqual(retrieved?.pages.count, 3) // 8. Cargar imágenes desde disco let loadedImage1 = storageService.loadImage( mangaSlug: "test-manga", chapterNumber: 1, pageIndex: 0 ) let loadedImage2 = storageService.loadImage( mangaSlug: "test-manga", chapterNumber: 1, pageIndex: 1 ) let loadedImage3 = storageService.loadImage( mangaSlug: "test-manga", chapterNumber: 1, pageIndex: 2 ) XCTAssertNotNil(loadedImage1) XCTAssertNotNil(loadedImage2) XCTAssertNotNil(loadedImage3) } func testReadingProgressTrackingFlow() async throws { // Simular el flujo de seguimiento de progreso de lectura let mangaSlug = "tower-of-god" // 1. Usuario comienza a leer capítulo 1 let progress1 = ReadingProgress( mangaSlug: mangaSlug, chapterNumber: 1, pageNumber: 0, timestamp: Date() ) storageService.saveReadingProgress(progress1) // 2. Usuario avanza a página 5 let progress2 = ReadingProgress( mangaSlug: mangaSlug, chapterNumber: 1, pageNumber: 5, timestamp: Date().addingTimeInterval(60) // 1 minuto después ) storageService.saveReadingProgress(progress2) // 3. Usuario cambia al capítulo 2 let progress3 = ReadingProgress( mangaSlug: mangaSlug, chapterNumber: 2, pageNumber: 0, timestamp: Date().addingTimeInterval(120) // 2 minutos después ) storageService.saveReadingProgress(progress3) // 4. Usuario lee capítulo 2 hasta página 10 let progress4 = ReadingProgress( mangaSlug: mangaSlug, chapterNumber: 2, pageNumber: 10, timestamp: Date().addingTimeInterval(300) // 5 minutos después ) storageService.saveReadingProgress(progress4) // 5. Verificar progreso del capítulo 1 let ch1Progress = storageService.getReadingProgress( mangaSlug: mangaSlug, chapterNumber: 1 ) XCTAssertEqual(ch1Progress?.pageNumber, 5) // 6. Verificar progreso del capítulo 2 let ch2Progress = storageService.getReadingProgress( mangaSlug: mangaSlug, chapterNumber: 2 ) XCTAssertEqual(ch2Progress?.pageNumber, 10) // 7. Verificar último capítulo leído let lastRead = storageService.getLastReadChapter(mangaSlug: mangaSlug) XCTAssertEqual(lastRead?.chapterNumber, 2) // 8. Verificar que el capítulo se marca como completado XCTAssertTrue(ch2Progress?.isCompleted ?? false) } func testFavoriteManagementFlow() { // Simular el flujo de gestión de favoritos let mangaSlugs = [ "solo-leveling", "tower-of-god", "the-beginning-after-the-end", "omniscient-reader" ] // 1. Agregar varios favoritos mangaSlugs.forEach { slug in storageService.saveFavorite(mangaSlug: slug) } // 2. Verificar que todos están en favoritos let favorites = storageService.getFavorites() XCTAssertEqual(favorites.count, 4) mangaSlugs.forEach { slug in XCTAssertTrue(storageService.isFavorite(mangaSlug: slug)) } // 3. Remover uno storageService.removeFavorite(mangaSlug: "tower-of-god") // 4. Verificar que se eliminó XCTAssertFalse(storageService.isFavorite(mangaSlug: "tower-of-god")) XCTAssertEqual(storageService.getFavorites().count, 3) // 5. Intentar agregar duplicado storageService.saveFavorite(mangaSlug: "solo-leveling") // 6. Verificar que no se duplicó let updatedFavorites = storageService.getFavorites() XCTAssertEqual(updatedFavorites.count, 3) // 7. Contar ocurrencias let soloLevelingCount = updatedFavorites.filter { $0 == "solo-leveling" }.count XCTAssertEqual(soloLevelingCount, 1, "Should only appear once") } // MARK: - Multi-Manga Scenarios func testMultipleMangasProgressTracking() async throws { // Simular seguimiento de progreso para múltiples mangas let mangas = [ "solo-leveling", "tower-of-god", "the-beginning-after-the-end" ] // Agregar progreso para cada manga for (index, manga) in mangas.enumerated() { let progress = ReadingProgress( mangaSlug: manga, chapterNumber: index + 1, pageNumber: (index + 1) * 10, timestamp: Date().addingTimeInterval(Double(index * 100)) ) storageService.saveReadingProgress(progress) } // Verificar progreso individual for (index, manga) in mangas.enumerated() { let progress = storageService.getLastReadChapter(mangaSlug: manga) XCTAssertNotNil(progress) XCTAssertEqual(progress?.chapterNumber, index + 1) } // Verificar todo el progreso let allProgress = storageService.getAllReadingProgress() XCTAssertEqual(allProgress.count, 3) } func testMultipleChapterDownloads() async throws { // Simular descarga de múltiples capítulos de diferentes mangas let downloads = [ ("manga1", 1), ("manga1", 2), ("manga2", 1), ("manga2", 3), ("manga3", 5) ] // Crear y guardar capítulos descargados for (manga, chapter) in downloads { let chapter = DownloadedChapter( mangaSlug: manga, mangaTitle: "Manga \(manga)", chapterNumber: chapter, pages: [], downloadedAt: Date() ) storageService.saveDownloadedChapter(chapter) } // Verificar todos los capítulos descargados let allDownloaded = storageService.getDownloadedChapters() XCTAssertEqual(allDownloaded.count, 5) // Verificar capítulos por manga let manga1Chapters = allDownloaded.filter { $0.mangaSlug == "manga1" } XCTAssertEqual(manga1Chapters.count, 2) let manga2Chapters = allDownloaded.filter { $0.mangaSlug == "manga2" } XCTAssertEqual(manga2Chapters.count, 2) let manga3Chapters = allDownloaded.filter { $0.mangaSlug == "manga3" } XCTAssertEqual(manga3Chapters.count, 1) } // MARK: - Error Handling Scenarios func testDownloadFlowWithMissingImages() async throws { // Simular descarga con imágenes faltantes // Guardar solo algunas imágenes let image1 = createTestImage(color: .red, size: CGSize(width: 800, height: 1200)) _ = try await storageService.saveImage( image1, mangaSlug: "test-manga", chapterNumber: 1, pageIndex: 0 ) // Intentar cargar imagen que no existe let missingImage = storageService.loadImage( mangaSlug: "test-manga", chapterNumber: 1, pageIndex: 1 ) XCTAssertNil(missingImage, "Missing image should return nil") // Verificar que la imagen existente sí se carga let existingImage = storageService.loadImage( mangaSlug: "test-manga", chapterNumber: 1, pageIndex: 0 ) XCTAssertNotNil(existingImage, "Existing image should load successfully") } func testStorageCleanupFlow() async throws { // Simular flujo de limpieza de almacenamiento // 1. Llenar almacenamiento con datos for i in 0..<5 { let image = createTestImage(color: .blue, size: CGSize(width: 800, height: 1200)) _ = try await storageService.saveImage( image, mangaSlug: "test", chapterNumber: i, pageIndex: 0 ) let chapter = DownloadedChapter( mangaSlug: "test", mangaTitle: "Test", chapterNumber: i, pages: [], downloadedAt: Date() ) storageService.saveDownloadedChapter(chapter) } // Verificar que hay datos XCTAssertGreaterThan(storageService.getStorageSize(), 0) XCTAssertEqual(storageService.getDownloadedChapters().count, 5) // 2. Limpiar todo storageService.clearAllDownloads() // 3. Verificar que todo se eliminó XCTAssertEqual(storageService.getStorageSize(), 0) XCTAssertEqual(storageService.getDownloadedChapters().count, 0) } // MARK: - Data Persistence Tests func testDataPersistenceAcrossOperations() async throws { // Verificar que los datos persisten a través de múltiples operaciones // 1. Guardar datos iniciales storageService.saveFavorite(mangaSlug: "persistent-manga") let progress1 = ReadingProgress( mangaSlug: "persistent-manga", chapterNumber: 1, pageNumber: 5, timestamp: Date() ) storageService.saveReadingProgress(progress1) // 2. Verificar que existen XCTAssertTrue(storageService.isFavorite(mangaSlug: "persistent-manga")) XCTAssertNotNil(storageService.getReadingProgress( mangaSlug: "persistent-manga", chapterNumber: 1 )) // 3. Realizar operaciones intermedias storageService.saveFavorite(mangaSlug: "temp-manga") storageService.removeFavorite(mangaSlug: "temp-manga") // 4. Verificar que los datos originales persistieron XCTAssertTrue(storageService.isFavorite(mangaSlug: "persistent-manga")) let originalProgress = storageService.getReadingProgress( mangaSlug: "persistent-manga", chapterNumber: 1 ) XCTAssertEqual(originalProgress?.pageNumber, 5) } // MARK: - Concurrent Operations Tests func testConcurrentFavoriteOperations() { // Probar operaciones concurrentes en favoritos let expectations = (0..<10).map { _ in XCTestExpectation(description: "Favorite operation") } let queue = DispatchQueue.global(qos: .userInitiated) for i in 0..<10 { queue.async { self.storageService.saveFavorite(mangaSlug: "manga-\(i)") expectations[i].fulfill() } } wait(for: expectations, timeout: 5.0) let favorites = storageService.getFavorites() XCTAssertEqual(favorites.count, 10) } func testConcurrentProgressOperations() { // Probar operaciones concurrentes de progreso let expectations = (0..<10).map { _ in XCTestExpectation(description: "Progress operation") } let queue = DispatchQueue.global(qos: .userInitiated) for i in 0..<10 { queue.async { let progress = ReadingProgress( mangaSlug: "manga-\(i % 3)", chapterNumber: i, pageNumber: i * 2, timestamp: Date() ) self.storageService.saveReadingProgress(progress) expectations[i].fulfill() } } wait(for: expectations, timeout: 5.0) let allProgress = storageService.getAllReadingProgress() XCTAssertEqual(allProgress.count, 10) } func testConcurrentImageOperations() async throws { // Probar guardado concurrente de imágenes await withTaskGroup(of: URL.self) { group in for i in 0..<20 { group.addTask { let image = self.createTestImage( color: .red, size: CGSize(width: 800, height: 1200) ) return try! await self.storageService.saveImage( image, mangaSlug: "concurrent-test", chapterNumber: 1, pageIndex: i ) } } } // Verificar que todas las imágenes se guardaron for i in 0..<20 { let image = storageService.loadImage( mangaSlug: "concurrent-test", chapterNumber: 1, pageIndex: i ) XCTAssertNotNil(image, "Image at index \(i) should exist") } } // MARK: - Large Scale Tests func testLargeScaleFavoriteOperations() { // Probar con muchos favoritos let count = 1000 measure { for i in 0.. UIImage { let renderer = UIGraphicsImageRenderer(size: size) return renderer.image { context in color.setFill() context.fill(CGRect(origin: .zero, size: size)) } } }