import XCTest final class LargeBookOnDemandTests: XCTestCase { private let app = XCUIApplication() private let largeBookQuery = "凡人修仙传" override func setUpWithError() throws { continueAfterFailure = false } func testLargeBookInitialOpenIsReadableWhilePaginationIsPartial() throws { app.launchAndOpenSampleBook(bookTitleQuery: largeBookQuery, resetsReaderState: true) app.waitForReader(timeout: 20) let partialState = app.waitForDemoReaderState(timeout: 15, description: "大书首开进入局部分页") { state in state.mode == "bookPageMap" && state.pagination == "partial" && (state.page ?? 0) >= 1 } XCTAssertEqual(partialState.mode, "bookPageMap") XCTAssertEqual(partialState.pagination, "partial") XCTAssertTrue(app.otherElements[IDs.readerContent].waitForExistence(timeout: 5), "首开局部分页阶段应已可阅读") } func testLargeBookBackgroundPaginationProgresses() throws { app.launchAndOpenSampleBook(bookTitleQuery: largeBookQuery, resetsReaderState: true) app.waitForReader(timeout: 20) let initialState = app.waitForDemoReaderState(timeout: 15, description: "获取初始分页进度") { state in state.mode == "bookPageMap" && state.knownChapters != nil && state.buildableChapters != nil } let initialKnownChapters = initialState.knownChapters ?? 0 let initialKnownPages = initialState.knownPages ?? 0 let progressedState = app.waitForDemoReaderState(timeout: 45, description: "后台分页推进") { state in guard state.mode == "bookPageMap" else { return false } if state.pagination == "full" { return true } return (state.knownChapters ?? 0) > initialKnownChapters || (state.knownPages ?? 0) > initialKnownPages } XCTAssertEqual(progressedState.mode, "bookPageMap") XCTAssertTrue( (progressedState.knownChapters ?? 0) >= initialKnownChapters || (progressedState.knownPages ?? 0) >= initialKnownPages ) } func testLargeBookSecondOpenRestoresFromCache() throws { app.launchAndOpenSampleBook(bookTitleQuery: largeBookQuery, resetsReaderState: true) app.waitForReader(timeout: 20) let firstFullState = app.waitForDemoReaderState(timeout: 60, description: "首次打开完成全书缓存") { state in state.mode == "bookPageMap" && state.pagination == "full" && (state.buildableChapters ?? 0) > 0 } XCTAssertGreaterThan(firstFullState.cacheFiles ?? 0, 0, "首次全量解析后应生成章节摘要缓存") app.showReaderChromeIfNeeded() XCTAssertTrue(app.buttons[IDs.readerBack].waitForExistence(timeout: 5), "返回按钮不存在") app.buttons[IDs.readerBack].tap() XCTAssertTrue(app.tables[IDs.demoBooksTable].waitForExistence(timeout: 5), "书架列表未出现") app.launchAndOpenSampleBook(bookTitleQuery: largeBookQuery, resetsReaderState: false) app.waitForReader(timeout: 15) let reopenedState = app.waitForDemoReaderState(timeout: 10, description: "二开直接恢复完整缓存") { state in state.mode == "bookPageMap" && state.pagination == "full" && (state.knownChapters ?? 0) == (state.buildableChapters ?? -1) } XCTAssertEqual(reopenedState.pagination, "full") XCTAssertEqual(reopenedState.knownChapters, reopenedState.buildableChapters) XCTAssertEqual(reopenedState.knownPages, firstFullState.knownPages, "二开命中缓存后总页数应稳定一致") } func testCorruptedCacheFallsBackGracefully() throws { app.launchAndOpenSampleBook(bookTitleQuery: largeBookQuery, resetsReaderState: true, clearsCache: true) app.waitForReader(timeout: 20) _ = app.waitForDemoReaderState(timeout: 60, description: "首次打开完成缓存建立") { state in state.pagination == "full" && (state.cacheFiles ?? 0) > 0 } app.showReaderChromeIfNeeded() app.buttons[IDs.readerBack].tap() XCTAssertTrue(app.tables[IDs.demoBooksTable].waitForExistence(timeout: 5)) app.launchAndOpenSampleBook(bookTitleQuery: largeBookQuery, resetsReaderState: false, corruptsCache: true) app.waitForReader(timeout: 20) let recoveredState = app.waitForDemoReaderState(timeout: 45, description: "损坏缓存后仍可恢复阅读") { state in state.isOpened && state.pagination != "none" && state.mode == "bookPageMap" } XCTAssertEqual(recoveredState.lastError, "none", "缓存损坏不应导致阅读器进入错误状态") } func testClearingCacheForcesRebuild() throws { app.launchAndOpenSampleBook(bookTitleQuery: largeBookQuery, resetsReaderState: true, clearsCache: true) app.waitForReader(timeout: 20) _ = app.waitForDemoReaderState(timeout: 60, description: "首次缓存建立") { state in state.pagination == "full" && (state.cacheFiles ?? 0) > 0 } app.showReaderChromeIfNeeded() app.buttons[IDs.readerBack].tap() XCTAssertTrue(app.tables[IDs.demoBooksTable].waitForExistence(timeout: 5)) app.launchAndOpenSampleBook(bookTitleQuery: largeBookQuery, resetsReaderState: false, clearsCache: true) app.waitForReader(timeout: 20) let rebuiltState = app.waitForDemoReaderState(timeout: 60, description: "清空缓存后重新构建") { state in state.pagination == "full" && (state.cacheFiles ?? 0) > 0 } XCTAssertGreaterThan(rebuiltState.cacheFiles ?? 0, 0, "清空缓存后应重新写入缓存文件") } func testMiddleChapterOpenPositionStableAfterFullParse() throws { // 从中间章节打开,等后台解析完成,验证阅读位置不跳转 app.launchAndOpenSampleBook( bookTitleQuery: largeBookQuery, pageNumber: 50, resetsReaderState: true ) app.waitForReader(timeout: 20) let partialState = app.waitForDemoReaderState(timeout: 15, description: "中间章节首开进入局部分页") { state in state.mode == "bookPageMap" && state.pagination == "partial" && (state.page ?? 0) >= 1 } let pageAfterOpen = partialState.page ?? 0 XCTAssertGreaterThan(pageAfterOpen, 1, "应从中间章节打开,当前页=\(pageAfterOpen)") // 等待后台解析完成 let fullState = app.waitForDemoReaderState(timeout: 90, description: "后台解析完成") { state in state.pagination == "full" } XCTAssertEqual(fullState.pagination, "full", "后台解析应完成") // 验证页码未跳转(方案 C:pending map 不会在解析完成时应用) let pageAfterParse = fullState.page ?? 0 XCTAssertEqual(pageAfterParse, pageAfterOpen, "后台解析完成后页码不应跳转:解析前=\(pageAfterOpen) 解析后=\(pageAfterParse)") // 验证翻页功能正常(触发 pending map 应用) let paging = app.collectionViews[IDs.readerPaging] if paging.waitForExistence(timeout: 5) { paging.swipeLeft() } let afterSwipe = app.waitForDemoReaderState(timeout: 10, description: "翻页后页码推进") { state in state.page != nil && state.page != pageAfterParse } XCTAssertGreaterThan(afterSwipe.page ?? 0, pageAfterParse, "翻页后页码应前进:翻页前=\(pageAfterParse) 翻页后=\(afterSwipe.page ?? 0)") } func testMiddleChapterOpenSwipeAfterFullParseResolvesCorrectly() throws { // 从中间章节打开,等解析完成,连续翻页验证页码连续性 app.launchAndOpenSampleBook( bookTitleQuery: largeBookQuery, pageNumber: 30, resetsReaderState: true ) app.waitForReader(timeout: 20) _ = app.waitForDemoReaderState(timeout: 15, description: "局部分页就绪") { state in state.mode == "bookPageMap" && state.pagination == "partial" } // 等待解析完成 _ = app.waitForDemoReaderState(timeout: 90, description: "后台解析完成") { state in state.pagination == "full" } // 连续翻 3 页,验证页码连续递增 let paging = app.collectionViews[IDs.readerPaging] XCTAssertTrue(paging.waitForExistence(timeout: 5), "分页视图不存在") var previousPage = app.currentDemoReaderState()?.page ?? 0 for i in 0..<3 { paging.swipeLeft() RunLoop.current.run(until: Date().addingTimeInterval(1)) let currentState = app.currentDemoReaderState() let currentPage = currentState?.page ?? 0 XCTAssertGreaterThan(currentPage, previousPage, "第 \(i + 1) 次翻页后页码应递增:前=\(previousPage) 后=\(currentPage)") previousPage = currentPage } } func testLargeBookContinuousPagingExtendsKnownPageMap() throws { app.launchAndOpenSampleBook(bookTitleQuery: largeBookQuery, displayType: "scroll", resetsReaderState: true) app.waitForReader(timeout: 20) app.waitForDemoReaderState(timeout: 8, description: "display=horizontalScroll") { $0.display == "horizontalScroll" } let initialState = app.waitForDemoReaderState(timeout: 15, description: "初始局部分页") { state in state.mode == "bookPageMap" && state.knownPages != nil && state.page != nil } let initialKnownPages = initialState.knownPages ?? 0 let initialPage = initialState.page ?? 0 let paging = app.collectionViews[IDs.readerPaging] XCTAssertTrue(paging.waitForExistence(timeout: 5), "分页视图不存在") for _ in 0..<4 { paging.swipeLeft() } let extendedState = app.waitForDemoReaderState(timeout: 20, description: "连续翻页触发扩窗") { state in guard state.mode == "bookPageMap" else { return false } let pageAdvanced = (state.page ?? 0) > initialPage let pageMapExtended = (state.knownPages ?? 0) > initialKnownPages || state.pagination == "full" return pageAdvanced && pageMapExtended } XCTAssertTrue((extendedState.page ?? 0) > initialPage, "连续翻页后当前页应向后推进") XCTAssertTrue( (extendedState.knownPages ?? 0) > initialKnownPages || extendedState.pagination == "full", "连续翻页后已知页图应扩窗或直接完成全量分页" ) } }