✨ Features: - App iOS completa para leer manga sin publicidad - Scraper con WKWebView para manhwaweb.com - Sistema de descargas offline - Lector con zoom y navegación - Favoritos y progreso de lectura - Compatible con iOS 15+ y Sideloadly/3uTools 📦 Contenido: - Backend Node.js con Puppeteer (opcional) - App iOS con SwiftUI - Scraper de capítulos e imágenes - Sistema de almacenamiento local - Testing completo - Documentación exhaustiva 🧪 Prueba: Capítulo 789 de One Piece descargado exitosamente - 21 páginas descargadas - 4.68 MB total - URLs verificadas y funcionales 🎉 Generated with Claude Code (https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
610 lines
20 KiB
Swift
610 lines
20 KiB
Swift
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..<count {
|
|
storageService.saveFavorite(mangaSlug: "manga-\(i)")
|
|
}
|
|
}
|
|
|
|
let favorites = storageService.getFavorites()
|
|
XCTAssertEqual(favorites.count, count)
|
|
}
|
|
|
|
func testLargeScaleProgressOperations() {
|
|
// Probar con mucho progreso de lectura
|
|
let count = 500
|
|
|
|
measure {
|
|
for i in 0..<count {
|
|
let progress = ReadingProgress(
|
|
mangaSlug: "manga-\(i % 50)",
|
|
chapterNumber: i,
|
|
pageNumber: i,
|
|
timestamp: Date()
|
|
)
|
|
storageService.saveReadingProgress(progress)
|
|
}
|
|
}
|
|
|
|
let allProgress = storageService.getAllReadingProgress()
|
|
XCTAssertEqual(allProgress.count, count)
|
|
}
|
|
|
|
// MARK: - Helper Methods
|
|
|
|
private func createTestImage(color: UIColor, size: CGSize) -> UIImage {
|
|
let renderer = UIGraphicsImageRenderer(size: size)
|
|
return renderer.image { context in
|
|
color.setFill()
|
|
context.fill(CGRect(origin: .zero, size: size))
|
|
}
|
|
}
|
|
}
|