import XCTest import WebKit @testable import MangaReader /// Tests para el ManhwaWebScraper /// Mock de WKWebView y pruebas de parsing de HTML/JavaScript @MainActor final class ManhwaWebScraperTests: XCTestCase { var scraper: ManhwaWebScraper! // MARK: - Setup & Teardown override func setUp() async throws { try await super.setUp() scraper = ManhwaWebScraper.shared } override func tearDown() async throws { scraper = nil try await super.tearDown() } // MARK: - Scraper Error Tests func testScrapingErrorDescriptions() { // Test error descriptions XCTAssertEqual( ScrapingError.webViewNotInitialized.errorDescription, "WebView no está inicializado" ) XCTAssertEqual( ScrapingError.pageLoadFailed.errorDescription, "Error al cargar la página" ) XCTAssertEqual( ScrapingError.noContentFound.errorDescription, "No se encontró contenido" ) XCTAssertEqual( ScrapingError.parsingError.errorDescription, "Error al procesar el contenido" ) } func testScrapingErrorLocalizedError() { let error = ScrapingError.pageLoadFailed XCTAssertNotNil(error.errorDescription) let nsError = error as NSError XCTAssertNotNil(nsError.localizedDescription) } // MARK: - Mock WKWebView Tests func testWebViewInitialization() { // Test que el scraper se inicializa correctamente XCTAssertNotNil(scraper) } // MARK: - Chapter Parsing Tests (Simulated) func testChapterParsingFromJavaScriptResult() { // Simular el resultado de JavaScript let jsResult: [[String: Any]] = [ [ "number": 150, "title": "The Final Battle", "url": "https://manhwaweb.com/leer/solo-leveling/150", "slug": "solo-leveling/150" ], [ "number": 149, "title": "The Beginning", "url": "https://manhwaweb.com/leer/solo-leveling/149", "slug": "solo-leveling/149" ], [ "number": 148, "title": "Preparation", "url": "https://manhwaweb.com/leer/solo-leveling/148", "slug": "solo-leveling/148" ] ] // Parsear capítulos let chapters = jsResult.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, 3) XCTAssertEqual(chapters[0].number, 150) XCTAssertEqual(chapters[0].title, "The Final Battle") XCTAssertEqual(chapters[1].number, 149) XCTAssertEqual(chapters[2].number, 148) } func testChapterParsingWithInvalidData() { // Simular resultado con datos inválidos let jsResult: [[String: Any]] = [ [ "number": "not-a-number", "title": "Invalid Chapter", "url": "https://manhwaweb.com/leer/test/1", "slug": "test/1" ], [ "number": 10, "title": "", "url": "https://manhwaweb.com/leer/test/10", "slug": "test/10" ], [ "number": 5, "title": "Valid", "url": "invalid-url", "slug": "test/5" ] ] // Parsear - debería fallar para todos los casos inválidos let chapters = jsResult.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 !title.isEmpty && !url.isEmpty ? Chapter(number: number, title: title, url: url, slug: slug) : nil } // Todos deberían ser nil por datos inválidos XCTAssertTrue(chapters.isEmpty) } func testChapterDeduplication() { // Simular duplicados en el resultado let jsResult: [[String: Any]] = [ ["number": 10, "title": "Chapter 10", "url": "url1", "slug": "slug1"], ["number": 10, "title": "Chapter 10 Duplicate", "url": "url2", "slug": "slug2"], ["number": 9, "title": "Chapter 9", "url": "url3", "slug": "slug3"], ["number": 9, "title": "Chapter 9 Duplicate", "url": "url4", "slug": "slug4"] ] // Aplicar lógica de deduplicación let unique = jsResult.filter { (chapter) in jsResult.firstIndex(where: { ($0["number"] as? Int) == (chapter["number"] as? Int) }) == jsResult.firstIndex(of: chapter) } XCTAssertEqual(unique.count, 2, "Should have only 2 unique chapters") } func testChapterSorting() { // Simular capítulos desordenados let chapters = [ Chapter(number: 5, title: "Ch 5", url: "", slug: ""), Chapter(number: 10, title: "Ch 10", url: "", slug: ""), Chapter(number: 1, title: "Ch 1", url: "", slug: ""), Chapter(number: 7, title: "Ch 7", url: "", slug: "") ] // Ordenar descendente (como hace el scraper) let sorted = chapters.sorted { $0.number > $1.number } XCTAssertEqual(sorted[0].number, 10) XCTAssertEqual(sorted[1].number, 7) XCTAssertEqual(sorted[2].number, 5) XCTAssertEqual(sorted[3].number, 1) } // MARK: - Image Parsing Tests (Simulated) func testImageParsingFromJavaScriptResult() { // Simular el resultado de JavaScript para imágenes let jsResult: [String] = [ "https://example.com/image1.jpg", "https://example.com/image2.jpg", "https://example.com/image3.jpg", "https://example.com/image1.jpg", // Duplicado "https://example.com/avatar.jpg", "https://example.com/logo.png", "https://example.com/image4.jpg" ] // Aplicar filtros del scraper let filteredImages = jsResult.filter { url in let lowercased = url.lowercased() // Filtrar UI elements let isUIElement = lowercased.contains("avatar") || lowercased.contains("icon") || lowercased.contains("logo") || lowercased.contains("button") return !isUIElement && lowercased.contains("http") } // Eliminar duplicados let uniqueImages = Array(Set(filteredImages)) XCTAssertEqual(uniqueImages.count, 4, "Should have 4 unique non-UI images") XCTAssertTrue(uniqueImages.contains("https://example.com/image1.jpg")) XCTAssertFalse(uniqueImages.contains("https://example.com/avatar.jpg")) XCTAssertFalse(uniqueImages.contains("https://example.com/logo.png")) } func testImageParsingWithEmptyArray() { let jsResult: [String] = [] // Aplicar parsing let images = jsResult.filter { $0.contains("http") } XCTAssertTrue(images.isEmpty) } func testImageParsingWithInvalidURLs() { let jsResult: [String] = [ "not-a-url", "", "ftp://invalid-protocol.com/image.jpg", "https://valid.com/image.jpg" ] let validImages = jsResult.filter { $0.hasPrefix("https://") } XCTAssertEqual(validImages.count, 1) XCTAssertEqual(validImages.first, "https://valid.com/image.jpg") } // MARK: - Manga Info Parsing Tests (Simulated) func testMangaInfoParsingFromJavaScriptResult() { // Simular el resultado de JavaScript let jsResult: [String: Any] = [ "title": "Solo Leveling", "description": "The weakest hunter becomes the strongest in a world where dungeons have appeared.", "genres": ["Action", "Adventure", "Fantasy"], "status": "PUBLICANDOSE", "coverImage": "https://example.com/cover.jpg" ] // Parsear información del manga let title = jsResult["title"] as? String ?? "Unknown" let description = jsResult["description"] as? String ?? "" let genres = jsResult["genres"] as? [String] ?? [] let status = jsResult["status"] as? String ?? "UNKNOWN" let coverImage = jsResult["coverImage"] as? String let manga = Manga( slug: "solo-leveling", title: title, description: description, genres: genres, status: status, url: "https://manhwaweb.com/manga/solo-leveling", coverImage: coverImage?.isEmpty == false ? coverImage : nil ) XCTAssertEqual(manga.title, "Solo Leveling") XCTAssertEqual(manga.genres.count, 3) XCTAssertTrue(manga.genres.contains("Action")) XCTAssertEqual(manga.status, "PUBLICANDOSE") XCTAssertEqual(manga.coverImage, "https://example.com/cover.jpg") } func testMangaInfoParsingWithEmptyFields() { let jsResult: [String: Any] = [ "title": "", "description": "", "genres": [], "status": "", "coverImage": "" ] let title = jsResult["title"] as? String ?? "Unknown" let description = jsResult["description"] as? String ?? "" let genres = jsResult["genres"] as? [String] ?? [] let status = jsResult["status"] as? String ?? "UNKNOWN" let coverImage = jsResult["coverImage"] as? String let manga = Manga( slug: "test", title: title, description: description, genres: genres, status: status, url: "url", coverImage: coverImage?.isEmpty == false ? coverImage : nil ) XCTAssertEqual(manga.title, "Unknown") XCTAssertTrue(manga.description.isEmpty) XCTAssertTrue(manga.genres.isEmpty) XCTAssertEqual(manga.status, "") XCTAssertNil(manga.coverImage) } func testMangaStatusParsing() { // Simular diferentes estados let testCases: [(String, String)] = [ ("PUBLICANDOSE", "PUBLICANDOSE"), ("FINALIZADO", "FINALIZADO"), ("EN PAUSA", "EN_PAUSA"), ("EN_ESPERA", "EN_ESPERA"), ("Unknown", "UNKNOWN") ] for (input, expected) in testCases { let normalized = input.uppercased().replacingOccurrences(of: " ", with: "_") XCTAssertEqual(normalized, expected) } } // MARK: - URL Construction Tests func testMangaURLConstruction() { let slug = "solo-leveling" let expectedURL = "https://manhwaweb.com/manga/\(slug)" // Simular construcción de URL let url = URL(string: expectedURL) XCTAssertNotNil(url) XCTAssertEqual(url?.absoluteString, expectedURL) } func testChapterURLConstruction() { let slug = "solo-leveling/150" let expectedURL = "https://manhwaweb.com/leer/\(slug)" let url = URL(string: expectedURL) XCTAssertNotNil(url) XCTAssertEqual(url?.absoluteString, expectedURL) } func testURLConstructionWithSpecialCharacters() { let slug = "manga-with-special-chars" let expectedURL = "https://manhwaweb.com/manga/\(slug)" let url = URL(string: expectedURL) XCTAssertNotNil(url) XCTAssertEqual(url?.absoluteString, expectedURL) } // MARK: - Edge Cases Tests func testChapterNumberExtraction() { // Simular regex extraction de números de capítulo let testCases: [(String, Int?)] = [ ("https://manhwaweb.com/leer/solo-leveling/150", 150), ("https://manhwaweb.com/leer/solo-leveling/150/", 150), ("https://manhwaweb.com/leer/solo-leveling/50?page=2", 50), ("https://manhwaweb.com/leer/test", nil), ("", nil) ] let pattern = "(\\d+)(?:\\/|\\?|\\s*$)" for (url, expectedNumber) in testCases { let regex = try? NSRegularExpression(pattern: pattern) let range = NSRange(url.startIndex..., in: url) let match = regex?.firstMatch(in: url, range: range) if let match = match, let matchRange = Range(match.range(at: 1), in: url) { let number = Int(String(url[matchRange])) XCTAssertEqual(number, expectedNumber) } else { XCTAssertNil(expectedNumber, "URL \(url) should not extract a number") } } } func testChapterSlugExtraction() { let href = "/leer/solo-leveling/150" // Simular extracción de slug let slug = href.replacingOccurrences(of: "/leer/", with: "").replacingOccurrences(of: "^/", with: "", options: .regularExpression) XCTAssertEqual(slug, "solo-leveling/150") } func testDuplicateRemovalPreservingOrder() { let chapters = [ ["number": 10, "title": "Ch 10"], ["number": 9, "title": "Ch 9"], ["number": 10, "title": "Ch 10 Dup"], ["number": 8, "title": "Ch 8"], ["number": 9, "title": "Ch 9 Dup"] ] // Eliminar duplicados preservando el orden de primera aparición let seen = NSMutableSet() let unique = chapters.filter { chapter in let number = chapter["number"] as! Int if seen.contains(number) { return false } seen.add(number) return true } XCTAssertEqual(unique.count, 3) XCTAssertEqual((unique[0]["number"] as? Int), 10) XCTAssertEqual((unique[1]["number"] as? Int), 9) XCTAssertEqual((unique[2]["number"] as? Int), 8) } // MARK: - Async Behavior Tests func testScraperIsMainActor() { // Verificar que el scraper opera en MainActor XCTAssertTrue(MainActor.isMainActor, "Scraper should run on main actor") } // MARK: - Performance Tests func testChapterParsingPerformance() { let jsResult: [[String: Any]] = (0..<1000).map { i in [ "number": i, "title": "Chapter \(i)", "url": "https://manhwaweb.com/leer/test/\(i)", "slug": "test/\(i)" ] } measure { _ = jsResult.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) } } } func testImageFilteringPerformance() { // Crear 10,000 URLs con algunos duplicados var urls: [String] = [] for i in 0..<10000 { urls.append("https://example.com/image\(i).jpg") if i % 100 == 0 { urls.append("https://example.com/avatar\(i).jpg") urls.append("https://example.com/icon\(i).png") } } measure { let filtered = urls.filter { url in let lowercased = url.lowercased() return !lowercased.contains("avatar") && !lowercased.contains("icon") && !lowercased.contains("logo") } _ = Array(Set(filtered)) } } func testChapterSortingPerformance() { var chapters: [Chapter] = [] for i in 0..<1000 { chapters.append(Chapter(number: Int.random(in: 1...1000), title: "Ch", url: "", slug: "")) } measure { _ = chapters.sorted { $0.number > $1.number } } } // MARK: - Integration Simulation Tests func testCompleteScrapingFlowSimulation() { // Simular el flujo completo de scraping // 1. Simular respuesta de capítulos let chapterJSResponse: [[String: Any]] = [ ["number": 10, "title": "Chapter 10", "url": "url10", "slug": "slug10"], ["number": 9, "title": "Chapter 9", "url": "url9", "slug": "slug9"] ] let chapters = chapterJSResponse.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) } XCTAssertEqual(chapters.count, 2) // 2. Simular respuesta de imágenes para el primer capítulo let imagesJSResponse: [String] = [ "https://example.com/page1.jpg", "https://example.com/page2.jpg", "https://example.com/page3.jpg" ] let images = imagesJSResponse.filter { $0.hasPrefix("https://") } XCTAssertEqual(images.count, 3) // 3. Crear páginas let pages = images.enumerated().map { index, url in MangaPage(url: url, index: index) } XCTAssertEqual(pages.count, 3) XCTAssertEqual(pages[0].url, "https://example.com/page1.jpg") XCTAssertEqual(pages[0].index, 0) } }