✨ 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>
529 lines
15 KiB
Swift
529 lines
15 KiB
Swift
import XCTest
|
|
@testable import MangaReader
|
|
|
|
/// Tests unitarios para DownloadManager
|
|
///
|
|
/// Estos tests deben agregarse a tu target de tests en Xcode
|
|
@MainActor
|
|
class DownloadManagerTests: XCTestCase {
|
|
|
|
var downloadManager: DownloadManager!
|
|
var storage: StorageService!
|
|
|
|
override func setUp() async throws {
|
|
downloadManager = DownloadManager.shared
|
|
storage = StorageService.shared
|
|
|
|
// Limpiar estado antes de cada test
|
|
downloadManager.cancelAllDownloads()
|
|
downloadManager.clearCompletedHistory()
|
|
downloadManager.clearFailedHistory()
|
|
}
|
|
|
|
override func tearDown() async throws {
|
|
// Limpiar estado después de cada test
|
|
downloadManager.cancelAllDownloads()
|
|
}
|
|
|
|
// MARK: - Test: Descarga Individual
|
|
|
|
func testDownloadSingleChapter() async throws {
|
|
// Given
|
|
let mangaSlug = "test-manga"
|
|
let mangaTitle = "Test Manga"
|
|
let chapter = Chapter(
|
|
number: 1,
|
|
title: "Chapter 1",
|
|
url: "https://example.com/chapter1",
|
|
slug: "chapter-1"
|
|
)
|
|
|
|
// When
|
|
try await downloadManager.downloadChapter(
|
|
mangaSlug: mangaSlug,
|
|
mangaTitle: mangaTitle,
|
|
chapter: chapter
|
|
)
|
|
|
|
// Then
|
|
XCTAssertEqual(downloadManager.activeDownloads.count, 0, "No debe haber descargas activas")
|
|
XCTAssertEqual(downloadManager.completedDownloads.count, 1, "Debe haber una descarga completada")
|
|
XCTAssertTrue(storage.isChapterDownloaded(mangaSlug: mangaSlug, chapterNumber: 1), "El capítulo debe estar descargado")
|
|
}
|
|
|
|
func testDownloadAlreadyDownloadedChapter() async throws {
|
|
// Given
|
|
let mangaSlug = "test-manga"
|
|
let mangaTitle = "Test Manga"
|
|
let chapter = Chapter(
|
|
number: 1,
|
|
title: "Chapter 1",
|
|
url: "https://example.com/chapter1",
|
|
slug: "chapter-1"
|
|
)
|
|
|
|
// Descargar por primera vez
|
|
try await downloadManager.downloadChapter(
|
|
mangaSlug: mangaSlug,
|
|
mangaTitle: mangaTitle,
|
|
chapter: chapter
|
|
)
|
|
|
|
// When & Then - Intentar descargar nuevamente
|
|
do {
|
|
try await downloadManager.downloadChapter(
|
|
mangaSlug: mangaSlug,
|
|
mangaTitle: mangaTitle,
|
|
chapter: chapter
|
|
)
|
|
XCTFail("Debe lanzar error alreadyDownloaded")
|
|
} catch DownloadError.alreadyDownloaded {
|
|
// Éxito - Error esperado
|
|
} catch {
|
|
XCTFail("Error incorrecto: \(error)")
|
|
}
|
|
}
|
|
|
|
// MARK: - Test: Descarga Múltiple
|
|
|
|
func testDownloadMultipleChapters() async throws {
|
|
// Given
|
|
let mangaSlug = "test-manga"
|
|
let mangaTitle = "Test Manga"
|
|
let chapters = [
|
|
Chapter(number: 1, title: "Chapter 1", url: "https://example.com/ch1", slug: "ch1"),
|
|
Chapter(number: 2, title: "Chapter 2", url: "https://example.com/ch2", slug: "ch2"),
|
|
Chapter(number: 3, title: "Chapter 3", url: "https://example.com/ch3", slug: "ch3")
|
|
]
|
|
|
|
// When
|
|
await downloadManager.downloadChapters(
|
|
mangaSlug: mangaSlug,
|
|
mangaTitle: mangaTitle,
|
|
chapters: chapters
|
|
)
|
|
|
|
// Esperar a que todas terminen
|
|
try await Task.sleep(nanoseconds: 10_000_000_000) // 10 segundos
|
|
|
|
// Then
|
|
XCTAssertEqual(downloadManager.completedDownloads.count, 3, "Debe haber 3 descargas completadas")
|
|
}
|
|
|
|
// MARK: - Test: Cancelación
|
|
|
|
func testCancelDownload() async throws {
|
|
// Given
|
|
let mangaSlug = "test-manga"
|
|
let mangaTitle = "Test Manga"
|
|
let chapter = Chapter(
|
|
number: 1,
|
|
title: "Chapter 1",
|
|
url: "https://example.com/chapter1",
|
|
slug: "chapter-1"
|
|
)
|
|
|
|
// Iniciar descarga en background
|
|
Task {
|
|
try? await downloadManager.downloadChapter(
|
|
mangaSlug: mangaSlug,
|
|
mangaTitle: mangaTitle,
|
|
chapter: chapter
|
|
)
|
|
}
|
|
|
|
// Esperar un poco para que inicie
|
|
try await Task.sleep(nanoseconds: 500_000_000) // 0.5 segundos
|
|
|
|
// When
|
|
guard let task = downloadManager.activeDownloads.first else {
|
|
XCTFail("Debe haber una descarga activa")
|
|
return
|
|
}
|
|
downloadManager.cancelDownload(taskId: task.id)
|
|
|
|
// Then
|
|
XCTAssertEqual(downloadManager.activeDownloads.count, 0, "No debe haber descargas activas")
|
|
XCTAssertFalse(storage.isChapterDownloaded(mangaSlug: mangaSlug, chapterNumber: 1), "El capítulo no debe estar descargado")
|
|
}
|
|
|
|
func testCancelAllDownloads() async throws {
|
|
// Given
|
|
let mangaSlug = "test-manga"
|
|
let mangaTitle = "Test Manga"
|
|
let chapters = [
|
|
Chapter(number: 1, title: "Chapter 1", url: "https://example.com/ch1", slug: "ch1"),
|
|
Chapter(number: 2, title: "Chapter 2", url: "https://example.com/ch2", slug: "ch2"),
|
|
Chapter(number: 3, title: "Chapter 3", url: "https://example.com/ch3", slug: "ch3")
|
|
]
|
|
|
|
// Iniciar descargas
|
|
Task {
|
|
await downloadManager.downloadChapters(
|
|
mangaSlug: mangaSlug,
|
|
mangaTitle: mangaTitle,
|
|
chapters: chapters
|
|
)
|
|
}
|
|
|
|
// Esperar un poco
|
|
try await Task.sleep(nanoseconds: 500_000_000) // 0.5 segundos
|
|
|
|
// When
|
|
downloadManager.cancelAllDownloads()
|
|
|
|
// Then
|
|
XCTAssertEqual(downloadManager.activeDownloads.count, 0, "No debe haber descargas activas")
|
|
}
|
|
|
|
// MARK: - Test: Progreso
|
|
|
|
func testDownloadProgress() async throws {
|
|
// Given
|
|
let mangaSlug = "test-manga"
|
|
let mangaTitle = "Test Manga"
|
|
let chapter = Chapter(
|
|
number: 1,
|
|
title: "Chapter 1",
|
|
url: "https://example.com/chapter1",
|
|
slug: "chapter-1"
|
|
)
|
|
|
|
// Expectation para progreso
|
|
let progressExpectation = expectation(description: "Progreso actualizado")
|
|
|
|
// Observer de progreso
|
|
let cancellable = downloadManager.$activeDownloads.sink { tasks in
|
|
if let task = tasks.first {
|
|
if task.progress > 0 && task.progress < 1 {
|
|
progressExpectation.fulfill()
|
|
}
|
|
}
|
|
}
|
|
|
|
// When
|
|
Task {
|
|
try? await downloadManager.downloadChapter(
|
|
mangaSlug: mangaSlug,
|
|
mangaTitle: mangaTitle,
|
|
chapter: chapter
|
|
)
|
|
}
|
|
|
|
// Then
|
|
await fulfillment(of: [progressExpectation], timeout: 5.0)
|
|
cancellable.cancel()
|
|
}
|
|
|
|
// MARK: - Test: Errores
|
|
|
|
func testDownloadWithInvalidURL() async throws {
|
|
// Given
|
|
let mangaSlug = "test-manga"
|
|
let mangaTitle = "Test Manga"
|
|
let chapter = Chapter(
|
|
number: 1,
|
|
title: "Chapter 1",
|
|
url: "invalid-url",
|
|
slug: "invalid-chapter"
|
|
)
|
|
|
|
// When & Then
|
|
do {
|
|
try await downloadManager.downloadChapter(
|
|
mangaSlug: mangaSlug,
|
|
mangaTitle: mangaTitle,
|
|
chapter: chapter
|
|
)
|
|
XCTFail("Debe lanzar error")
|
|
} catch {
|
|
// Éxito - Se espera un error
|
|
XCTAssertNotNil(error, "Debe haber un error")
|
|
}
|
|
}
|
|
|
|
// MARK: - Test: Concurrencia
|
|
|
|
func testMaxConcurrentDownloads() async throws {
|
|
// Given
|
|
let mangaSlug = "test-manga"
|
|
let mangaTitle = "Test Manga"
|
|
let chapters = (1...10).map { i in
|
|
Chapter(number: i, title: "Chapter \(i)", url: "https://example.com/ch\(i)", slug: "ch\(i)")
|
|
}
|
|
|
|
// When
|
|
Task {
|
|
await downloadManager.downloadChapters(
|
|
mangaSlug: mangaSlug,
|
|
mangaTitle: mangaTitle,
|
|
chapters: chapters
|
|
)
|
|
}
|
|
|
|
// Esperar un poco
|
|
try await Task.sleep(nanoseconds: 1_000_000_000) // 1 segundo
|
|
|
|
// Then - No debe exceder el máximo de descargas concurrentes
|
|
XCTAssertLessThanOrEqual(
|
|
downloadManager.activeDownloads.count,
|
|
3,
|
|
"No debe haber más de 3 descargas activas simultáneas"
|
|
)
|
|
}
|
|
|
|
// MARK: - Test: Storage
|
|
|
|
func testStorageIntegration() async throws {
|
|
// Given
|
|
let mangaSlug = "test-manga"
|
|
let mangaTitle = "Test Manga"
|
|
let chapter = Chapter(
|
|
number: 1,
|
|
title: "Chapter 1",
|
|
url: "https://example.com/chapter1",
|
|
slug: "chapter-1"
|
|
)
|
|
|
|
// When
|
|
try await downloadManager.downloadChapter(
|
|
mangaSlug: mangaSlug,
|
|
mangaTitle: mangaTitle,
|
|
chapter: chapter
|
|
)
|
|
|
|
// Then - Verificar integración con StorageService
|
|
XCTAssertTrue(
|
|
storage.isChapterDownloaded(mangaSlug: mangaSlug, chapterNumber: 1),
|
|
"StorageService debe reportar el capítulo como descargado"
|
|
)
|
|
|
|
XCTAssertNotNil(
|
|
storage.getDownloadedChapter(mangaSlug: mangaSlug, chapterNumber: 1),
|
|
"StorageService debe retornar metadata del capítulo"
|
|
)
|
|
|
|
let chapterDir = storage.getChapterDirectory(mangaSlug: mangaSlug, chapterNumber: 1)
|
|
XCTAssertTrue(
|
|
FileManager.default.fileExists(atPath: chapterDir.path),
|
|
"El directorio del capítulo debe existir"
|
|
)
|
|
}
|
|
|
|
func testClearStorage() async throws {
|
|
// Given
|
|
let mangaSlug = "test-manga"
|
|
let mangaTitle = "Test Manga"
|
|
let chapter = Chapter(
|
|
number: 1,
|
|
title: "Chapter 1",
|
|
url: "https://example.com/chapter1",
|
|
slug: "chapter-1"
|
|
)
|
|
|
|
// Descargar capítulo
|
|
try await downloadManager.downloadChapter(
|
|
mangaSlug: mangaSlug,
|
|
mangaTitle: mangaTitle,
|
|
chapter: chapter
|
|
)
|
|
|
|
// When
|
|
storage.deleteDownloadedChapter(mangaSlug: mangaSlug, chapterNumber: 1)
|
|
|
|
// Then
|
|
XCTAssertFalse(
|
|
storage.isChapterDownloaded(mangaSlug: mangaSlug, chapterNumber: 1),
|
|
"El capítulo no debe estar descargado después de eliminarlo"
|
|
)
|
|
|
|
XCTAssertEqual(
|
|
storage.getStorageSize(),
|
|
0,
|
|
"El tamaño de almacenamiento debe ser 0"
|
|
)
|
|
}
|
|
|
|
// MARK: - Test: Estadísticas
|
|
|
|
func testDownloadStats() async throws {
|
|
// Given
|
|
let initialStats = downloadManager.downloadStats
|
|
|
|
// When
|
|
let mangaSlug = "test-manga"
|
|
let mangaTitle = "Test Manga"
|
|
let chapter = Chapter(
|
|
number: 1,
|
|
title: "Chapter 1",
|
|
url: "https://example.com/chapter1",
|
|
slug: "chapter-1"
|
|
)
|
|
|
|
try await downloadManager.downloadChapter(
|
|
mangaSlug: mangaSlug,
|
|
mangaTitle: mangaTitle,
|
|
chapter: chapter
|
|
)
|
|
|
|
let finalStats = downloadManager.downloadStats
|
|
|
|
// Then
|
|
XCTAssertEqual(
|
|
finalStats.completedDownloads,
|
|
initialStats.completedDownloads + 1,
|
|
"Las descargas completadas deben incrementar en 1"
|
|
)
|
|
}
|
|
|
|
// MARK: - Test: Historiales
|
|
|
|
func testClearCompletedHistory() async throws {
|
|
// Given
|
|
let mangaSlug = "test-manga"
|
|
let mangaTitle = "Test Manga"
|
|
let chapter = Chapter(
|
|
number: 1,
|
|
title: "Chapter 1",
|
|
url: "https://example.com/chapter1",
|
|
slug: "chapter-1"
|
|
)
|
|
|
|
try await downloadManager.downloadChapter(
|
|
mangaSlug: mangaSlug,
|
|
mangaTitle: mangaTitle,
|
|
chapter: chapter
|
|
)
|
|
|
|
// When
|
|
downloadManager.clearCompletedHistory()
|
|
|
|
// Then
|
|
XCTAssertEqual(
|
|
downloadManager.completedDownloads.count,
|
|
0,
|
|
"El historial de completadas debe estar vacío"
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Tests para DownloadTask
|
|
@MainActor
|
|
class DownloadTaskTests: XCTestCase {
|
|
|
|
func testDownloadTaskInitialization() {
|
|
// Given
|
|
let task = DownloadTask(
|
|
mangaSlug: "test-manga",
|
|
mangaTitle: "Test Manga",
|
|
chapterNumber: 1,
|
|
chapterTitle: "Chapter 1",
|
|
imageURLs: ["url1", "url2", "url3"]
|
|
)
|
|
|
|
// Then
|
|
XCTAssertEqual(task.mangaSlug, "test-manga")
|
|
XCTAssertEqual(task.chapterNumber, 1)
|
|
XCTAssertEqual(task.imageURLs.count, 3)
|
|
XCTAssertEqual(task.downloadedPages, 0)
|
|
XCTAssertEqual(task.progress, 0.0)
|
|
}
|
|
|
|
func testDownloadTaskProgress() {
|
|
// Given
|
|
let task = DownloadTask(
|
|
mangaSlug: "test-manga",
|
|
mangaTitle: "Test Manga",
|
|
chapterNumber: 1,
|
|
chapterTitle: "Chapter 1",
|
|
imageURLs: ["url1", "url2", "url3", "url4", "url5"]
|
|
)
|
|
|
|
// When
|
|
task.updateProgress(downloaded: 2, total: 5)
|
|
|
|
// Then
|
|
XCTAssertEqual(task.downloadedPages, 2)
|
|
XCTAssertEqual(task.progress, 0.4, accuracy: 0.01)
|
|
}
|
|
|
|
func testDownloadTaskCompletion() {
|
|
// Given
|
|
let task = DownloadTask(
|
|
mangaSlug: "test-manga",
|
|
mangaTitle: "Test Manga",
|
|
chapterNumber: 1,
|
|
chapterTitle: "Chapter 1",
|
|
imageURLs: ["url1", "url2", "url3"]
|
|
)
|
|
|
|
// When
|
|
task.complete()
|
|
|
|
// Then
|
|
XCTAssertTrue(task.state.isCompleted)
|
|
}
|
|
|
|
func testDownloadTaskCancellation() {
|
|
// Given
|
|
let task = DownloadTask(
|
|
mangaSlug: "test-manga",
|
|
mangaTitle: "Test Manga",
|
|
chapterNumber: 1,
|
|
chapterTitle: "Chapter 1",
|
|
imageURLs: ["url1", "url2", "url3"]
|
|
)
|
|
|
|
// When
|
|
task.cancel()
|
|
|
|
// Then
|
|
XCTAssertTrue(task.isCancelled)
|
|
XCTAssertTrue(task.state.isTerminal)
|
|
}
|
|
}
|
|
|
|
/// Tests para Extensions
|
|
class DownloadExtensionsTests: XCTestCase {
|
|
|
|
func testDownloadTaskFormattedSize() {
|
|
// Given
|
|
let task = DownloadTask(
|
|
mangaSlug: "test-manga",
|
|
mangaTitle: "Test Manga",
|
|
chapterNumber: 1,
|
|
chapterTitle: "Chapter 1",
|
|
imageURLs: Array(repeating: "url", count: 10)
|
|
)
|
|
|
|
// When
|
|
let size = task.formattedSize
|
|
|
|
// Then
|
|
XCTAssertFalse(size.isEmpty, "El tamaño formateado no debe estar vacío")
|
|
}
|
|
|
|
func testUIImageOptimization() {
|
|
// Given
|
|
let imageSize = CGSize(width: 3000, height: 4000)
|
|
UIGraphicsBeginImageContextWithOptions(imageSize, false, 1.0)
|
|
let context = UIGraphicsGetCurrentContext()
|
|
context?.setFillColor(UIColor.blue.cgColor)
|
|
context?.fill(CGRect(origin: .zero, size: imageSize))
|
|
let largeImage = UIGraphicsGetImageFromCurrentImageContext()
|
|
UIGraphicsEndImageContext()
|
|
|
|
guard let image = largeImage else {
|
|
XCTFail("No se pudo crear la imagen de prueba")
|
|
return
|
|
}
|
|
|
|
// When
|
|
let optimizedData = image.optimizedForStorage()
|
|
|
|
// Then
|
|
XCTAssertNotNil(optimizedData, "Debe generar datos optimizados")
|
|
XCTAssertTrue(optimizedData!.count > 0, "Los datos no deben estar vacíos")
|
|
}
|
|
}
|