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.. 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..( 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 { """ Chapter 150 Chapter 149 Chapter 148 """ } /// Simula una respuesta HTML para imágenes de capítulo static func mockChapterImagesHTML() -> String { """ Page 1 Page 2 Page 3 Avatar Logo """ } /// Simula una respuesta HTML para información de manga static func mockMangaInfoHTML(title: String) -> String { """ \(title) - ManhwaWeb

\(title)

This is a long description of the manga that contains more than 100 characters to meet the minimum requirement for extraction.

Action Fantasy
Cover

Estado: PUBLICANDOSE

""" } /// 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( _ 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( _ 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..