阅读器在多会话朗读、加密 EPUB 资源、并发打开同一本书和多 UserDefaults 容器等场景下,存在旧异步结果覆盖新状态、锁屏控制串会话、大型明文资源被误拒绝、解压半成品被复用及标注跨容器混用的风险;同时高亮、书签、搜索和设置面板的空状态与自动化可访问性入口不完整。\n\n本次为朗读会话引入代次和当前 utterance 校验,远程控制改为仅响应当前会话并按自身 token 清理;加密资源 provider 先判定是否实际返回解密数据,明文大资源继续流式读取;解压缓存串行化以避免并发复用未完成目录。书签与高亮迁移至受保护文件,按 UserDefaults 容器隔离命名空间,保留标准容器旧文件兼容并确保新副本成功落盘后才删除历史数据。\n\n同步调整缓存淘汰、FoundationModels 弱链接、搜索/标注/设置面板交互与 UI 测试,并更新 Pod 生成配置及 API、风险文档。已执行 git diff --check 和 ReadViewDemo Debug Simulator 构建,结果为 BUILD SUCCEEDED。
209 lines
9.2 KiB
Swift
209 lines
9.2 KiB
Swift
import XCTest
|
||
|
||
final class LocationPersistenceTests: XCTestCase {
|
||
private let app = XCUIApplication()
|
||
|
||
override func setUpWithError() throws {
|
||
continueAfterFailure = false
|
||
}
|
||
|
||
func testReadingPositionRestoredOnReopen() throws {
|
||
// 必须用滚动模式:仿真翻页(默认)下没有 `epub.reader.paging`
|
||
// collectionView,swipeLeft 落不到任何东西,页码永远停在 1
|
||
app.launchAndOpenSampleBook(displayType: "scroll")
|
||
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() }
|
||
|
||
// 重开必须保留状态,否则 --demo-reset-state 会清掉刚存的阅读位置
|
||
app.launchAndOpenSampleBook(displayType: "scroll", resetsReaderState: false)
|
||
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() }
|
||
app.increaseSettingsFontSize()
|
||
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(displayType: "scroll", 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()
|
||
|
||
XCTAssertTrue(
|
||
app.selectSettingsPillOption(pillIdentifier: IDs.settingsFontChoice, optionTitle: "宋体"),
|
||
"字体选择控件应可切换"
|
||
)
|
||
XCTAssertTrue(
|
||
app.selectSettingsPillOption(pillIdentifier: IDs.settingsLineHeight, optionTitle: "标准"),
|
||
"行距控件应可切换"
|
||
)
|
||
|
||
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 = try app.requireSelectionText()
|
||
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(pageNumber: 3, resetsReaderState: true)
|
||
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()
|
||
XCTAssertTrue(
|
||
app.selectSettingsPillOption(pillIdentifier: IDs.settingsFontChoice, optionTitle: "宋体"),
|
||
"字体选择控件应可切换"
|
||
)
|
||
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, "字体变化后,通过高亮面板跳转仍应回到原始高亮页")
|
||
}
|
||
}
|