diff --git a/AIStudyApp/AIStudyAppTests/ReadingPositionAdapterTests.swift b/AIStudyApp/AIStudyAppTests/ReadingPositionAdapterTests.swift new file mode 100644 index 0000000..09594e2 --- /dev/null +++ b/AIStudyApp/AIStudyAppTests/ReadingPositionAdapterTests.swift @@ -0,0 +1,202 @@ +import XCTest +@testable import AIStudyApp + +final class ReadingPositionAdapterTests: XCTestCase { + + // MARK: - fromValue: Markdown + + func test_fromValue_markdown_with_blockId() { + let v = ReadingPositionValue(type: "Markdown", blockId: "b1", lineNumber: nil, + pageNumber: nil, chapterId: nil, scrollProgress: 0.5, pageProgress: nil, + overallProgress: nil, zoomScale: nil, offsetX: nil, offsetY: nil, chapterProgress: nil) + let pos = ReadingPositionAdapter.fromValue(v) + guard case let .markdown(blockId, scrollProgress) = pos else { + return XCTFail("Expected .markdown, got \(String(describing: pos))") + } + XCTAssertEqual(blockId, "b1") + XCTAssertEqual(scrollProgress, 0.5) + } + + func test_fromValue_markdown_nil_blockId_returns_nil() { + let v = ReadingPositionValue(type: "Markdown", blockId: nil, lineNumber: nil, + pageNumber: nil, chapterId: nil, scrollProgress: nil, pageProgress: nil, + overallProgress: nil, zoomScale: nil, offsetX: nil, offsetY: nil, chapterProgress: nil) + XCTAssertNil(ReadingPositionAdapter.fromValue(v)) + } + + // MARK: - fromValue: Text + + func test_fromValue_text_with_optional_fields() { + let v = ReadingPositionValue(type: "Text", blockId: nil, lineNumber: 42, + pageNumber: nil, chapterId: nil, scrollProgress: 0.8, pageProgress: nil, + overallProgress: nil, zoomScale: nil, offsetX: nil, offsetY: nil, chapterProgress: nil) + guard case let .text(lineNumber, scrollProgress) = ReadingPositionAdapter.fromValue(v) else { + return XCTFail("Expected .text") + } + XCTAssertEqual(lineNumber, 42) + XCTAssertEqual(scrollProgress, 0.8) + } + + func test_fromValue_text_defaults_lineNumber_to_1() { + let v = ReadingPositionValue(type: "Text", blockId: nil, lineNumber: nil, + pageNumber: nil, chapterId: nil, scrollProgress: nil, pageProgress: nil, + overallProgress: nil, zoomScale: nil, offsetX: nil, offsetY: nil, chapterProgress: nil) + guard case let .text(lineNumber, _) = ReadingPositionAdapter.fromValue(v) else { + return XCTFail("Expected .text") + } + XCTAssertEqual(lineNumber, 1) + } + + // MARK: - fromValue: Pdf + + func test_fromValue_pdf_with_all_fields() { + let v = ReadingPositionValue(type: "Pdf", blockId: nil, lineNumber: nil, + pageNumber: 5, chapterId: nil, scrollProgress: nil, pageProgress: 0.3, + overallProgress: 0.15, zoomScale: nil, offsetX: nil, offsetY: nil, chapterProgress: nil) + guard case let .pdf(pageNumber, pageProgress, overallProgress) = ReadingPositionAdapter.fromValue(v) else { + return XCTFail("Expected .pdf") + } + XCTAssertEqual(pageNumber, 5) + XCTAssertEqual(pageProgress, 0.3) + XCTAssertEqual(overallProgress, 0.15) + } + + func test_fromValue_pdf_defaults_to_page_1() { + let v = ReadingPositionValue(type: "Pdf", blockId: nil, lineNumber: nil, + pageNumber: nil, chapterId: nil, scrollProgress: nil, pageProgress: nil, + overallProgress: nil, zoomScale: nil, offsetX: nil, offsetY: nil, chapterProgress: nil) + guard case let .pdf(pageNumber, _, _) = ReadingPositionAdapter.fromValue(v) else { + return XCTFail("Expected .pdf") + } + XCTAssertEqual(pageNumber, 1) + } + + // MARK: - fromValue: Image + + func test_fromValue_image_with_zoom_and_offset() { + let v = ReadingPositionValue(type: "Image", blockId: nil, lineNumber: nil, + pageNumber: nil, chapterId: nil, scrollProgress: nil, pageProgress: nil, + overallProgress: nil, zoomScale: 2.0, offsetX: 100, offsetY: 200, chapterProgress: nil) + guard case let .image(zoomScale, offsetX, offsetY) = ReadingPositionAdapter.fromValue(v) else { + return XCTFail("Expected .image") + } + XCTAssertEqual(zoomScale, 2.0) + XCTAssertEqual(offsetX, 100) + XCTAssertEqual(offsetY, 200) + } + + func test_fromValue_image_defaults_zoom_to_1() { + let v = ReadingPositionValue(type: "Image", blockId: nil, lineNumber: nil, + pageNumber: nil, chapterId: nil, scrollProgress: nil, pageProgress: nil, + overallProgress: nil, zoomScale: nil, offsetX: nil, offsetY: nil, chapterProgress: nil) + guard case let .image(zoomScale, _, _) = ReadingPositionAdapter.fromValue(v) else { + return XCTFail("Expected .image") + } + XCTAssertEqual(zoomScale, 1.0) + } + + // MARK: - fromValue: Epub + + func test_fromValue_epub_with_chapterId() { + let v = ReadingPositionValue(type: "Epub", blockId: nil, lineNumber: nil, + pageNumber: nil, chapterId: "ch3", scrollProgress: nil, pageProgress: nil, + overallProgress: 0.6, zoomScale: nil, offsetX: nil, offsetY: nil, chapterProgress: 0.4) + guard case let .epub(chapterId, chapterProgress, overallProgress) = ReadingPositionAdapter.fromValue(v) else { + return XCTFail("Expected .epub") + } + XCTAssertEqual(chapterId, "ch3") + XCTAssertEqual(chapterProgress, 0.4) + XCTAssertEqual(overallProgress, 0.6) + } + + func test_fromValue_epub_nil_chapterId_returns_nil() { + let v = ReadingPositionValue(type: "Epub", blockId: nil, lineNumber: nil, + pageNumber: nil, chapterId: nil, scrollProgress: nil, pageProgress: nil, + overallProgress: nil, zoomScale: nil, offsetX: nil, offsetY: nil, chapterProgress: nil) + XCTAssertNil(ReadingPositionAdapter.fromValue(v)) + } + + // MARK: - fromValue: Unknown / default + + func test_fromValue_unknown_type_returns_unknown() { + let v = ReadingPositionValue(type: "UnknownType", blockId: nil, lineNumber: nil, + pageNumber: nil, chapterId: nil, scrollProgress: nil, pageProgress: nil, + overallProgress: nil, zoomScale: nil, offsetX: nil, offsetY: nil, chapterProgress: nil) + guard case .unknown = ReadingPositionAdapter.fromValue(v) else { + return XCTFail("Expected .unknown") + } + } + + func test_fromValue_nil_type_returns_unknown() { + let v = ReadingPositionValue(type: nil, blockId: nil, lineNumber: nil, + pageNumber: nil, chapterId: nil, scrollProgress: nil, pageProgress: nil, + overallProgress: nil, zoomScale: nil, offsetX: nil, offsetY: nil, chapterProgress: nil) + guard case .unknown = ReadingPositionAdapter.fromValue(v) else { + return XCTFail("Expected .unknown") + } + } + + // MARK: - Clamp behavior + + func test_clamp_scrollProgress_above_1() { + let v = ReadingPositionValue(type: "Text", blockId: nil, lineNumber: nil, + pageNumber: nil, chapterId: nil, scrollProgress: 1.5, pageProgress: nil, + overallProgress: nil, zoomScale: nil, offsetX: nil, offsetY: nil, chapterProgress: nil) + guard case let .text(_, scrollProgress) = ReadingPositionAdapter.fromValue(v) else { + return XCTFail("Expected .text") + } + XCTAssertEqual(scrollProgress, 1.0) + } + + func test_clamp_progress_negative() { + let v = ReadingPositionValue(type: "Text", blockId: nil, lineNumber: nil, + pageNumber: nil, chapterId: nil, scrollProgress: -0.5, pageProgress: nil, + overallProgress: nil, zoomScale: nil, offsetX: nil, offsetY: nil, chapterProgress: nil) + guard case let .text(_, scrollProgress) = ReadingPositionAdapter.fromValue(v) else { + return XCTFail("Expected .text") + } + XCTAssertEqual(scrollProgress, 0.0) + } + + // MARK: - blockId extraction + + func test_blockId_from_heading() { + let block = DocumentBlock.heading(id: "h1", level: 2, text: "Title") + XCTAssertEqual(ReadingPositionAdapter.blockId(from: block), "h1") + } + + func test_blockId_from_paragraph() { + let block = DocumentBlock.paragraph(id: "p1", text: "Content") + XCTAssertEqual(ReadingPositionAdapter.blockId(from: block), "p1") + } + + func test_blockId_from_list() { + let block = DocumentBlock.list(id: "l1", items: [], ordered: true) + XCTAssertEqual(ReadingPositionAdapter.blockId(from: block), "l1") + } + + func test_blockId_from_codeBlock() { + let block = DocumentBlock.codeBlock(id: "c1", language: "swift", code: "print(\"hi\")") + XCTAssertEqual(ReadingPositionAdapter.blockId(from: block), "c1") + } + + func test_blockId_from_quote() { + let block = DocumentBlock.quote(id: "q1", text: "quote") + XCTAssertEqual(ReadingPositionAdapter.blockId(from: block), "q1") + } + + func test_blockId_from_table() { + let block = DocumentBlock.table(id: "t1", headers: [], rows: []) + XCTAssertEqual(ReadingPositionAdapter.blockId(from: block), "t1") + } + + func test_blockId_from_imageBlock() { + let block = DocumentBlock.imageBlock(id: "img1", alt: "img", source: "file.png") + XCTAssertEqual(ReadingPositionAdapter.blockId(from: block), "img1") + } + + func test_blockId_from_horizontalRule() { + let block = DocumentBlock.horizontalRule(id: "hr1") + XCTAssertEqual(ReadingPositionAdapter.blockId(from: block), "hr1") + } +}