Files
ReadViewSDK/ReadViewDemo/ReadViewDemoUITests/ReaderUITests/LocationPersistenceTests.swift
T
shenlei 3e60bf1869 修复阅读器资源、朗读与持久化稳定性
阅读器在多会话朗读、加密 EPUB 资源、并发打开同一本书和多 UserDefaults 容器等场景下,存在旧异步结果覆盖新状态、锁屏控制串会话、大型明文资源被误拒绝、解压半成品被复用及标注跨容器混用的风险;同时高亮、书签、搜索和设置面板的空状态与自动化可访问性入口不完整。\n\n本次为朗读会话引入代次和当前 utterance 校验,远程控制改为仅响应当前会话并按自身 token 清理;加密资源 provider 先判定是否实际返回解密数据,明文大资源继续流式读取;解压缓存串行化以避免并发复用未完成目录。书签与高亮迁移至受保护文件,按 UserDefaults 容器隔离命名空间,保留标准容器旧文件兼容并确保新副本成功落盘后才删除历史数据。\n\n同步调整缓存淘汰、FoundationModels 弱链接、搜索/标注/设置面板交互与 UI 测试,并更新 Pod 生成配置及 API、风险文档。已执行 git diff --check 和 ReadViewDemo Debug Simulator 构建,结果为 BUILD SUCCEEDED。
2026-07-30 20:52:21 +09:00

209 lines
9.2 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import XCTest
final class LocationPersistenceTests: XCTestCase {
private let app = XCUIApplication()
override func setUpWithError() throws {
continueAfterFailure = false
}
func testReadingPositionRestoredOnReopen() throws {
// 必须用滚动模式:仿真翻页(默认)下没有 `epub.reader.paging`
// collectionViewswipeLeft 落不到任何东西,页码永远停在 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, "字体变化后,通过高亮面板跳转仍应回到原始高亮页")
}
}