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:
540
ios-app/Tests/ModelTests.swift
Normal file
540
ios-app/Tests/ModelTests.swift
Normal 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])
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user