Initial commit: MangaReader iOS App
✨ 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>
This commit is contained in:
528
ios-app/Sources/Tests/DownloadManagerTests.swift
Normal file
528
ios-app/Sources/Tests/DownloadManagerTests.swift
Normal file
@@ -0,0 +1,528 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user