✨ 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>
541 lines
17 KiB
Swift
541 lines
17 KiB
Swift
import XCTest
|
|
@testable import MangaReader
|
|
|
|
/// Tests para los modelos de datos: Manga, Chapter, MangaPage, ReadingProgress, DownloadedChapter
|
|
final class ModelTests: XCTestCase {
|
|
|
|
// MARK: - Setup & Teardown
|
|
|
|
override func setUp() {
|
|
super.setUp()
|
|
// Configuración inicial antes de cada test
|
|
}
|
|
|
|
override func tearDown() {
|
|
// Limpieza después de cada test
|
|
super.tearDown()
|
|
}
|
|
|
|
// MARK: - Manga Model Tests
|
|
|
|
func testMangaInitialization() {
|
|
let manga = Manga(
|
|
slug: "tower-of-god",
|
|
title: "Tower of God",
|
|
description: "A young boy enters a mysterious tower",
|
|
genres: ["Action", "Fantasy", "Adventure"],
|
|
status: "PUBLICANDOSE",
|
|
url: "https://manhwaweb.com/manga/tower-of-god",
|
|
coverImage: "https://example.com/cover.jpg"
|
|
)
|
|
|
|
XCTAssertEqual(manga.slug, "tower-of-god")
|
|
XCTAssertEqual(manga.title, "Tower of God")
|
|
XCTAssertEqual(manga.genres.count, 3)
|
|
XCTAssertEqual(manga.id, "tower-of-god")
|
|
}
|
|
|
|
func testMangaCodableSerialization() {
|
|
let manga = Manga(
|
|
slug: "solo-leveling",
|
|
title: "Solo Leveling",
|
|
description: "The weakest hunter becomes the strongest",
|
|
genres: ["Action", "Adventure"],
|
|
status: "FINALIZADO",
|
|
url: "https://manhwaweb.com/manga/solo-leveling",
|
|
coverImage: nil
|
|
)
|
|
|
|
// Test encoding
|
|
do {
|
|
let encoder = JSONEncoder()
|
|
encoder.outputFormatting = .prettyPrinted
|
|
let jsonData = try encoder.encode(manga)
|
|
XCTAssertFalse(jsonData.isEmpty)
|
|
|
|
// Test decoding
|
|
let decoder = JSONDecoder()
|
|
let decodedManga = try decoder.decode(Manga.self, from: jsonData)
|
|
|
|
XCTAssertEqual(manga.slug, decodedManga.slug)
|
|
XCTAssertEqual(manga.title, decodedManga.title)
|
|
XCTAssertEqual(manga.description, decodedManga.description)
|
|
XCTAssertEqual(manga.genres, decodedManga.genres)
|
|
XCTAssertEqual(manga.status, decodedManga.status)
|
|
XCTAssertEqual(manga.url, decodedManga.url)
|
|
XCTAssertEqual(manga.coverImage, decodedManga.coverImage)
|
|
} catch {
|
|
XCTFail("Coding/decoding failed: \(error)")
|
|
}
|
|
}
|
|
|
|
func testMangaDisplayStatus() {
|
|
let publishingManga = Manga(
|
|
slug: "test1",
|
|
title: "Test",
|
|
description: "Desc",
|
|
genres: [],
|
|
status: "PUBLICANDOSE",
|
|
url: ""
|
|
)
|
|
XCTAssertEqual(publishingManga.displayStatus, "En publicación")
|
|
|
|
let finishedManga = Manga(
|
|
slug: "test2",
|
|
title: "Test",
|
|
description: "Desc",
|
|
genres: [],
|
|
status: "FINALIZADO",
|
|
url: ""
|
|
)
|
|
XCTAssertEqual(finishedManga.displayStatus, "Finalizado")
|
|
|
|
let pausedManga = Manga(
|
|
slug: "test3",
|
|
title: "Test",
|
|
description: "Desc",
|
|
genres: [],
|
|
status: "EN_PAUSA",
|
|
url: ""
|
|
)
|
|
XCTAssertEqual(pausedManga.displayStatus, "En pausa")
|
|
|
|
let waitingManga = Manga(
|
|
slug: "test4",
|
|
title: "Test",
|
|
description: "Desc",
|
|
genres: [],
|
|
status: "EN_ESPERA",
|
|
url: ""
|
|
)
|
|
XCTAssertEqual(waitingManga.displayStatus, "En pausa")
|
|
|
|
let unknownManga = Manga(
|
|
slug: "test5",
|
|
title: "Test",
|
|
description: "Desc",
|
|
genres: [],
|
|
status: "UNKNOWN_STATUS",
|
|
url: ""
|
|
)
|
|
XCTAssertEqual(unknownManga.displayStatus, "UNKNOWN_STATUS")
|
|
}
|
|
|
|
func testMangaHashable() {
|
|
let manga1 = Manga(
|
|
slug: "test",
|
|
title: "Test Manga",
|
|
description: "Description",
|
|
genres: ["Action"],
|
|
status: "PUBLICANDOSE",
|
|
url: "https://example.com",
|
|
coverImage: nil
|
|
)
|
|
|
|
let manga2 = Manga(
|
|
slug: "test",
|
|
title: "Different Title",
|
|
description: "Different Description",
|
|
genres: ["Drama"],
|
|
status: "FINALIZADO",
|
|
url: "https://different.com",
|
|
coverImage: "cover.jpg"
|
|
)
|
|
|
|
XCTAssertEqual(manga1, manga2)
|
|
XCTAssertEqual(manga1.hashValue, manga2.hashValue)
|
|
|
|
let set: Set<Manga> = [manga1, manga2]
|
|
XCTAssertEqual(set.count, 1, "Mangas with same slug should be considered equal")
|
|
}
|
|
|
|
// MARK: - Chapter Model Tests
|
|
|
|
func testChapterInitialization() {
|
|
let chapter = Chapter(
|
|
number: 150,
|
|
title: "The Final Battle",
|
|
url: "https://manhwaweb.com/leer/solo-leveling/150",
|
|
slug: "solo-leveling/150"
|
|
)
|
|
|
|
XCTAssertEqual(chapter.id, 150)
|
|
XCTAssertEqual(chapter.number, 150)
|
|
XCTAssertEqual(chapter.title, "The Final Battle")
|
|
XCTAssertEqual(chapter.isRead, false)
|
|
XCTAssertEqual(chapter.isDownloaded, false)
|
|
XCTAssertEqual(chapter.lastReadPage, 0)
|
|
}
|
|
|
|
func testChapterDisplayNumber() {
|
|
let chapter = Chapter(number: 42, title: "Test", url: "", slug: "")
|
|
XCTAssertEqual(chapter.displayNumber, "Capítulo 42")
|
|
}
|
|
|
|
func testChapterProgress() {
|
|
var chapter = Chapter(number: 1, title: "Test", url: "", slug: "")
|
|
XCTAssertEqual(chapter.progress, 0.0)
|
|
|
|
chapter.lastReadPage = 5
|
|
XCTAssertEqual(chapter.progress, 5.0)
|
|
|
|
chapter.lastReadPage = 100
|
|
XCTAssertEqual(chapter.progress, 100.0)
|
|
}
|
|
|
|
func testChapterCodableSerialization() {
|
|
let chapter = Chapter(
|
|
number: 50,
|
|
title: "Chapter 50",
|
|
url: "https://manhwaweb.com/leer/test/50",
|
|
slug: "test/50",
|
|
isRead: true,
|
|
isDownloaded: true,
|
|
lastReadPage: 25
|
|
)
|
|
|
|
// Test encoding
|
|
do {
|
|
let encoder = JSONEncoder()
|
|
let jsonData = try encoder.encode(chapter)
|
|
|
|
// Test decoding
|
|
let decoder = JSONDecoder()
|
|
let decodedChapter = try decoder.decode(Chapter.self, from: jsonData)
|
|
|
|
XCTAssertEqual(chapter.number, decodedChapter.number)
|
|
XCTAssertEqual(chapter.title, decodedChapter.title)
|
|
XCTAssertEqual(chapter.url, decodedChapter.url)
|
|
XCTAssertEqual(chapter.slug, decodedChapter.slug)
|
|
XCTAssertEqual(chapter.isRead, decodedChapter.isRead)
|
|
XCTAssertEqual(chapter.isDownloaded, decodedChapter.isDownloaded)
|
|
XCTAssertEqual(chapter.lastReadPage, decodedChapter.lastReadPage)
|
|
} catch {
|
|
XCTFail("Chapter coding/decoding failed: \(error)")
|
|
}
|
|
}
|
|
|
|
func testChapterHashable() {
|
|
let chapter1 = Chapter(
|
|
number: 10,
|
|
title: "Chapter 10",
|
|
url: "url1",
|
|
slug: "slug1",
|
|
isRead: true,
|
|
isDownloaded: false,
|
|
lastReadPage: 5
|
|
)
|
|
|
|
let chapter2 = Chapter(
|
|
number: 10,
|
|
title: "Different Title",
|
|
url: "url2",
|
|
slug: "slug2",
|
|
isRead: false,
|
|
isDownloaded: true,
|
|
lastReadPage: 10
|
|
)
|
|
|
|
XCTAssertEqual(chapter1, chapter2)
|
|
XCTAssertEqual(chapter1.hashValue, chapter2.hashValue)
|
|
}
|
|
|
|
// MARK: - MangaPage Model Tests
|
|
|
|
func testMangaPageInitialization() {
|
|
let page = MangaPage(url: "https://example.com/page1.jpg", index: 0)
|
|
|
|
XCTAssertEqual(page.id, "https://example.com/page1.jpg")
|
|
XCTAssertEqual(page.url, "https://example.com/page1.jpg")
|
|
XCTAssertEqual(page.index, 0)
|
|
XCTAssertEqual(page.isCached, false)
|
|
}
|
|
|
|
func testMangaPageThumbnailURL() {
|
|
let page = MangaPage(url: "https://example.com/high-res.jpg", index: 5)
|
|
XCTAssertEqual(page.thumbnailURL, "https://example.com/high-res.jpg")
|
|
}
|
|
|
|
func testMangaPageCodableSerialization() {
|
|
let page = MangaPage(
|
|
url: "https://example.com/page.jpg",
|
|
index: 10,
|
|
isCached: true
|
|
)
|
|
|
|
do {
|
|
let encoder = JSONEncoder()
|
|
let jsonData = try encoder.encode(page)
|
|
|
|
let decoder = JSONDecoder()
|
|
let decodedPage = try decoder.decode(MangaPage.self, from: jsonData)
|
|
|
|
XCTAssertEqual(page.url, decodedPage.url)
|
|
XCTAssertEqual(page.index, decodedPage.index)
|
|
XCTAssertEqual(page.isCached, decodedPage.isCached)
|
|
} catch {
|
|
XCTFail("MangaPage coding/decoding failed: \(error)")
|
|
}
|
|
}
|
|
|
|
func testMangaPageHashable() {
|
|
let page1 = MangaPage(url: "https://example.com/page.jpg", index: 0)
|
|
let page2 = MangaPage(url: "https://example.com/page.jpg", index: 5, isCached: true)
|
|
|
|
XCTAssertEqual(page1, page2)
|
|
XCTAssertEqual(page1.hashValue, page2.hashValue)
|
|
}
|
|
|
|
// MARK: - ReadingProgress Model Tests
|
|
|
|
func testReadingProgressInitialization() {
|
|
let progress = ReadingProgress(
|
|
mangaSlug: "solo-leveling",
|
|
chapterNumber: 50,
|
|
pageNumber: 10,
|
|
timestamp: Date()
|
|
)
|
|
|
|
XCTAssertEqual(progress.mangaSlug, "solo-leveling")
|
|
XCTAssertEqual(progress.chapterNumber, 50)
|
|
XCTAssertEqual(progress.pageNumber, 10)
|
|
}
|
|
|
|
func testReadingProgressIsCompleted() {
|
|
let incompleteProgress = ReadingProgress(
|
|
mangaSlug: "test",
|
|
chapterNumber: 1,
|
|
pageNumber: 3,
|
|
timestamp: Date()
|
|
)
|
|
XCTAssertFalse(incompleteProgress.isCompleted)
|
|
|
|
let completedProgress = ReadingProgress(
|
|
mangaSlug: "test",
|
|
chapterNumber: 1,
|
|
pageNumber: 6,
|
|
timestamp: Date()
|
|
)
|
|
XCTAssertTrue(completedProgress.isCompleted)
|
|
|
|
let boundaryProgress = ReadingProgress(
|
|
mangaSlug: "test",
|
|
chapterNumber: 1,
|
|
pageNumber: 5,
|
|
timestamp: Date()
|
|
)
|
|
XCTAssertFalse(boundaryProgress.isCompleted, "Exactly 5 pages should not be considered completed")
|
|
}
|
|
|
|
func testReadingProgressCodableSerialization() {
|
|
let timestamp = Date(timeIntervalSince1970: 1609459200) // 2021-01-01
|
|
let progress = ReadingProgress(
|
|
mangaSlug: "tower-of-god",
|
|
chapterNumber: 500,
|
|
pageNumber: 42,
|
|
timestamp: timestamp
|
|
)
|
|
|
|
do {
|
|
let encoder = JSONEncoder()
|
|
let jsonData = try encoder.encode(progress)
|
|
|
|
let decoder = JSONDecoder()
|
|
let decodedProgress = try decoder.decode(ReadingProgress.self, from: jsonData)
|
|
|
|
XCTAssertEqual(progress.mangaSlug, decodedProgress.mangaSlug)
|
|
XCTAssertEqual(progress.chapterNumber, decodedProgress.chapterNumber)
|
|
XCTAssertEqual(progress.pageNumber, decodedProgress.pageNumber)
|
|
|
|
// Compare timestamp with tolerance for encoding/decoding precision
|
|
let timeDifference = abs(progress.timestamp.timeIntervalSince(decodedProgress.timestamp))
|
|
XCTAssertLessThan(timeDifference, 0.001, "Timestamps should match within milliseconds")
|
|
} catch {
|
|
XCTFail("ReadingProgress coding/decoding failed: \(error)")
|
|
}
|
|
}
|
|
|
|
// MARK: - DownloadedChapter Model Tests
|
|
|
|
func testDownloadedChapterInitialization() {
|
|
let pages = [
|
|
MangaPage(url: "page1.jpg", index: 0),
|
|
MangaPage(url: "page2.jpg", index: 1)
|
|
]
|
|
|
|
let downloadedChapter = DownloadedChapter(
|
|
mangaSlug: "solo-leveling",
|
|
mangaTitle: "Solo Leveling",
|
|
chapterNumber: 100,
|
|
pages: pages,
|
|
downloadedAt: Date(),
|
|
totalSize: 1024000
|
|
)
|
|
|
|
XCTAssertEqual(downloadedChapter.id, "solo-leveling-chapter100")
|
|
XCTAssertEqual(downloadedChapter.mangaSlug, "solo-leveling")
|
|
XCTAssertEqual(downloadedChapter.pages.count, 2)
|
|
}
|
|
|
|
func testDownloadedChapterDisplayTitle() {
|
|
let chapter = DownloadedChapter(
|
|
mangaSlug: "test",
|
|
mangaTitle: "Test Manga",
|
|
chapterNumber: 25,
|
|
pages: [],
|
|
downloadedAt: Date()
|
|
)
|
|
|
|
XCTAssertEqual(chapter.displayTitle, "Test Manga - Capítulo 25")
|
|
}
|
|
|
|
func testDownloadedChapterCodableSerialization() {
|
|
let pages = [
|
|
MangaPage(url: "page1.jpg", index: 0, isCached: true),
|
|
MangaPage(url: "page2.jpg", index: 1, isCached: true)
|
|
]
|
|
|
|
let downloadDate = Date(timeIntervalSince1970: 1609459200)
|
|
let downloadedChapter = DownloadedChapter(
|
|
mangaSlug: "test-manga",
|
|
mangaTitle: "Test Manga",
|
|
chapterNumber: 10,
|
|
pages: pages,
|
|
downloadedAt: downloadDate,
|
|
totalSize: 2048000
|
|
)
|
|
|
|
do {
|
|
let encoder = JSONEncoder()
|
|
let jsonData = try encoder.encode(downloadedChapter)
|
|
|
|
let decoder = JSONDecoder()
|
|
let decodedChapter = try decoder.decode(DownloadedChapter.self, from: jsonData)
|
|
|
|
XCTAssertEqual(downloadedChapter.mangaSlug, decodedChapter.mangaSlug)
|
|
XCTAssertEqual(downloadedChapter.mangaTitle, decodedChapter.mangaTitle)
|
|
XCTAssertEqual(downloadedChapter.chapterNumber, decodedChapter.chapterNumber)
|
|
XCTAssertEqual(downloadedChapter.pages.count, decodedChapter.pages.count)
|
|
XCTAssertEqual(downloadedChapter.totalSize, decodedChapter.totalSize)
|
|
} catch {
|
|
XCTFail("DownloadedChapter coding/decoding failed: \(error)")
|
|
}
|
|
}
|
|
|
|
// MARK: - Edge Cases Tests
|
|
|
|
func testMangaWithEmptyGenres() {
|
|
let manga = Manga(
|
|
slug: "test",
|
|
title: "Test",
|
|
description: "Description",
|
|
genres: [],
|
|
status: "PUBLICANDOSE",
|
|
url: "url"
|
|
)
|
|
|
|
XCTAssertTrue(manga.genres.isEmpty)
|
|
XCTAssertEqual(manga.genres.count, 0)
|
|
}
|
|
|
|
func testMangaWithNilCoverImage() {
|
|
let manga1 = Manga(
|
|
slug: "test",
|
|
title: "Test",
|
|
description: "Desc",
|
|
genres: [],
|
|
status: "PUBLICANDOSE",
|
|
url: "url",
|
|
coverImage: nil
|
|
)
|
|
XCTAssertNil(manga1.coverImage)
|
|
|
|
let manga2 = Manga(
|
|
slug: "test",
|
|
title: "Test",
|
|
description: "Desc",
|
|
genres: [],
|
|
status: "PUBLICANDOSE",
|
|
url: "url",
|
|
coverImage: ""
|
|
)
|
|
XCTAssertNil(manga2.coverImage, "Empty string should be treated as nil")
|
|
}
|
|
|
|
func testChapterWithZeroNumber() {
|
|
let chapter = Chapter(number: 0, title: "Prologue", url: "url", slug: "slug")
|
|
XCTAssertEqual(chapter.id, 0)
|
|
XCTAssertEqual(chapter.displayNumber, "Capítulo 0")
|
|
}
|
|
|
|
func testChapterWithLargePageNumber() {
|
|
var chapter = Chapter(number: 1, title: "Test", url: "url", slug: "slug")
|
|
chapter.lastReadPage = 10000
|
|
|
|
XCTAssertEqual(chapter.progress, 10000.0)
|
|
}
|
|
|
|
func testMangaPageWithNegativeIndex() {
|
|
let page = MangaPage(url: "test.jpg", index: -1)
|
|
XCTAssertEqual(page.index, -1)
|
|
// Edge case: negative indices should still work
|
|
}
|
|
|
|
func testReadingProgressWithZeroPages() {
|
|
let progress = ReadingProgress(
|
|
mangaSlug: "test",
|
|
chapterNumber: 1,
|
|
pageNumber: 0,
|
|
timestamp: Date()
|
|
)
|
|
|
|
XCTAssertFalse(progress.isCompleted)
|
|
}
|
|
|
|
func testDownloadedChapterWithEmptyPages() {
|
|
let chapter = DownloadedChapter(
|
|
mangaSlug: "test",
|
|
mangaTitle: "Test",
|
|
chapterNumber: 1,
|
|
pages: [],
|
|
downloadedAt: Date()
|
|
)
|
|
|
|
XCTAssertTrue(chapter.pages.isEmpty)
|
|
XCTAssertEqual(chapter.totalSize, 0)
|
|
}
|
|
|
|
// MARK: - Performance Tests
|
|
|
|
func testMangaEncodingPerformance() {
|
|
let manga = Manga(
|
|
slug: "test-manga-with-very-long-slug",
|
|
title: "Test Manga Title That Is Quite Long",
|
|
description: "This is a very long description that contains a lot of text about the manga plot and characters",
|
|
genres: ["Action", "Adventure", "Comedy", "Drama", "Fantasy", "Horror", "Mystery", "Romance", "Sci-Fi", "Thriller"],
|
|
status: "PUBLICANDOSE",
|
|
url: "https://manhwaweb.com/manga/test-manga",
|
|
coverImage: "https://example.com/cover.jpg"
|
|
)
|
|
|
|
measure {
|
|
for _ in 0..<1000 {
|
|
_ = try? JSONEncoder().encode(manga)
|
|
}
|
|
}
|
|
}
|
|
|
|
func testChapterArrayEqualityPerformance() {
|
|
var chapters: [Chapter] = []
|
|
for i in 0..<1000 {
|
|
chapters.append(Chapter(number: i, title: "Chapter \(i)", url: "url\(i)", slug: "slug\(i)"))
|
|
}
|
|
|
|
let set = Set(chapters)
|
|
|
|
measure {
|
|
_ = set.contains(chapters[500])
|
|
}
|
|
}
|
|
}
|