阅读器在多会话朗读、加密 EPUB 资源、并发打开同一本书和多 UserDefaults 容器等场景下,存在旧异步结果覆盖新状态、锁屏控制串会话、大型明文资源被误拒绝、解压半成品被复用及标注跨容器混用的风险;同时高亮、书签、搜索和设置面板的空状态与自动化可访问性入口不完整。\n\n本次为朗读会话引入代次和当前 utterance 校验,远程控制改为仅响应当前会话并按自身 token 清理;加密资源 provider 先判定是否实际返回解密数据,明文大资源继续流式读取;解压缓存串行化以避免并发复用未完成目录。书签与高亮迁移至受保护文件,按 UserDefaults 容器隔离命名空间,保留标准容器旧文件兼容并确保新副本成功落盘后才删除历史数据。\n\n同步调整缓存淘汰、FoundationModels 弱链接、搜索/标注/设置面板交互与 UI 测试,并更新 Pod 生成配置及 API、风险文档。已执行 git diff --check 和 ReadViewDemo Debug Simulator 构建,结果为 BUILD SUCCEEDED。
160 lines
6.6 KiB
Swift
160 lines
6.6 KiB
Swift
import XCTest
|
||
|
||
/// 选区批注动作和高亮持久化测试
|
||
/// 覆盖:批注菜单动作创建高亮、高亮跨重启存活、重排后高亮跳转页码正确
|
||
final class SelectionAnnotateTests: XCTestCase {
|
||
|
||
private let app = XCUIApplication()
|
||
|
||
override func setUpWithError() throws {
|
||
continueAfterFailure = false
|
||
}
|
||
|
||
/// 点击批注菜单项应创建高亮
|
||
func testAnnotateMenuActionCreatesHighlight() throws {
|
||
app.launchAndOpenSampleBook()
|
||
app.waitForReader()
|
||
|
||
// 长按并拖拽选中文本
|
||
let selectionText = app.otherElements[IDs.readerSelectionText].firstMatch
|
||
guard selectionText.waitForExistence(timeout: 5) else {
|
||
throw XCTSkip("文本选区元素不存在")
|
||
}
|
||
|
||
let start = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.3, dy: 0.5))
|
||
let end = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.6, dy: 0.5))
|
||
start.press(forDuration: 0.8, thenDragTo: end)
|
||
|
||
// 等待选区状态
|
||
Thread.sleep(forTimeInterval: 0.5)
|
||
|
||
// 点击批注菜单项
|
||
let annotateItem = app.menuItems["注释"]
|
||
guard annotateItem.waitForExistence(timeout: 3) else {
|
||
throw XCTSkip("批注菜单项不存在")
|
||
}
|
||
annotateItem.tap()
|
||
|
||
// 验证高亮数量变为 1
|
||
app.waitForDemoReaderState(timeout: 5, description: "highlights=1") { $0.highlights == 1 }
|
||
}
|
||
|
||
/// 高亮跨重启存活
|
||
func testHighlightSurvivesRestart() throws {
|
||
app.launchAndOpenSampleBook()
|
||
app.waitForReader()
|
||
|
||
// 创建高亮
|
||
let selectionText = app.otherElements[IDs.readerSelectionText].firstMatch
|
||
guard selectionText.waitForExistence(timeout: 5) else {
|
||
throw XCTSkip("文本选区元素不存在")
|
||
}
|
||
|
||
let start = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.3, dy: 0.5))
|
||
let end = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.6, dy: 0.5))
|
||
start.press(forDuration: 0.8, thenDragTo: end)
|
||
|
||
Thread.sleep(forTimeInterval: 0.5)
|
||
|
||
let annotateItem = app.menuItems["注释"]
|
||
guard annotateItem.waitForExistence(timeout: 3) else {
|
||
throw XCTSkip("批注菜单项不存在")
|
||
}
|
||
annotateItem.tap()
|
||
|
||
// 等待高亮创建完成
|
||
app.waitForDemoReaderState(timeout: 5, description: "highlights=1") { $0.highlights == 1 }
|
||
|
||
// 关闭阅读器
|
||
app.buttons[IDs.readerBack].tap()
|
||
|
||
// 重新打开(不重置状态)
|
||
app.launchAndOpenSampleBook(resetsReaderState: false)
|
||
app.waitForReader()
|
||
|
||
// 验证高亮仍然存在
|
||
app.waitForDemoReaderState(timeout: 8, description: "highlights=1 after restart") { $0.highlights == 1 }
|
||
}
|
||
|
||
/// 重排后高亮跳转页码应正确(多页覆盖)
|
||
/// 回归:chapterOffset(for:) 曾优先使用 row/column 查表而非存储的 chapterOffset,
|
||
/// 导致重新排版后跳转到错误的页面
|
||
func testHighlightNavigationAfterRepagination() throws {
|
||
// 测试多页:在不同页面创建高亮,改变字体后跳转,验证页码正确
|
||
let testPages = [2, 3, 5]
|
||
|
||
for page in testPages {
|
||
// 每轮重新启动以确保干净状态
|
||
app.launchAndOpenSampleBook(pageNumber: page)
|
||
app.waitForReader()
|
||
|
||
let stateBefore = app.waitForDemoReaderState(timeout: 5, description: "at page \(page)") { $0.page == page }
|
||
XCTAssertEqual(stateBefore.page, page, "应成功打开第 \(page) 页")
|
||
|
||
// 创建高亮
|
||
let selectionText = app.otherElements[IDs.readerSelectionText].firstMatch
|
||
guard selectionText.waitForExistence(timeout: 5) else {
|
||
throw XCTSkip("文本选区元素不存在")
|
||
}
|
||
let start = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.2, dy: 0.5))
|
||
let end = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5))
|
||
start.press(forDuration: 0.8, thenDragTo: end)
|
||
Thread.sleep(forTimeInterval: 0.5)
|
||
|
||
let highlightItem = app.menuItems["划线"]
|
||
guard highlightItem.waitForExistence(timeout: 3) else {
|
||
throw XCTSkip("高亮菜单项不存在")
|
||
}
|
||
highlightItem.tap()
|
||
app.waitForDemoReaderState(timeout: 5, description: "highlights=1 on page \(page)") { $0.highlights == 1 }
|
||
|
||
// 改变字体大小触发重新排版
|
||
app.showReaderChromeIfNeeded()
|
||
let settingsButton = app.buttons[IDs.readerSettings]
|
||
XCTAssertTrue(settingsButton.waitForExistence(timeout: 3), "设置按钮应存在")
|
||
settingsButton.tap()
|
||
|
||
XCTAssertTrue(app.adjustSettingsFontSize(toNormalizedPosition: 0.9), "字号滑块应存在")
|
||
Thread.sleep(forTimeInterval: 0.5)
|
||
|
||
let doneButton = app.buttons[IDs.settingsDone]
|
||
if doneButton.waitForExistence(timeout: 3) {
|
||
doneButton.tap()
|
||
}
|
||
Thread.sleep(forTimeInterval: 1)
|
||
|
||
// 翻到其他页
|
||
let content = app.otherElements[IDs.readerContent].firstMatch
|
||
if content.waitForExistence(timeout: 3) {
|
||
for _ in 0..<3 {
|
||
content.swipeLeft()
|
||
Thread.sleep(forTimeInterval: 0.3)
|
||
}
|
||
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()
|
||
|
||
// 等待跳转完成
|
||
Thread.sleep(forTimeInterval: 1)
|
||
|
||
// 验证跳转后的页码应为创建高亮时的页码
|
||
let stateAfter = app.waitForDemoReaderState(timeout: 5, description: "back at page \(page) after repagination") {
|
||
$0.page == page
|
||
}
|
||
XCTAssertEqual(stateAfter.page, page, "重排后高亮跳转应回到第 \(page) 页,实际:\(stateAfter.page ?? -1)")
|
||
}
|
||
}
|
||
}
|