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,540 @@
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])
}
}
}