import XCTest final class LocationPersistenceTests: XCTestCase { private let app = XCUIApplication() override func setUpWithError() throws { continueAfterFailure = false } func testReadingPositionRestoredOnReopen() throws { app.launchAndOpenSampleBook() app.waitForReader(timeout: 12) _ = app.waitForDemoReaderState(timeout: 5, description: "存在页码") { $0.page != nil } let paging = app.collectionViews[IDs.readerPaging] if paging.waitForExistence(timeout: 5) { paging.swipeLeft() } let stateAfterSwipe = app.waitForDemoReaderState(timeout: 8, description: "翻页后页码变化") { state in (state.page ?? 0) > 1 } let pageAfterSwipe = stateAfterSwipe.page ?? 0 guard pageAfterSwipe > 1 else { throw XCTSkip("当前翻页模式下未能翻到第 2 页") } let backButton = app.buttons[IDs.readerBack] if backButton.waitForExistence(timeout: 3) { backButton.tap() } app.launchAndOpenSampleBook() app.waitForReader(timeout: 12) let restoredState = app.waitForDemoReaderState(timeout: 8, description: "恢复阅读位置") { state in state.page != nil } let restoredPage = restoredState.page ?? 0 XCTAssertEqual(restoredPage, pageAfterSwipe, "重新打开后应恢复到上次阅读页码") } func testResetStateLaunchesAtFirstPage() throws { app.launchAndOpenSampleBook(resetsReaderState: true) app.waitForReader(timeout: 12) let state = app.waitForDemoReaderState(timeout: 5, description: "首开第 1 页") { state in state.page != nil } let page = state.page ?? 0 XCTAssertEqual(page, 1, "重置状态后应从第 1 页开始") } func testPositionSurvivesFontChange() throws { app.launchAndOpenSampleBook() app.waitForReader(timeout: 12) let paging = app.collectionViews[IDs.readerPaging] if paging.waitForExistence(timeout: 5) { paging.swipeLeft() } app.waitForDemoReaderState(timeout: 5, description: "page exists") { $0.page != nil } app.showReaderChromeIfNeeded() if app.buttons[IDs.readerSettings].waitForExistence(timeout: 3) { app.buttons[IDs.readerSettings].tap() } if app.buttons[IDs.settingsFontIncrease].waitForExistence(timeout: 5) { app.buttons[IDs.settingsFontIncrease].tap() } if app.buttons[IDs.settingsDone].waitForExistence(timeout: 3) { app.buttons[IDs.settingsDone].tap() } let backButton = app.buttons[IDs.readerBack] if backButton.waitForExistence(timeout: 3) { backButton.tap() } app.launchAndOpenSampleBook() app.waitForReader(timeout: 12) app.waitForDemoReaderState(timeout: 8, description: "reader=opened") { $0.isOpened } } func testPositionSurvivesFontChoiceAndLineHeightChange() throws { app.launchAndOpenSampleBook(resetsReaderState: true) app.waitForReader(timeout: 12) let paging = app.collectionViews[IDs.readerPaging] if paging.waitForExistence(timeout: 5) { paging.swipeLeft() paging.swipeLeft() } let stateBefore = app.waitForDemoReaderState(timeout: 8, description: "翻页后定位稳定") { state in (state.page ?? 0) >= 2 && state.cfi?.isEmpty == false } let pageBefore = stateBefore.page ?? 0 let hrefBefore = stateBefore.href let cfiBefore = stateBefore.cfi XCTAssertGreaterThanOrEqual(pageBefore, 2, "测试前应先进入第 2 页或之后") XCTAssertNotNil(hrefBefore, "重排前应存在当前位置 href") XCTAssertNotNil(cfiBefore, "重排前应存在当前位置 CFI") app.showReaderChromeIfNeeded() XCTAssertTrue(app.buttons[IDs.readerSettings].waitForExistence(timeout: 3)) app.buttons[IDs.readerSettings].tap() let fontChoiceControl = app.segmentedControls[IDs.settingsFontChoice] XCTAssertTrue(fontChoiceControl.waitForExistence(timeout: 5), "字体选择控件应出现") if fontChoiceControl.buttons.count > 1 { fontChoiceControl.buttons.element(boundBy: 1).tap() } let lineHeightControl = app.segmentedControls[IDs.settingsLineHeight] XCTAssertTrue(lineHeightControl.waitForExistence(timeout: 5), "行距控件应出现") if lineHeightControl.buttons.count > 1 { lineHeightControl.buttons.element(boundBy: min(1, lineHeightControl.buttons.count - 1)).tap() } XCTAssertTrue(app.buttons[IDs.settingsDone].waitForExistence(timeout: 3)) app.buttons[IDs.settingsDone].tap() let stateAfter = app.waitForDemoReaderState(timeout: 10, description: "改字体与行距后页码保持") { state in state.page != nil && state.cfi?.isEmpty == false } let pageAfter = stateAfter.page ?? 0 XCTAssertEqual(pageAfter, pageBefore, "字体与行距变化后,应尽量保留当前阅读页") XCTAssertEqual(stateAfter.href, hrefBefore, "重排后应仍定位在同一章节") XCTAssertEqual(stateAfter.cfi, cfiBefore, "重排后应保持当前阅读锚点的 CFI 不变") } func testBookmarkAndHighlightPersistAcrossReopen() throws { app.launchAndOpenSampleBook(resetsReaderState: true) app.waitForReader(timeout: 12) app.showReaderChromeIfNeeded() XCTAssertTrue(app.buttons[IDs.readerBookmark].waitForExistence(timeout: 3)) app.buttons[IDs.readerBookmark].tap() app.waitForDemoReaderState(timeout: 5, description: "bookmarks=1") { $0.bookmarks == 1 } app.hideReaderChromeIfNeeded() let content = app.otherElements[IDs.readerSelectionText].firstMatch XCTAssertTrue(content.waitForExistence(timeout: 5)) let start = content.coordinate(withNormalizedOffset: CGVector(dx: 0.2, dy: 0.5)) let end = content.coordinate(withNormalizedOffset: CGVector(dx: 0.55, dy: 0.5)) start.press(forDuration: 0.8, thenDragTo: end) XCTAssertTrue(app.menuItems["划线"].waitForExistence(timeout: 3)) app.menuItems["划线"].tap() app.waitForDemoReaderState(timeout: 8, description: "highlights=1") { $0.highlights == 1 } app.showReaderChromeIfNeeded() app.buttons[IDs.readerBack].tap() app.launchAndOpenSampleBook(resetsReaderState: false) app.waitForReader(timeout: 12) let state = app.waitForDemoReaderState(timeout: 8, description: "bookmark/highlight restored") { state in state.bookmarks == 1 && state.highlights == 1 } XCTAssertEqual(state.bookmarks, 1) XCTAssertEqual(state.highlights, 1) } func testHighlightNavigationSurvivesFontChoiceChange() throws { app.launchAndOpenSampleBook(resetsReaderState: true, pageNumber: 3) app.waitForReader(timeout: 12) let initialState = app.waitForDemoReaderState(timeout: 8, description: "初始位于第 3 页") { state in state.page == 3 } XCTAssertEqual(initialState.page, 3) app.hideReaderChromeIfNeeded() let selectionText = try app.requireSelectionText(expectedPage: 3) let start = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.25, dy: 0.5)) let end = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.6, dy: 0.5)) start.press(forDuration: 0.8, thenDragTo: end) let highlightItem = app.menuItems["划线"] XCTAssertTrue(highlightItem.waitForExistence(timeout: 3), "应出现高亮菜单") highlightItem.tap() app.waitForDemoReaderState(timeout: 8, description: "highlights=1") { $0.highlights == 1 } app.showReaderChromeIfNeeded() app.buttons[IDs.readerSettings].tap() let fontChoiceControl = app.segmentedControls[IDs.settingsFontChoice] XCTAssertTrue(fontChoiceControl.waitForExistence(timeout: 5), "字体选择控件应出现") if fontChoiceControl.buttons.count > 1 { fontChoiceControl.buttons.element(boundBy: 1).tap() } app.buttons[IDs.settingsDone].tap() let content = app.otherElements[IDs.readerContent].firstMatch if content.waitForExistence(timeout: 3) { content.swipeLeft() content.swipeLeft() Thread.sleep(forTimeInterval: 0.5) } app.showReaderChromeIfNeeded() let highlightsButton = app.buttons[IDs.readerHighlights] XCTAssertTrue(highlightsButton.waitForExistence(timeout: 3), "高亮面板按钮应存在") highlightsButton.tap() let highlightsTable = app.tables[IDs.readerHighlightsTable] XCTAssertTrue(highlightsTable.waitForExistence(timeout: 5), "高亮列表应出现") let firstCell = highlightsTable.cells.firstMatch XCTAssertTrue(firstCell.waitForExistence(timeout: 3), "高亮列表应至少有一个条目") firstCell.tap() let restoredState = app.waitForDemoReaderState(timeout: 10, description: "字体变化后高亮跳回原页") { state in state.page == 3 } XCTAssertEqual(restoredState.page, 3, "字体变化后,通过高亮面板跳转仍应回到原始高亮页") } }