Files
MangaReader/ios-app/Tests/TestHelpers.swift
renato97 b474182dd9 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>
2026-02-04 15:34:18 +01:00

577 lines
17 KiB
Swift

import XCTest
import UIKit
@testable import MangaReader
/// Helpers y mocks para los tests
/// Proporciona métodos de utilidad para crear datos de prueba y configurar tests
// MARK: - Test Data Factory
class TestDataFactory {
/// Crea un manga de prueba
static func createManga(
slug: String = "test-manga",
title: String = "Test Manga",
description: String = "A test manga description",
genres: [String] = ["Action", "Adventure"],
status: String = "PUBLICANDOSE",
url: String = "https://manhwaweb.com/manga/test",
coverImage: String? = nil
) -> Manga {
return Manga(
slug: slug,
title: title,
description: description,
genres: genres,
status: status,
url: url,
coverImage: coverImage
)
}
/// Crea un capítulo de prueba
static func createChapter(
number: Int = 1,
title: String = "Chapter 1",
url: String = "https://manhwaweb.com/leer/test/1",
slug: String = "test/1",
isRead: Bool = false,
isDownloaded: Bool = false,
lastReadPage: Int = 0
) -> Chapter {
var chapter = Chapter(number: number, title: title, url: url, slug: slug)
chapter.isRead = isRead
chapter.isDownloaded = isDownloaded
chapter.lastReadPage = lastReadPage
return chapter
}
/// Crea una página de manga de prueba
static func createMangaPage(
url: String = "https://example.com/page.jpg",
index: Int = 0,
isCached: Bool = false
) -> MangaPage {
return MangaPage(url: url, index: index, isCached: isCached)
}
/// Crea un progreso de lectura de prueba
static func createReadingProgress(
mangaSlug: String = "test-manga",
chapterNumber: Int = 1,
pageNumber: Int = 0,
timestamp: Date = Date()
) -> ReadingProgress {
return ReadingProgress(
mangaSlug: mangaSlug,
chapterNumber: chapterNumber,
pageNumber: pageNumber,
timestamp: timestamp
)
}
/// Crea un capítulo descargado de prueba
static func createDownloadedChapter(
mangaSlug: String = "test-manga",
mangaTitle: String = "Test Manga",
chapterNumber: Int = 1,
pages: [MangaPage] = [],
downloadedAt: Date = Date(),
totalSize: Int64 = 0
) -> DownloadedChapter {
return DownloadedChapter(
mangaSlug: mangaSlug,
mangaTitle: mangaTitle,
chapterNumber: chapterNumber,
pages: pages,
downloadedAt: downloadedAt,
totalSize: totalSize
)
}
/// Crea múltiples capítulos de prueba
static func createChapters(count: Int, startingFrom: Int = 1) -> [Chapter] {
return (startingFrom..<(startingFrom + count)).map { i in
createChapter(
number: i,
title: "Chapter \(i)",
url: "https://manhwaweb.com/leer/test/\(i)",
slug: "test/\(i)"
)
}
}
/// Crea múltiples páginas de prueba
static func createPages(count: Int) -> [MangaPage] {
return (0..<count).map { i in
createMangaPage(
url: "https://example.com/page\(i).jpg",
index: i
)
}
}
}
// MARK: - Image Test Helpers
class ImageTestHelpers {
/// Crea una imagen de prueba con un color específico
static func createTestImage(
color: UIColor = .blue,
size: CGSize = CGSize(width: 800, height: 1200)
) -> UIImage {
let renderer = UIGraphicsImageRenderer(size: size)
return renderer.image { context in
color.setFill()
context.fill(CGRect(origin: .zero, size: size))
}
}
/// Crea una imagen de prueba con texto
static func createTestImageWithText(
text: String,
size: CGSize = CGSize(width: 800, height: 1200)
) -> UIImage {
let renderer = UIGraphicsImageRenderer(size: size)
return renderer.image { context in
UIColor.white.setFill()
context.fill(CGRect(origin: .zero, size: size))
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
let attrs: [NSAttributedString.Key: Any] = [
.font: UIFont.systemFont(ofSize: 48),
.foregroundColor: UIColor.black,
.paragraphStyle: paragraphStyle
]
let string = NSAttributedString(string: text, attributes: attrs)
let rect = CGRect(origin: .zero, size: size)
string.draw(in: rect)
}
}
/// Compara dos imágenes para verificar si son iguales
static func compareImages(_ image1: UIImage?, _ image2: UIImage?) -> Bool {
guard let img1 = image1, let img2 = image2 else {
return image1 == nil && image2 == nil
}
return img1.pngData() == img2.pngData()
}
/// Verifica que una imagen no esté vacía
static func isImageNotEmpty(_ image: UIImage?) -> Bool {
guard let image = image else { return false }
return image.size.width > 0 && image.size.height > 0
}
}
// MARK: - File System Test Helpers
class FileSystemTestHelpers {
/// Crea un directorio temporal para tests
static func createTemporaryDirectory() throws -> URL {
let tempDir = FileManager.default.temporaryDirectory
.appendingPathComponent("test_\(UUID().uuidString)")
try FileManager.default.createDirectory(
at: tempDir,
withIntermediateDirectories: true
)
return tempDir
}
/// Elimina un directorio temporal
static func removeTemporaryDirectory(at url: URL) throws {
try FileManager.default.removeItem(at: url)
}
/// Crea un archivo de prueba
static func createTestFile(at url: URL, content: Data) throws {
try content.write(to: url)
}
/// Verifica que un archivo existe
static func fileExists(at url: URL) -> Bool {
FileManager.default.fileExists(atPath: url.path)
}
/// Obtiene el tamaño de un archivo
static func fileSize(at url: URL) -> Int64? {
let attributes = try? FileManager.default.attributesOfItem(atPath: url.path)
return attributes?[.size] as? Int64
}
/// Crea una estructura de directorios de prueba para capítulos
static func createTestChapterStructure(
mangaSlug: String,
chapterNumber: Int,
pageCount: Int,
in directory: URL
) throws {
let chapterDir = directory
.appendingPathComponent(mangaSlug)
.appendingPathComponent("Chapter\(chapterNumber)")
try FileManager.default.createDirectory(
at: chapterDir,
withIntermediateDirectories: true
)
// Crear archivos de prueba
for i in 0..<pageCount {
let imageData = Data(repeating: UInt8(i), count: 1024)
let fileURL = chapterDir.appendingPathComponent("page_\(i).jpg")
try imageData.write(to: fileURL)
}
}
}
// MARK: - Storage Test Helpers
class StorageTestHelpers {
/// Limpia todos los datos de almacenamiento
static func clearAllStorage() {
UserDefaults.standard.removeObject(forKey: "favoriteMangas")
UserDefaults.standard.removeObject(forKey: "readingProgress")
UserDefaults.standard.removeObject(forKey: "downloadedChaptersMetadata")
let storageService = StorageService.shared
storageService.clearAllDownloads()
}
/// Crea datos de prueba en el almacenamiento
static func seedTestData(
favoriteCount: Int = 5,
progressCount: Int = 10,
downloadedChapterCount: Int = 3
) {
let storageService = StorageService.shared
// Agregar favoritos
for i in 0..<favoriteCount {
storageService.saveFavorite(mangaSlug: "manga-\(i)")
}
// Agregar progreso
for i in 0..<progressCount {
let progress = TestDataFactory.createReadingProgress(
mangaSlug: "manga-\(i % 3)",
chapterNumber: i,
pageNumber: i * 5
)
storageService.saveReadingProgress(progress)
}
// Agregar capítulos descargados
for i in 0..<downloadedChapterCount {
let chapter = TestDataFactory.createDownloadedChapter(
mangaSlug: "manga-\(i % 2)",
mangaTitle: "Manga \(i % 2)",
chapterNumber: i,
pages: TestDataFactory.createPages(count: 5)
)
storageService.saveDownloadedChapter(chapter)
}
}
/// Verifica que el almacenamiento está vacío
static func assertStorageIsEmpty() {
let storageService = StorageService.shared
XCTAssertTrue(
storageService.getFavorites().isEmpty,
"Favorites should be empty"
)
XCTAssertTrue(
storageService.getAllReadingProgress().isEmpty,
"Reading progress should be empty"
)
XCTAssertTrue(
storageService.getDownloadedChapters().isEmpty,
"Downloaded chapters should be empty"
)
XCTAssertEqual(
storageService.getStorageSize(),
0,
"Storage size should be 0"
)
}
}
// MARK: - Async Test Helpers
class AsyncTestHelpers {
/// Ejecuta una operación asíncrona con timeout
static func executeWithTimeout<T>(
timeout: TimeInterval = 5.0,
operation: @escaping () async throws -> T
) async throws -> T {
try await withThrowingTaskGroup(of: T.self) { group in
group.addTask {
return try await operation()
}
group.addTask {
try await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000))
throw TimeoutError()
}
let result = try await group.next()
group.cancelAll()
guard let result = result else {
throw TimeoutError()
}
return result
}
}
struct TimeoutError: Error {
let localizedDescription = "Operation timed out"
}
}
// MARK: - Scraper Test Helpers
class ScraperTestHelpers {
/// Simula una respuesta HTML para lista de capítulos
static func mockChapterListHTML(mangaSlug: String) -> String {
"""
<!DOCTYPE html>
<html>
<body>
<a href="/leer/\(mangaSlug)/150">Chapter 150</a>
<a href="/leer/\(mangaSlug)/149">Chapter 149</a>
<a href="/leer/\(mangaSlug)/148">Chapter 148</a>
</body>
</html>
"""
}
/// Simula una respuesta HTML para imágenes de capítulo
static func mockChapterImagesHTML() -> String {
"""
<!DOCTYPE html>
<html>
<body>
<img src="https://example.com/page1.jpg" alt="Page 1">
<img src="https://example.com/page2.jpg" alt="Page 2">
<img src="https://example.com/page3.jpg" alt="Page 3">
<img src="https://example.com/avatar.jpg" alt="Avatar">
<img src="https://example.com/logo.png" alt="Logo">
</body>
</html>
"""
}
/// Simula una respuesta HTML para información de manga
static func mockMangaInfoHTML(title: String) -> String {
"""
<!DOCTYPE html>
<html>
<head>
<title>\(title) - ManhwaWeb</title>
</head>
<body>
<h1>\(title)</h1>
<p>This is a long description of the manga that contains more than 100 characters to meet the minimum requirement for extraction.</p>
<a href="/genero/action">Action</a>
<a href="/genero/fantasy">Fantasy</a>
<div class="cover">
<img src="https://example.com/cover.jpg" alt="Cover">
</div>
<p>Estado: PUBLICANDOSE</p>
</body>
</html>
"""
}
/// Simula un resultado de JavaScript para capítulos
static func mockChapterJSResult() -> [[String: Any]] {
return [
[
"number": 150,
"title": "Chapter 150",
"url": "https://manhwaweb.com/leer/test/150",
"slug": "test/150"
],
[
"number": 149,
"title": "Chapter 149",
"url": "https://manhwaweb.com/leer/test/149",
"slug": "test/149"
]
]
}
/// Simula un resultado de JavaScript para imágenes
static func mockImagesJSResult() -> [String] {
return [
"https://example.com/page1.jpg",
"https://example.com/page2.jpg",
"https://example.com/page3.jpg"
]
}
/// Simula un resultado de JavaScript para info de manga
static func mockMangaInfoJSResult() -> [String: Any] {
return [
"title": "Test Manga",
"description": "A test manga description",
"genres": ["Action", "Fantasy"],
"status": "PUBLICANDOSE",
"coverImage": "https://example.com/cover.jpg"
]
}
}
// MARK: - Assertion Helpers
class AssertionHelpers {
/// Afirma que dos arrays son iguales independientemente del orden
static func assertArraysEqual<T: Equatable>(
_ array1: [T],
_ array2: [T],
file: StaticString = #file,
line: UInt = #line
) {
XCTAssertEqual(
array1.sorted(),
array2.sorted(),
"Arrays are not equal (order independent)",
file: file,
line: line
)
}
/// Afirma que un array contiene elementos específicos
static func assertArrayContains<T: Equatable>(
_ array: [T],
_ elements: [T],
file: StaticString = #file,
line: UInt = #line
) {
for element in elements {
XCTAssertTrue(
array.contains(element),
"Array does not contain expected element: \(element)",
file: file,
line: line
)
}
}
/// Afirma que una URL es válida
static func assertValidURL(
_ urlString: String,
file: StaticString = #file,
line: UInt = #line
) {
XCTAssertNotNil(
URL(string: urlString),
"URL string is not valid: \(urlString)",
file: file,
line: line
)
}
/// Afirma que un manga tiene datos válidos
static func assertValidManga(
_ manga: Manga,
file: StaticString = #file,
line: UInt = #line
) {
XCTAssertFalse(
manga.slug.isEmpty,
"Manga slug should not be empty",
file: file,
line: line
)
XCTAssertFalse(
manga.title.isEmpty,
"Manga title should not be empty",
file: file,
line: line
)
XCTAssertFalse(
manga.url.isEmpty,
"Manga URL should not be empty",
file: file,
line: line
)
AssertionHelpers.assertValidURL(manga.url, file: file, line: line)
}
/// Afirma que un capítulo tiene datos válidos
static func assertValidChapter(
_ chapter: Chapter,
file: StaticString = #file,
line: UInt = #line
) {
XCTAssertGreaterThan(
chapter.number,
0,
"Chapter number should be greater than 0",
file: file,
line: line
)
XCTAssertFalse(
chapter.title.isEmpty,
"Chapter title should not be empty",
file: file,
line: line
)
XCTAssertFalse(
chapter.url.isEmpty,
"Chapter URL should not be empty",
file: file,
line: line
)
}
}
// MARK: - Performance Test Helpers
class PerformanceTestHelpers {
/// Mide el tiempo de ejecución de una operación
static func measureTime(_ operation: () -> Void) -> TimeInterval {
let start = Date()
operation()
return Date().timeIntervalSince(start)
}
/// Mide el tiempo de ejecución de una operación asíncrona
static func measureAsyncTime(_ operation: () async throws -> Void) async throws -> TimeInterval {
let start = Date()
try await operation()
return Date().timeIntervalSince(start)
}
/// Ejecuta una operación múltiples veces y retorna el tiempo promedio
static func averageTime(
iterations: Int = 10,
operation: () -> Void
) -> TimeInterval {
var totalTime: TimeInterval = 0
for _ in 0..<iterations {
totalTime += measureTime(operation)
}
return totalTime / Double(iterations)
}
}