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() let fontIncrease = app.buttons[IDs.settingsFontIncrease] XCTAssertTrue(fontIncrease.waitForExistence(timeout: 3), "字号增大按钮应存在") fontIncrease.tap() fontIncrease.tap() 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)") } } }