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:
2026-02-04 15:34:18 +01:00
commit b474182dd9
6394 changed files with 1063909 additions and 0 deletions

View File

@@ -0,0 +1,415 @@
import XCTest
@testable import MangaReader
/// Ejemplos de cómo escribir tests para el proyecto MangaReader
/// Este archivo serve como guía de referencia para crear nuevos tests
// MARK: - Ejemplo 1: Test Unitario Básico
final class ExampleTests: XCTestCase {
// MARK: - Ejemplo: Test simple de modelo
func testEjemploModeloSimple() {
// Arrange: Preparar los datos
let manga = Manga(
slug: "test",
title: "Test Manga",
description: "Desc",
genres: ["Action"],
status: "PUBLICANDOSE",
url: "https://example.com"
)
// Act: Ejecutar la acción (si es necesario)
let displayTitle = manga.title
// Assert: Verificar el resultado
XCTAssertEqual(displayTitle, "Test Manga")
XCTAssertEqual(manga.slug, "test")
XCTAssertTrue(manga.genres.contains("Action"))
}
// MARK: - Ejemplo: Test con async/await
func testEjemploAsync() async throws {
// Arrange
let storageService = StorageService.shared
let image = createTestImage()
// Act
let url = try await storageService.saveImage(
image,
mangaSlug: "test",
chapterNumber: 1,
pageIndex: 0
)
// Assert
XCTAssertTrue(FileManager.default.fileExists(atPath: url.path))
}
// MARK: - Ejemplo: Test de error
func testEjemploManejoDeError() async {
// Arrange
let storageService = StorageService.shared
// Act & Assert
do {
_ = try await storageService.saveImage(
UIImage(), // Imagen vacía
mangaSlug: "test",
chapterNumber: 1,
pageIndex: 0
)
XCTFail("Debería haber lanzado un error")
} catch {
XCTAssertNotNil(error)
}
}
// MARK: - Ejemplo: Test con helpers
func testEjemploConHelpers() {
// Usar TestDataFactory para crear datos de prueba
let manga = TestDataFactory.createManga(
slug: "mi-manga",
title: "Mi Manga",
genres: ["Action", "Fantasy"]
)
// Usar AssertionHelpers
AssertionHelpers.assertValidManga(manga)
XCTAssertTrue(manga.genres.count == 2)
}
// MARK: - Ejemplo: Test de múltiples escenarios
func testEjemploMultiplesEscenarios() {
// Escenario 1: Manga publicado
let publishedManga = TestDataFactory.createManga(
status: "PUBLICANDOSE"
)
XCTAssertEqual(publishedManga.displayStatus, "En publicación")
// Escenario 2: Manga finalizado
let finishedManga = TestDataFactory.createManga(
status: "FINALIZADO"
)
XCTAssertEqual(finishedManga.displayStatus, "Finalizado")
// Escenario 3: Manga en pausa
let pausedManga = TestDataFactory.createManga(
status: "EN_PAUSA"
)
XCTAssertEqual(pausedManga.displayStatus, "En pausa")
}
// MARK: - Ejemplo: Test de performance
func testEjemploPerformance() {
let manga = TestDataFactory.createManga()
// Medir cuánto tarda en codificar 1000 veces
measure {
for _ in 0..<1000 {
_ = try? JSONEncoder().encode(manga)
}
}
}
// MARK: - Ejemplo: Test con setup/teardown
var storageService: StorageService!
override func setUp() async throws {
try await super.setUp()
// Se ejecuta antes de cada test
storageService = StorageService.shared
StorageTestHelpers.clearAllStorage()
}
override func tearDown() async throws {
// Se ejecuta después de cada test
StorageTestHelpers.clearAllStorage()
storageService = nil
try await super.tearDown()
}
func testEjemploConSetup() {
// El storageService ya está inicializado y limpio
storageService.saveFavorite(mangaSlug: "test")
XCTAssertTrue(storageService.isFavorite(mangaSlug: "test"))
}
// MARK: - Ejemplo: Test de integración
func testEjemploIntegracion() async throws {
// Simular flujo completo del usuario
// 1. Usuario busca un manga
let manga = TestDataFactory.createManga(slug: "tower-of-god")
// 2. Usuario lo agrega a favoritos
storageService.saveFavorite(mangaSlug: manga.slug)
XCTAssertTrue(storageService.isFavorite(mangaSlug: manga.slug))
// 3. Usuario comienza a leer
let progress = ReadingProgress(
mangaSlug: manga.slug,
chapterNumber: 1,
pageNumber: 0,
timestamp: Date()
)
storageService.saveReadingProgress(progress)
// 4. Verificar que todo se guardó correctamente
let retrieved = storageService.getReadingProgress(
mangaSlug: manga.slug,
chapterNumber: 1
)
XCTAssertNotNil(retrieved)
XCTAssertEqual(retrieved?.pageNumber, 0)
}
// MARK: - Ejemplo: Test concurrente
func testEjemploConcurrencia() async throws {
// Crear múltiples tareas concurrentes
await withTaskGroup(of: Void.self) { group in
for i in 0..<10 {
group.addTask {
let manga = TestDataFactory.createManga(slug: "manga-\(i)")
self.storageService.saveFavorite(mangaSlug: manga.slug)
}
}
}
// Verificar que todas se guardaron
let favorites = storageService.getFavorites()
XCTAssertEqual(favorites.count, 10)
}
// MARK: - Ejemplo: Test con mock
func testEjemploConMock() {
// Mock de respuesta de JavaScript
let mockResponse: [[String: Any]] = [
["number": 1, "title": "Chapter 1", "url": "url1", "slug": "slug1"],
["number": 2, "title": "Chapter 2", "url": "url2", "slug": "slug2"]
]
// Parsear respuesta mock
let chapters = mockResponse.compactMap { dict -> Chapter? in
guard let number = dict["number"] as? Int,
let title = dict["title"] as? String,
let url = dict["url"] as? String,
let slug = dict["slug"] as? String else {
return nil
}
return Chapter(number: number, title: title, url: url, slug: slug)
}
// Verificar parsing
XCTAssertEqual(chapters.count, 2)
XCTAssertEqual(chapters[0].number, 1)
XCTAssertEqual(chapters[1].number, 2)
}
// MARK: - Ejemplo: Test de edge cases
func testEjemploEdgeCases() {
// Caso 1: String vacío
let emptyManga = TestDataFactory.createManga(title: "")
XCTAssertEqual(emptyManga.title, "")
// Caso 2: Array vacío
let noGenresManga = TestDataFactory.createManga(genres: [])
XCTAssertTrue(noGenresManga.genres.isEmpty)
// Caso 3: Valor negativo
let chapter = TestDataFactory.createChapter(number: -1)
XCTAssertEqual(chapter.number, -1)
// Caso 4: Caracteres especiales
let specialSlug = "manga-áéíóú-ñ-@#$"
storageService.saveFavorite(mangaSlug: specialSlug)
XCTAssertTrue(storageService.isFavorite(mangaSlug: specialSlug))
}
// MARK: - Ejemplo: Test con assertions personalizadas
func testEjemploAssertionsPersonalizadas() {
let manga1 = TestDataFactory.createManga(slug: "test")
let manga2 = TestDataFactory.createManga(slug: "test")
// Usar assert personalizado
XCTAssertEqual(manga1, manga2)
XCTAssertEqual(manga1.hashValue, manga2.hashValue)
// Verificar URL válida
AssertionHelpers.assertValidURL(manga1.url)
// Verificar manga válido
AssertionHelpers.assertValidManga(manga1)
}
// MARK: - Helpers para ejemplos
private func createTestImage() -> UIImage {
let size = CGSize(width: 800, height: 1200)
UIGraphicsBeginImageContext(size)
let context = UIGraphicsGetCurrentContext()
context?.setFillColor(UIColor.blue.cgColor)
context?.fill(CGRect(origin: .zero, size: size))
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image!
}
}
// MARK: - Plantillas de Tests
/// Plantilla para test unitario simple
/*
func test[NombreFuncionalidad]_[Condición]_[ResultadoEsperado]() {
// Arrange
let [input] = [valor]
// Act
let result = [función](input)
// Assert
XCTAssertEqual(result, [esperado])
}
*/
/// Plantilla para test asíncrono
/*
func test[NombreFuncionalidad]_Async() async throws {
// Arrange
let [input] = [valor]
// Act
let result = try await [funciónAsync](input)
// Assert
XCTAssertNotNil(result)
}
*/
/// Plantilla para test de error
/*
func test[NombreFuncionalidad]_ThrowsError() {
// Arrange
let [inputInvalido] = [valor]
// Act & Assert
XCTAssertThrowsError(
try [función](inputInvalido)
) { error in
XCTAssertEqual(error as! [TipoError], [errorEsperado])
}
}
*/
/// Plantilla para test de performance
/*
func test[NombreFuncionalidad]_Performance() {
let [data] = [crearDatosDePrueba]
measure {
_ = [función](data)
}
}
*/
/// Plantilla para test de integración
/*
func test[FlujoCompleto]_Integration() async throws {
// Paso 1: [acción inicial]
let [result1] = try await [función1]()
// Paso 2: [acción siguiente]
let [result2] = [función2](result1)
// Paso 3: [verificación final]
XCTAssertNotNil([resultFinal])
XCTAssertTrue([condición])
}
*/
// MARK: - Consejos para Escribir Tests
/*
BUENOS HÁBITOS:
1. Usa nombres descriptivos:
- testSaveFavorite_AddsNewFavorite_WhenNotExists
- testChapterProgress_ReturnsDouble_WhenSet
2. Un assert por test:
- Separa en múltiples tests si hay varios asserts
- Usa subtests si están relacionados
3. Arrange-Act-Assert:
- Arrange: Prepara los datos
- Act: Ejecuta la acción
- Assert: Verifica el resultado
4. Tests independientes:
- No dependen del orden de ejecución
- Limpian después de sí mismos
5. Usa helpers:
- TestDataFactory para crear objetos
- AssertionHelpers para verificar condiciones
MALOS HÁBITOS:
1. Tests con múltiples asserts no relacionados
2. Tests que dependen del orden
3. Tests que no limpian después
4. Tests con nombres ambiguos
5. Tests que llaman a APIs reales
*/
// MARK: - Referencias Rápidas
/*
COMUNES ASSERTIONS:
- XCTAssertEqual(a, b) - Verifica igualdad
- XCTAssertNotEqual(a, b) - Verifica desigualdad
- XCTAssertTrue(condición) - Verifica true
- XCTAssertFalse(condición) - Verifica false
- XCTAssertNil(valor) - Verifica nil
- XCTAssertNotNil(valor) - Verifica no nil
- XCTAssertThrowsError(expr) - Verifica que lanza error
- XCTAssertNoThrow(expr) - Verifica que NO lanza error
ASYNC/AWAIT:
- try await [función async] - Ejecutar función async
- try await Task.sleep(...) - Esperar
- await withTaskGroup {} - Tareas concurrentes
SETUP/TEARDOWN:
- override func setUp() - Antes de cada test
- override func tearDown() - Después de cada test
- override func setUpWithError() - Con errores
- override func tearDownWithError() - Con errores
PERFORMANCE:
- measure { } - Medir bloques de código
- measure(metrics: ...) { } - Métricas específicas
MOCKS:
- TestDataFactory - Crear objetos de prueba
- ImageTestHelpers - Crear imágenes
- FileSystemTestHelpers - Operaciones de archivos
*/