feat: EPUB 阅读器搜索、选中注释、书签 chrome 状态及大量重构优化
- 新增 RDEPUBReaderSearchCoordinator 与 RDEPUBSelectionState 管理搜索和选中状态 - 新增 BookmarkChromeStateTests、NavigationBackwardTests、SelectionAnnotateTests 等 UI 测试 - 新增多个边界测试 epub 样本(损坏结构、空归档、缺失文件、流式外链验证) - 重构阅读器 chrome 状态管理,统一 tool bar 与 search bar 交互 - 优化大书分页缓存策略(RDEPUBChapterSummaryDiskCache、RDEPUBPageCountCache) - 移除废弃的 RDEPUBLocationConverter 和 RDEPUBPageBreakPolicy - 更新 epub-bridge.js 与 JS bridge 通信协议 - 全面更新现有 UI 测试以适配新的 helper 和状态管理
This commit is contained in:
@@ -40,6 +40,16 @@ struct DemoReaderState {
|
||||
var windowSize: Int? { fields["windowSize"].flatMap(Int.init) }
|
||||
var parseMs: Int? { fields["parseMs"].flatMap(Int.init) }
|
||||
var parseConcurrency: Int? { fields["parseConcurrency"].flatMap(Int.init) }
|
||||
var inspectable: Int? { fields["inspectable"].flatMap(Int.init) }
|
||||
var streamedResources: Int? { fields["streamedResources"].flatMap(Int.init) }
|
||||
var inMemoryResources: Int? { fields["inMemoryResources"].flatMap(Int.init) }
|
||||
var resourceFailures: Int? { fields["resourceFailures"].flatMap(Int.init) }
|
||||
var cacheFiles: Int? { fields["cacheFiles"].flatMap(Int.init) }
|
||||
var cacheBytes: Int? { fields["cacheBytes"].flatMap(Int.init) }
|
||||
var externalLinks: Int? { fields["externalLinks"].flatMap(Int.init) }
|
||||
var lastExternalURL: String? { fields["lastExternalURL"] }
|
||||
var lastError: String? { fields["lastError"] }
|
||||
var searchMatchText: String? { fields["searchMatchText"] }
|
||||
}
|
||||
|
||||
extension XCUIApplication {
|
||||
|
||||
@@ -9,6 +9,9 @@ extension XCUIApplication {
|
||||
windowSize: Int? = nil,
|
||||
concurrency: Int? = nil,
|
||||
clearsCache: Bool = false,
|
||||
corruptsCache: Bool = false,
|
||||
allowsInspectableWebViews: Bool = false,
|
||||
enablesVerboseWebLogs: Bool = false,
|
||||
searchKeyword: String? = nil
|
||||
) {
|
||||
var args = ["--demo-book-title", bookTitleQuery]
|
||||
@@ -18,6 +21,15 @@ extension XCUIApplication {
|
||||
if clearsCache {
|
||||
args.append("--demo-clear-cache")
|
||||
}
|
||||
if corruptsCache {
|
||||
args.append("--demo-corrupt-cache")
|
||||
}
|
||||
if allowsInspectableWebViews {
|
||||
args.append("--demo-allow-inspectable-webviews")
|
||||
}
|
||||
if enablesVerboseWebLogs {
|
||||
args.append("--demo-enable-verbose-weblogs")
|
||||
}
|
||||
if let displayType {
|
||||
args += ["--demo-display-type", displayType]
|
||||
}
|
||||
@@ -52,16 +64,27 @@ extension XCUIApplication {
|
||||
}
|
||||
}
|
||||
|
||||
func showReaderChromeIfNeeded() {
|
||||
if buttons[IDs.readerBack].exists && buttons[IDs.readerSettings].exists {
|
||||
func showReaderChromeIfNeeded(
|
||||
requiredButtonIDs: [String] = [IDs.readerBack, IDs.readerSettings],
|
||||
timeout: TimeInterval = 5
|
||||
) {
|
||||
if readerChromeIsVisible(requiredButtonIDs: requiredButtonIDs) {
|
||||
return
|
||||
}
|
||||
|
||||
let content = otherElements[IDs.readerContent]
|
||||
if content.waitForExistence(timeout: 3) {
|
||||
content.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
|
||||
} else {
|
||||
windows.firstMatch.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
|
||||
let deadline = Date().addingTimeInterval(timeout)
|
||||
while Date() < deadline {
|
||||
let content = otherElements[IDs.readerContent]
|
||||
if content.waitForExistence(timeout: 1) {
|
||||
content.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
|
||||
} else {
|
||||
windows.firstMatch.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
|
||||
}
|
||||
|
||||
RunLoop.current.run(until: Date().addingTimeInterval(0.35))
|
||||
if readerChromeIsVisible(requiredButtonIDs: requiredButtonIDs) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,4 +117,39 @@ extension XCUIApplication {
|
||||
|
||||
XCTFail("阅读器状态未包含 \(expectedText),当前:\(lastLabel)")
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func requireSelectionText(
|
||||
expectedPage: Int? = nil,
|
||||
timeout: TimeInterval = 12
|
||||
) throws -> XCUIElement {
|
||||
let selectionText = otherElements[IDs.readerSelectionText].firstMatch
|
||||
let deadline = Date().addingTimeInterval(timeout)
|
||||
|
||||
while Date() < deadline {
|
||||
if let expectedPage {
|
||||
let state = currentDemoReaderState()
|
||||
if state?.page != expectedPage {
|
||||
RunLoop.current.run(until: Date().addingTimeInterval(0.1))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if selectionText.exists {
|
||||
return selectionText
|
||||
}
|
||||
|
||||
let content = otherElements[IDs.readerContent]
|
||||
if content.exists {
|
||||
content.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
|
||||
}
|
||||
RunLoop.current.run(until: Date().addingTimeInterval(0.2))
|
||||
}
|
||||
|
||||
throw XCTSkip("文本选区元素未出现,当前渲染路径未暴露 \(IDs.readerSelectionText)")
|
||||
}
|
||||
|
||||
private func readerChromeIsVisible(requiredButtonIDs: [String]) -> Bool {
|
||||
requiredButtonIDs.allSatisfy { buttons[$0].exists }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import XCTest
|
||||
|
||||
/// 书签 chrome 状态测试
|
||||
/// 覆盖:书签列表按钮在零书签时禁用、添加后启用、书签跨重启存活
|
||||
final class BookmarkChromeStateTests: XCTestCase {
|
||||
|
||||
private let app = XCUIApplication()
|
||||
|
||||
override func setUpWithError() throws {
|
||||
continueAfterFailure = false
|
||||
}
|
||||
|
||||
/// 零书签时,底部书签列表按钮应禁用
|
||||
func testBookmarksListButtonDisabledWhenNoBookmarks() throws {
|
||||
app.launchAndOpenSampleBook()
|
||||
app.waitForReader()
|
||||
|
||||
app.showReaderChromeIfNeeded()
|
||||
|
||||
let bookmarksButton = app.buttons[IDs.readerBookmarks]
|
||||
guard bookmarksButton.waitForExistence(timeout: 3) else {
|
||||
throw XCTSkip("书签列表按钮不存在")
|
||||
}
|
||||
// 零书签时按钮应存在但不可用
|
||||
XCTAssertFalse(bookmarksButton.isEnabled, "零书签时书签列表按钮应禁用")
|
||||
}
|
||||
|
||||
/// 添加书签后,底部书签列表按钮应启用
|
||||
func testBookmarksListButtonEnabledAfterAdding() throws {
|
||||
app.launchAndOpenSampleBook()
|
||||
app.waitForReader()
|
||||
|
||||
app.showReaderChromeIfNeeded()
|
||||
|
||||
// 添加书签
|
||||
let bookmarkButton = app.buttons[IDs.readerBookmark]
|
||||
guard bookmarkButton.waitForExistence(timeout: 3) else {
|
||||
throw XCTSkip("书签按钮不存在")
|
||||
}
|
||||
bookmarkButton.tap()
|
||||
|
||||
// 验证书签列表按钮启用
|
||||
let bookmarksButton = app.buttons[IDs.readerBookmarks]
|
||||
guard bookmarksButton.waitForExistence(timeout: 3) else {
|
||||
throw XCTSkip("书签列表按钮不存在")
|
||||
}
|
||||
XCTAssertTrue(bookmarksButton.isEnabled, "添加书签后书签列表按钮应启用")
|
||||
}
|
||||
|
||||
/// 书签跨重启存活
|
||||
func testBookmarkSurvivesRestart() {
|
||||
app.launchAndOpenSampleBook()
|
||||
app.waitForReader()
|
||||
|
||||
// 添加书签
|
||||
app.showReaderChromeIfNeeded()
|
||||
let bookmarkButton = app.buttons[IDs.readerBookmark]
|
||||
bookmarkButton.waitForExistence(timeout: 3)
|
||||
bookmarkButton.tap()
|
||||
|
||||
// 等待书签状态确认
|
||||
app.waitForDemoReaderState(timeout: 5, description: "bookmarks=1") { $0.bookmarks == 1 }
|
||||
|
||||
// 关闭阅读器
|
||||
app.buttons[IDs.readerBack].tap()
|
||||
|
||||
// 重新打开(不重置状态)
|
||||
app.launchAndOpenSampleBook(resetsReaderState: false)
|
||||
app.waitForReader()
|
||||
|
||||
// 验证书签仍然存在
|
||||
app.waitForDemoReaderState(timeout: 5, description: "bookmarks=1 after restart") { $0.bookmarks == 1 }
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ final class BookmarkManagementTests: XCTestCase {
|
||||
let bookmarkButton = app.buttons[IDs.readerBookmark]
|
||||
XCTAssertTrue(bookmarkButton.waitForExistence(timeout: 3), "书签按钮不存在")
|
||||
bookmarkButton.tap()
|
||||
app.waitForReaderState(containing: "bookmarks=1", timeout: 5)
|
||||
app.waitForDemoReaderState(timeout: 5, description: "bookmarks=1") { $0.bookmarks == 1 }
|
||||
|
||||
app.showReaderChromeIfNeeded()
|
||||
let bookmarksButton = app.buttons[IDs.readerBookmarks]
|
||||
@@ -47,7 +47,7 @@ final class BookmarkManagementTests: XCTestCase {
|
||||
|
||||
app.showReaderChromeIfNeeded()
|
||||
app.buttons[IDs.readerBookmark].tap()
|
||||
app.waitForReaderState(containing: "bookmarks=1", timeout: 5)
|
||||
app.waitForDemoReaderState(timeout: 5, description: "bookmarks=1") { $0.bookmarks == 1 }
|
||||
app.showReaderChromeIfNeeded()
|
||||
app.buttons[IDs.readerBookmarks].tap()
|
||||
|
||||
@@ -78,6 +78,39 @@ final class BookmarkManagementTests: XCTestCase {
|
||||
XCTAssertTrue(bookmarkButton.waitForExistence(timeout: 3), "移除书签后按钮应仍然存在")
|
||||
}
|
||||
|
||||
func testBookmarksButtonDisabledWhenNoBookmarks() throws {
|
||||
app.launchAndOpenSampleBook(resetsReaderState: true)
|
||||
app.waitForReader(timeout: 12)
|
||||
|
||||
app.showReaderChromeIfNeeded()
|
||||
let bookmarksButton = app.buttons[IDs.readerBookmarks]
|
||||
XCTAssertTrue(bookmarksButton.waitForExistence(timeout: 3), "书签列表按钮不存在")
|
||||
XCTAssertFalse(bookmarksButton.isEnabled, "零书签时书签列表按钮应禁用")
|
||||
}
|
||||
|
||||
func testBookmarkIndicatorStaysSelectedAtBookmarkedLocation() throws {
|
||||
app.launchAndOpenSampleBook(resetsReaderState: true)
|
||||
app.waitForReader(timeout: 12)
|
||||
|
||||
app.showReaderChromeIfNeeded()
|
||||
let bookmarkButton = app.buttons[IDs.readerBookmark]
|
||||
XCTAssertTrue(bookmarkButton.waitForExistence(timeout: 3), "书签按钮不存在")
|
||||
bookmarkButton.tap()
|
||||
app.waitForDemoReaderState(timeout: 5, description: "bookmarks=1") { $0.bookmarks == 1 }
|
||||
XCTAssertEqual(bookmarkButton.value as? String, "selected", "当前页已加书签时顶部书签按钮应为选中态")
|
||||
|
||||
let paging = app.collectionViews[IDs.readerPaging]
|
||||
if paging.waitForExistence(timeout: 5) {
|
||||
paging.swipeLeft()
|
||||
app.showReaderChromeIfNeeded()
|
||||
XCTAssertEqual(bookmarkButton.value as? String, "unselected", "离开书签页后顶部书签按钮应取消选中")
|
||||
|
||||
paging.swipeRight()
|
||||
app.showReaderChromeIfNeeded()
|
||||
XCTAssertEqual(bookmarkButton.value as? String, "selected", "返回书签页后顶部书签按钮应恢复选中")
|
||||
}
|
||||
}
|
||||
|
||||
private func waitForCellCount(
|
||||
in table: XCUIElement,
|
||||
minimum: Int,
|
||||
|
||||
@@ -15,12 +15,37 @@ final class DisplayTypeTests: XCTestCase {
|
||||
func testOpenWithHorizontalScroll() {
|
||||
app.launchAndOpenSampleBook(displayType: "scroll")
|
||||
app.waitForReader()
|
||||
app.waitForReaderState(containing: "display=horizontalScroll")
|
||||
app.waitForDemoReaderState(timeout: 8, description: "display=horizontalScroll") { $0.display == "horizontalScroll" }
|
||||
}
|
||||
|
||||
func testOpenWithVerticalScroll() {
|
||||
app.launchAndOpenSampleBook(displayType: "vertical")
|
||||
app.waitForReader()
|
||||
app.waitForReaderState(containing: "display=verticalScroll")
|
||||
app.waitForDemoReaderState(timeout: 8, description: "display=verticalScroll") { $0.display == "verticalScroll" }
|
||||
}
|
||||
|
||||
func testInspectableWebViewsDisabledByDefault() {
|
||||
app.launchAndOpenSampleBook(bookTitleQuery: "Web交互流式外链验证")
|
||||
app.waitForReader()
|
||||
|
||||
let state = app.waitForDemoReaderState(timeout: 8, description: "inspectable=0") { $0.inspectable != nil }
|
||||
XCTAssertEqual(state.inspectable, 0, "默认配置下 WebView inspectable 应关闭")
|
||||
}
|
||||
|
||||
func testDisplayTypeSwitchMidSession() {
|
||||
app.launchAndOpenSampleBook(displayType: "scroll")
|
||||
app.waitForReader()
|
||||
app.waitForDemoReaderState(timeout: 8, description: "display=horizontalScroll") { $0.display == "horizontalScroll" }
|
||||
|
||||
app.showReaderChromeIfNeeded()
|
||||
XCTAssertTrue(app.buttons[IDs.readerSettings].waitForExistence(timeout: 3))
|
||||
app.buttons[IDs.readerSettings].tap()
|
||||
|
||||
let displayTypeControl = app.segmentedControls[IDs.settingsDisplayType]
|
||||
XCTAssertTrue(displayTypeControl.waitForExistence(timeout: 5))
|
||||
displayTypeControl.buttons.element(boundBy: 2).tap()
|
||||
app.buttons[IDs.settingsDone].tap()
|
||||
|
||||
app.waitForDemoReaderState(timeout: 8, description: "display=verticalScroll after switch") { $0.display == "verticalScroll" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ final class ErrorAndEdgeCaseTests: XCTestCase {
|
||||
let paging = app.collectionViews[IDs.readerPaging]
|
||||
if paging.waitForExistence(timeout: 5) { paging.swipeUp() }
|
||||
|
||||
app.waitForReaderState(containing: "reader=opened", timeout: 5)
|
||||
app.waitForDemoReaderState(timeout: 5, description: "reader=opened") { $0.isOpened }
|
||||
}
|
||||
|
||||
func testOpenAndCloseMultipleTimes() throws {
|
||||
@@ -42,7 +42,7 @@ final class ErrorAndEdgeCaseTests: XCTestCase {
|
||||
|
||||
for _ in 1...5 { paging.swipeLeft() }
|
||||
|
||||
app.waitForReaderState(containing: "reader=opened", timeout: 8)
|
||||
app.waitForDemoReaderState(timeout: 8, description: "reader=opened") { $0.isOpened }
|
||||
}
|
||||
|
||||
func testSettingsPanelScrollReachesAllControls() throws {
|
||||
@@ -66,4 +66,85 @@ final class ErrorAndEdgeCaseTests: XCTestCase {
|
||||
|
||||
app.buttons[IDs.settingsDone].tap()
|
||||
}
|
||||
}
|
||||
|
||||
func testHTTPSExternalLinkShowsConfirmationAlert() throws {
|
||||
app.launchAndOpenSampleBook(bookTitleQuery: "Web交互流式外链验证", displayType: "scroll")
|
||||
app.waitForReader(timeout: 20)
|
||||
|
||||
let httpsLink = externalLinkElement(named: "HTTPS Link")
|
||||
guard httpsLink.waitForExistence(timeout: 8) else {
|
||||
throw XCTSkip("HTTPS 外链元素未暴露")
|
||||
}
|
||||
httpsLink.tap()
|
||||
|
||||
XCTAssertTrue(app.alerts["打开外部链接"].waitForExistence(timeout: 5), "HTTPS 外链应弹出确认框")
|
||||
XCTAssertTrue(app.buttons["打开"].waitForExistence(timeout: 2), "确认框应提供打开按钮")
|
||||
app.buttons["取消"].tap()
|
||||
}
|
||||
|
||||
func testMailtoExternalLinkBlockedByDefaultPolicy() throws {
|
||||
app.launchAndOpenSampleBook(bookTitleQuery: "Web交互流式外链验证", displayType: "scroll")
|
||||
app.waitForReader(timeout: 20)
|
||||
|
||||
let mailtoLink = externalLinkElement(named: "Mailto Link")
|
||||
guard mailtoLink.waitForExistence(timeout: 8) else {
|
||||
throw XCTSkip("Mailto 外链元素未暴露")
|
||||
}
|
||||
mailtoLink.tap()
|
||||
|
||||
XCTAssertFalse(app.alerts["打开外部链接"].waitForExistence(timeout: 2), "默认策略下 mailto 不应弹出确认框")
|
||||
let state = app.waitForDemoReaderState(timeout: 5, description: "mailto activation recorded") { state in
|
||||
(state.externalLinks ?? 0) >= 1 && state.lastExternalURL != nil
|
||||
}
|
||||
XCTAssertTrue(state.lastExternalURL?.contains("mailto:test@example.com") == true, "应记录 mailto 激活但不继续打开")
|
||||
}
|
||||
|
||||
func testLargeWebResourceUsesStreamingPath() throws {
|
||||
app.launchAndOpenSampleBook(bookTitleQuery: "Web交互流式外链验证", displayType: "scroll")
|
||||
app.waitForReader(timeout: 20)
|
||||
|
||||
let state = app.waitForDemoReaderState(timeout: 15, description: "streamedResources>0") { state in
|
||||
(state.streamedResources ?? 0) > 0
|
||||
}
|
||||
XCTAssertGreaterThan(state.streamedResources ?? 0, 0, "大资源应走流式传输路径")
|
||||
}
|
||||
|
||||
func testInvalidEPUBReportsFailure() {
|
||||
app.launchAndOpenSampleBook(bookTitleQuery: "损坏结构样本", resetsReaderState: true)
|
||||
|
||||
let state = app.waitForDemoReaderState(timeout: 12, description: "invalid epub failure") { state in
|
||||
state.lastError != nil && state.lastError != "none"
|
||||
}
|
||||
XCTAssertNotEqual(state.lastError, "none")
|
||||
}
|
||||
|
||||
func testEmptyArchiveReportsFailure() {
|
||||
app.launchAndOpenSampleBook(bookTitleQuery: "空归档样本", resetsReaderState: true)
|
||||
|
||||
let state = app.waitForDemoReaderState(timeout: 12, description: "empty archive failure") { state in
|
||||
state.lastError != nil && state.lastError != "none"
|
||||
}
|
||||
XCTAssertNotEqual(state.lastError, "none")
|
||||
}
|
||||
|
||||
func testMissingSpineDocumentReportsFailure() {
|
||||
app.launchAndOpenSampleBook(bookTitleQuery: "缺失文件样本", resetsReaderState: true)
|
||||
|
||||
let state = app.waitForDemoReaderState(timeout: 12, description: "missing file failure") { state in
|
||||
state.lastError != nil && state.lastError != "none"
|
||||
}
|
||||
XCTAssertNotEqual(state.lastError, "none")
|
||||
}
|
||||
|
||||
private func externalLinkElement(named label: String) -> XCUIElement {
|
||||
let link = app.links[label]
|
||||
if link.exists {
|
||||
return link
|
||||
}
|
||||
let staticText = app.staticTexts[label]
|
||||
if staticText.exists {
|
||||
return staticText
|
||||
}
|
||||
return link
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ final class HighlightsManagementTests: XCTestCase {
|
||||
if highlightMenuItem.waitForExistence(timeout: 3) { highlightMenuItem.tap() }
|
||||
else if app.buttons["高亮"].waitForExistence(timeout: 2) { app.buttons["高亮"].tap() }
|
||||
|
||||
app.waitForReaderState(containing: "highlights=", timeout: 5)
|
||||
app.waitForDemoReaderState(timeout: 5, description: "highlights exists") { $0.highlights != nil }
|
||||
|
||||
app.showReaderChromeIfNeeded()
|
||||
XCTAssertTrue(app.buttons[IDs.readerHighlights].waitForExistence(timeout: 3), "高亮列表按钮不存在")
|
||||
|
||||
@@ -50,9 +50,10 @@ final class LargeBookOnDemandTests: XCTestCase {
|
||||
app.launchAndOpenSampleBook(bookTitleQuery: largeBookQuery, resetsReaderState: true)
|
||||
app.waitForReader(timeout: 20)
|
||||
|
||||
_ = app.waitForDemoReaderState(timeout: 60, description: "首次打开完成全书缓存") { state in
|
||||
let firstFullState = app.waitForDemoReaderState(timeout: 60, description: "首次打开完成全书缓存") { state in
|
||||
state.mode == "bookPageMap" && state.pagination == "full" && (state.buildableChapters ?? 0) > 0
|
||||
}
|
||||
XCTAssertGreaterThan(firstFullState.cacheFiles ?? 0, 0, "首次全量解析后应生成章节摘要缓存")
|
||||
|
||||
app.showReaderChromeIfNeeded()
|
||||
XCTAssertTrue(app.buttons[IDs.readerBack].waitForExistence(timeout: 5), "返回按钮不存在")
|
||||
@@ -68,6 +69,47 @@ final class LargeBookOnDemandTests: XCTestCase {
|
||||
|
||||
XCTAssertEqual(reopenedState.pagination, "full")
|
||||
XCTAssertEqual(reopenedState.knownChapters, reopenedState.buildableChapters)
|
||||
XCTAssertEqual(reopenedState.knownPages, firstFullState.knownPages, "二开命中缓存后总页数应稳定一致")
|
||||
}
|
||||
|
||||
func testCorruptedCacheFallsBackGracefully() throws {
|
||||
app.launchAndOpenSampleBook(bookTitleQuery: largeBookQuery, resetsReaderState: true, clearsCache: true)
|
||||
app.waitForReader(timeout: 20)
|
||||
_ = app.waitForDemoReaderState(timeout: 60, description: "首次打开完成缓存建立") { state in
|
||||
state.pagination == "full" && (state.cacheFiles ?? 0) > 0
|
||||
}
|
||||
|
||||
app.showReaderChromeIfNeeded()
|
||||
app.buttons[IDs.readerBack].tap()
|
||||
XCTAssertTrue(app.tables[IDs.demoBooksTable].waitForExistence(timeout: 5))
|
||||
|
||||
app.launchAndOpenSampleBook(bookTitleQuery: largeBookQuery, resetsReaderState: false, corruptsCache: true)
|
||||
app.waitForReader(timeout: 20)
|
||||
|
||||
let recoveredState = app.waitForDemoReaderState(timeout: 45, description: "损坏缓存后仍可恢复阅读") { state in
|
||||
state.isOpened && state.pagination != "none" && state.mode == "bookPageMap"
|
||||
}
|
||||
XCTAssertEqual(recoveredState.lastError, "none", "缓存损坏不应导致阅读器进入错误状态")
|
||||
}
|
||||
|
||||
func testClearingCacheForcesRebuild() throws {
|
||||
app.launchAndOpenSampleBook(bookTitleQuery: largeBookQuery, resetsReaderState: true, clearsCache: true)
|
||||
app.waitForReader(timeout: 20)
|
||||
_ = app.waitForDemoReaderState(timeout: 60, description: "首次缓存建立") { state in
|
||||
state.pagination == "full" && (state.cacheFiles ?? 0) > 0
|
||||
}
|
||||
|
||||
app.showReaderChromeIfNeeded()
|
||||
app.buttons[IDs.readerBack].tap()
|
||||
XCTAssertTrue(app.tables[IDs.demoBooksTable].waitForExistence(timeout: 5))
|
||||
|
||||
app.launchAndOpenSampleBook(bookTitleQuery: largeBookQuery, resetsReaderState: false, clearsCache: true)
|
||||
app.waitForReader(timeout: 20)
|
||||
|
||||
let rebuiltState = app.waitForDemoReaderState(timeout: 60, description: "清空缓存后重新构建") { state in
|
||||
state.pagination == "full" && (state.cacheFiles ?? 0) > 0
|
||||
}
|
||||
XCTAssertGreaterThan(rebuiltState.cacheFiles ?? 0, 0, "清空缓存后应重新写入缓存文件")
|
||||
}
|
||||
|
||||
func testMiddleChapterOpenPositionStableAfterFullParse() throws {
|
||||
@@ -146,7 +188,7 @@ final class LargeBookOnDemandTests: XCTestCase {
|
||||
func testLargeBookContinuousPagingExtendsKnownPageMap() throws {
|
||||
app.launchAndOpenSampleBook(bookTitleQuery: largeBookQuery, displayType: "scroll", resetsReaderState: true)
|
||||
app.waitForReader(timeout: 20)
|
||||
app.waitForReaderState(containing: "display=horizontalScroll", timeout: 8)
|
||||
app.waitForDemoReaderState(timeout: 8, description: "display=horizontalScroll") { $0.display == "horizontalScroll" }
|
||||
|
||||
let initialState = app.waitForDemoReaderState(timeout: 15, description: "初始局部分页") { state in
|
||||
state.mode == "bookPageMap" && state.knownPages != nil && state.page != nil
|
||||
|
||||
@@ -57,7 +57,7 @@ final class LocationPersistenceTests: XCTestCase {
|
||||
|
||||
let paging = app.collectionViews[IDs.readerPaging]
|
||||
if paging.waitForExistence(timeout: 5) { paging.swipeLeft() }
|
||||
app.waitForReaderState(containing: "page=", timeout: 5)
|
||||
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() }
|
||||
@@ -70,6 +70,37 @@ final class LocationPersistenceTests: XCTestCase {
|
||||
app.launchAndOpenSampleBook()
|
||||
app.waitForReader(timeout: 12)
|
||||
|
||||
app.waitForReaderState(containing: "reader=opened", timeout: 8)
|
||||
app.waitForDemoReaderState(timeout: 8, description: "reader=opened") { $0.isOpened }
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import XCTest
|
||||
|
||||
/// 向后导航和显示模式切换测试
|
||||
/// 覆盖:向右滑动回到上一页、阅读中途切换显示模式
|
||||
final class NavigationBackwardTests: XCTestCase {
|
||||
|
||||
private let app = XCUIApplication()
|
||||
|
||||
override func setUpWithError() throws {
|
||||
continueAfterFailure = false
|
||||
}
|
||||
|
||||
/// 向右滑动应返回上一页
|
||||
func testSwipeRightGoesToPreviousPage() throws {
|
||||
app.launchAndOpenSampleBook()
|
||||
app.waitForReader()
|
||||
|
||||
// 先向左滑动两次前进
|
||||
let content = app.otherElements[IDs.readerContent].firstMatch
|
||||
guard content.waitForExistence(timeout: 5) else {
|
||||
throw XCTSkip("内容区域不存在")
|
||||
}
|
||||
|
||||
content.swipeLeft()
|
||||
Thread.sleep(forTimeInterval: 0.5)
|
||||
content.swipeLeft()
|
||||
Thread.sleep(forTimeInterval: 0.5)
|
||||
|
||||
// 记录当前页码
|
||||
let stateAfterForward = app.waitForDemoReaderState(timeout: 5, description: "page after forward") {
|
||||
($0.page ?? 0) > 1
|
||||
}
|
||||
let pageAfterForward = stateAfterForward.page ?? 2
|
||||
XCTAssertGreaterThan(pageAfterForward, 1, "应已前进到第 2 页或更后")
|
||||
|
||||
// 向右滑动返回
|
||||
content.swipeRight()
|
||||
Thread.sleep(forTimeInterval: 0.5)
|
||||
|
||||
// 验证页码减少
|
||||
let stateAfterBack = app.waitForDemoReaderState(timeout: 5, description: "page after back") {
|
||||
($0.page ?? 0) < pageAfterForward
|
||||
}
|
||||
XCTAssertLessThan(stateAfterBack.page ?? 0, pageAfterForward, "向右滑动后页码应减少")
|
||||
}
|
||||
|
||||
/// 阅读中途切换显示模式
|
||||
func testDisplayTypeSwitchMidSession() throws {
|
||||
app.launchAndOpenSampleBook(displayType: "horizontalScroll")
|
||||
app.waitForReader()
|
||||
|
||||
// 确认初始模式
|
||||
app.waitForDemoReaderState(timeout: 5, description: "initial horizontalScroll") {
|
||||
$0.display == "horizontalScroll"
|
||||
}
|
||||
|
||||
// 打开设置面板
|
||||
app.showReaderChromeIfNeeded()
|
||||
let settingsButton = app.buttons[IDs.readerSettings]
|
||||
guard settingsButton.waitForExistence(timeout: 3) else {
|
||||
throw XCTSkip("设置按钮不存在")
|
||||
}
|
||||
settingsButton.tap()
|
||||
|
||||
// 切换到垂直滚动
|
||||
let displayTypeButton = app.buttons[IDs.settingsDisplayType]
|
||||
guard displayTypeButton.waitForExistence(timeout: 3) else {
|
||||
throw XCTSkip("显示模式按钮不存在")
|
||||
}
|
||||
displayTypeButton.tap()
|
||||
|
||||
// 关闭设置
|
||||
let doneButton = app.buttons[IDs.settingsDone]
|
||||
if doneButton.waitForExistence(timeout: 3) {
|
||||
doneButton.tap()
|
||||
}
|
||||
|
||||
// 验证显示模式已切换
|
||||
app.waitForDemoReaderState(timeout: 5, description: "switched to verticalScroll") {
|
||||
$0.display == "verticalScroll"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ final class PageNavigationTests: XCTestCase {
|
||||
func testPagingViewExistsInScrollMode() {
|
||||
app.launchAndOpenSampleBook(displayType: "scroll")
|
||||
app.waitForReader()
|
||||
app.waitForReaderState(containing: "display=horizontalScroll", timeout: 5)
|
||||
app.waitForDemoReaderState(timeout: 5, description: "display=horizontalScroll") { $0.display == "horizontalScroll" }
|
||||
|
||||
let paging = app.collectionViews[IDs.readerPaging]
|
||||
XCTAssertTrue(paging.waitForExistence(timeout: 5), "分页滚动视图未出现")
|
||||
@@ -27,7 +27,7 @@ final class PageNavigationTests: XCTestCase {
|
||||
func testPagingViewExistsInVerticalScrollMode() {
|
||||
app.launchAndOpenSampleBook(displayType: "vertical")
|
||||
app.waitForReader()
|
||||
app.waitForReaderState(containing: "display=verticalScroll", timeout: 5)
|
||||
app.waitForDemoReaderState(timeout: 5, description: "display=verticalScroll") { $0.display == "verticalScroll" }
|
||||
|
||||
let paging = app.collectionViews[IDs.readerPaging]
|
||||
XCTAssertTrue(paging.waitForExistence(timeout: 5), "分页滚动视图未出现")
|
||||
@@ -36,7 +36,7 @@ final class PageNavigationTests: XCTestCase {
|
||||
func testSwipeInHorizontalScrollMode() {
|
||||
app.launchAndOpenSampleBook(displayType: "scroll")
|
||||
app.waitForReader()
|
||||
app.waitForReaderState(containing: "display=horizontalScroll", timeout: 5)
|
||||
app.waitForDemoReaderState(timeout: 5, description: "display=horizontalScroll") { $0.display == "horizontalScroll" }
|
||||
|
||||
let paging = app.collectionViews[IDs.readerPaging]
|
||||
XCTAssertTrue(paging.waitForExistence(timeout: 5))
|
||||
@@ -51,4 +51,26 @@ final class PageNavigationTests: XCTestCase {
|
||||
}
|
||||
XCTAssertNotEqual(beforePage, afterState.page, "水平滑动后页码未变化")
|
||||
}
|
||||
|
||||
func testSwipeRightReturnsToPreviousPage() {
|
||||
app.launchAndOpenSampleBook(displayType: "scroll", pageNumber: 3)
|
||||
app.waitForReader()
|
||||
app.waitForDemoReaderState(timeout: 8, description: "page=3") { $0.page == 3 }
|
||||
|
||||
let paging = app.collectionViews[IDs.readerPaging]
|
||||
XCTAssertTrue(paging.waitForExistence(timeout: 5))
|
||||
paging.swipeRight()
|
||||
|
||||
let state = app.waitForDemoReaderState(timeout: 8, description: "returned to previous page") { state in
|
||||
guard let page = state.page else { return false }
|
||||
return page < 3
|
||||
}
|
||||
XCTAssertEqual(state.page, 2, "向右滑动后应回到上一页")
|
||||
}
|
||||
|
||||
func testLaunchAtSpecificPageNumber() {
|
||||
app.launchAndOpenSampleBook(pageNumber: 4)
|
||||
app.waitForReader()
|
||||
app.waitForDemoReaderState(timeout: 8, description: "page=4") { $0.page == 4 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,12 +37,11 @@ final class ReaderAnnotationExtendedTests: XCTestCase {
|
||||
XCTAssertTrue(app.buttons[IDs.readerSettings].exists, "设置按钮未出现")
|
||||
}
|
||||
|
||||
func testSelectionTextElementExists() {
|
||||
func testSelectionTextElementExists() throws {
|
||||
app.launchAndOpenSampleBook()
|
||||
app.waitForReader()
|
||||
|
||||
let selectionText = app.otherElements[IDs.readerSelectionText].firstMatch
|
||||
XCTAssertTrue(selectionText.waitForExistence(timeout: 5), "文本选区元素未出现")
|
||||
_ = try app.requireSelectionText()
|
||||
}
|
||||
|
||||
func testReaderStateReportsPageInfo() {
|
||||
|
||||
@@ -7,22 +7,20 @@ final class ReaderAnnotationTests: XCTestCase {
|
||||
continueAfterFailure = false
|
||||
}
|
||||
|
||||
func testSelectionMenuCreatesHighlight() {
|
||||
app.launchAndOpenSampleBook(pageNumber: 2)
|
||||
func testSelectionMenuCreatesHighlight() throws {
|
||||
app.launchAndOpenSampleBook()
|
||||
app.waitForReader()
|
||||
app.waitForReaderPage(2)
|
||||
app.hideReaderChromeIfNeeded()
|
||||
|
||||
let content = app.otherElements[IDs.readerSelectionText].firstMatch
|
||||
XCTAssertTrue(content.waitForExistence(timeout: 5), "阅读内容区域未出现")
|
||||
let content = try app.requireSelectionText()
|
||||
|
||||
app.waitForReaderState(containing: "toolbar=hidden", timeout: 5)
|
||||
app.waitForDemoReaderState(timeout: 5, description: "toolbar=hidden") { $0.toolbar == "hidden" }
|
||||
|
||||
let start = content.coordinate(withNormalizedOffset: CGVector(dx: 0.2, dy: 0.5))
|
||||
let end = content.coordinate(withNormalizedOffset: CGVector(dx: 0.8, dy: 0.5))
|
||||
start.press(forDuration: 0.8, thenDragTo: end)
|
||||
|
||||
app.waitForReaderState(containing: "selection=1", timeout: 5)
|
||||
app.waitForDemoReaderState(timeout: 5, description: "selection=1") { $0.selection == 1 }
|
||||
|
||||
let highlightMenuItem = app.menuItems["高亮"]
|
||||
let highlightButton = app.buttons[IDs.readerSelectionHighlight]
|
||||
@@ -34,25 +32,43 @@ final class ReaderAnnotationTests: XCTestCase {
|
||||
XCTFail("选中文本后未出现高亮菜单")
|
||||
}
|
||||
|
||||
app.waitForReaderState(containing: "highlights=1", timeout: 8)
|
||||
app.waitForDemoReaderState(timeout: 8, description: "highlights=1") { $0.highlights == 1 }
|
||||
}
|
||||
|
||||
func testLongPressSelectionDoesNotShowToolbars() {
|
||||
app.launchAndOpenSampleBook(pageNumber: 2)
|
||||
func testLongPressSelectionDoesNotShowToolbars() throws {
|
||||
app.launchAndOpenSampleBook()
|
||||
app.waitForReader()
|
||||
app.waitForReaderPage(2)
|
||||
app.hideReaderChromeIfNeeded()
|
||||
|
||||
let content = app.otherElements[IDs.readerSelectionText].firstMatch
|
||||
XCTAssertTrue(content.waitForExistence(timeout: 5), "阅读内容区域未出现")
|
||||
let content = try app.requireSelectionText()
|
||||
|
||||
app.waitForReaderState(containing: "toolbar=hidden", timeout: 5)
|
||||
app.waitForDemoReaderState(timeout: 5, description: "toolbar=hidden") { $0.toolbar == "hidden" }
|
||||
|
||||
let start = content.coordinate(withNormalizedOffset: CGVector(dx: 0.2, dy: 0.5))
|
||||
let end = content.coordinate(withNormalizedOffset: CGVector(dx: 0.8, dy: 0.5))
|
||||
start.press(forDuration: 0.8, thenDragTo: end)
|
||||
|
||||
app.waitForReaderState(containing: "selection=1", timeout: 8)
|
||||
app.waitForReaderState(containing: "toolbar=hidden", timeout: 3)
|
||||
app.waitForDemoReaderState(timeout: 8, description: "selection=1") { $0.selection == 1 }
|
||||
app.waitForDemoReaderState(timeout: 3, description: "toolbar=hidden") { $0.toolbar == "hidden" }
|
||||
}
|
||||
|
||||
func testSelectionMenuAnnotateCreatesHighlight() throws {
|
||||
app.launchAndOpenSampleBook()
|
||||
app.waitForReader()
|
||||
app.hideReaderChromeIfNeeded()
|
||||
|
||||
let content = try app.requireSelectionText()
|
||||
|
||||
let start = content.coordinate(withNormalizedOffset: CGVector(dx: 0.25, dy: 0.5))
|
||||
let end = content.coordinate(withNormalizedOffset: CGVector(dx: 0.7, dy: 0.5))
|
||||
start.press(forDuration: 0.8, thenDragTo: end)
|
||||
|
||||
app.waitForDemoReaderState(timeout: 5, description: "selection=1") { $0.selection == 1 }
|
||||
|
||||
let annotateMenuItem = app.menuItems["批注"]
|
||||
XCTAssertTrue(annotateMenuItem.waitForExistence(timeout: 3), "选中文本后未出现批注菜单")
|
||||
annotateMenuItem.tap()
|
||||
|
||||
app.waitForDemoReaderState(timeout: 8, description: "highlights=1 after annotate") { $0.highlights == 1 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,6 +247,189 @@ final class SearchTests: XCTestCase {
|
||||
XCTAssertNotEqual(countLabel.label, "0/0", "搜索状态应在工具栏恢复后保留")
|
||||
}
|
||||
|
||||
// MARK: - 搜索高亮字符验证
|
||||
|
||||
/// 搜索匹配文本应与搜索关键字一致
|
||||
/// 验证当前匹配项的高亮确实覆盖了搜索的关键字
|
||||
func testSearchMatchTextEqualsKeyword() {
|
||||
let keyword = "辽"
|
||||
app.launchAndOpenSampleBook(bookTitleQuery: "宝山辽墓材料与释读", searchKeyword: keyword)
|
||||
app.waitForReader()
|
||||
app.showReaderChromeIfNeeded()
|
||||
|
||||
// 等待搜索结果
|
||||
let countLabel = app.staticTexts[IDs.searchCount]
|
||||
XCTAssertTrue(countLabel.waitForExistence(timeout: 10), "搜索计数应出现")
|
||||
XCTAssertNotEqual(countLabel.label, "0/0", "关键词 '\(keyword)' 应有匹配")
|
||||
|
||||
// 验证当前匹配的文本就是搜索关键字
|
||||
let state = app.waitForDemoReaderState(timeout: 5, description: "searchMatchText exists") {
|
||||
$0.searchMatchText != nil && $0.searchMatchText != "none"
|
||||
}
|
||||
XCTAssertEqual(state.searchMatchText, keyword,
|
||||
"当前搜索匹配文本应为 '\(keyword)',实际:\(state.searchMatchText ?? "nil")")
|
||||
}
|
||||
|
||||
/// 重排后搜索匹配文本仍应与关键字一致
|
||||
func testSearchMatchTextAfterRepagination() {
|
||||
let keyword = "辽"
|
||||
app.launchAndOpenSampleBook(bookTitleQuery: "宝山辽墓材料与释读", searchKeyword: keyword)
|
||||
app.waitForReader()
|
||||
app.showReaderChromeIfNeeded()
|
||||
|
||||
// 等待搜索结果
|
||||
let countLabel = app.staticTexts[IDs.searchCount]
|
||||
XCTAssertTrue(countLabel.waitForExistence(timeout: 10), "搜索计数应出现")
|
||||
|
||||
// 改变字体触发重排
|
||||
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)
|
||||
|
||||
// 重新打开搜索并导航到匹配
|
||||
app.showReaderChromeIfNeeded()
|
||||
let prevButton = app.buttons[IDs.searchPrevious]
|
||||
let nextButton = app.buttons[IDs.searchNext]
|
||||
if prevButton.waitForExistence(timeout: 3), prevButton.isEnabled {
|
||||
prevButton.tap()
|
||||
Thread.sleep(forTimeInterval: 0.5)
|
||||
}
|
||||
if nextButton.waitForExistence(timeout: 3), nextButton.isEnabled {
|
||||
nextButton.tap()
|
||||
Thread.sleep(forTimeInterval: 0.5)
|
||||
}
|
||||
|
||||
// 验证重排后匹配文本仍正确
|
||||
let state = app.waitForDemoReaderState(timeout: 5, description: "searchMatchText after repagination") {
|
||||
$0.searchMatchText != nil && $0.searchMatchText != "none"
|
||||
}
|
||||
XCTAssertEqual(state.searchMatchText, keyword,
|
||||
"重排后搜索匹配文本应为 '\(keyword)',实际:\(state.searchMatchText ?? "nil")")
|
||||
}
|
||||
|
||||
// MARK: - 重排后搜索跳转正确性
|
||||
|
||||
/// 重排后搜索跳转页码应正确(多匹配覆盖)
|
||||
/// 回归:chapterOffset(for:) 曾优先使用 row/column 查表而非存储的 chapterOffset,
|
||||
/// 导致重新排版后搜索跳转到错误的页面
|
||||
func testSearchNavigationAfterRepagination() {
|
||||
app.launchAndOpenSampleBook(searchKeyword: "的")
|
||||
app.waitForReader()
|
||||
app.showReaderChromeIfNeeded()
|
||||
|
||||
// 等待搜索结果
|
||||
let countLabel = app.staticTexts[IDs.searchCount]
|
||||
XCTAssertTrue(countLabel.waitForExistence(timeout: 10), "搜索计数应出现")
|
||||
XCTAssertNotEqual(countLabel.label, "0/0", "关键词 '的' 应有匹配")
|
||||
|
||||
// 解析匹配总数
|
||||
let countText = countLabel.label
|
||||
let parts = countText.split(separator: "/")
|
||||
guard parts.count == 2, let total = Int(parts[1]), total >= 3 else {
|
||||
XCTSkip("需要至少 3 个匹配才能测试多页跳转")
|
||||
return
|
||||
}
|
||||
|
||||
// 改变字体大小触发重新排版
|
||||
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 nextButton = app.buttons[IDs.searchNext]
|
||||
let prevButton = app.buttons[IDs.searchPrevious]
|
||||
XCTAssertTrue(nextButton.waitForExistence(timeout: 3), "下一个按钮应存在")
|
||||
XCTAssertTrue(prevButton.waitForExistence(timeout: 3), "上一个按钮应存在")
|
||||
|
||||
// 导航到第 2 个匹配,记录页码
|
||||
nextButton.tap()
|
||||
Thread.sleep(forTimeInterval: 0.5)
|
||||
let stateMatch2 = app.waitForDemoReaderState(timeout: 5, description: "match 2 page") {
|
||||
($0.page ?? 0) > 0
|
||||
}
|
||||
let pageAtMatch2 = stateMatch2.page ?? 1
|
||||
|
||||
// 翻到其他页
|
||||
let content = app.otherElements[IDs.readerContent].firstMatch
|
||||
if content.waitForExistence(timeout: 3) {
|
||||
for _ in 0..<4 {
|
||||
content.swipeLeft()
|
||||
Thread.sleep(forTimeInterval: 0.3)
|
||||
}
|
||||
Thread.sleep(forTimeInterval: 0.5)
|
||||
}
|
||||
|
||||
// 跳转到第 3 个匹配
|
||||
nextButton.tap()
|
||||
Thread.sleep(forTimeInterval: 0.5)
|
||||
let stateMatch3 = app.waitForDemoReaderState(timeout: 5, description: "match 3 page") {
|
||||
($0.page ?? 0) > 0
|
||||
}
|
||||
let pageAtMatch3 = stateMatch3.page ?? 1
|
||||
|
||||
// 再翻到其他页
|
||||
if content.waitForExistence(timeout: 3) {
|
||||
for _ in 0..<4 {
|
||||
content.swipeLeft()
|
||||
Thread.sleep(forTimeInterval: 0.3)
|
||||
}
|
||||
Thread.sleep(forTimeInterval: 0.5)
|
||||
}
|
||||
|
||||
// 跳回第 2 个匹配,验证页码不变
|
||||
prevButton.tap()
|
||||
Thread.sleep(forTimeInterval: 0.5)
|
||||
let stateBack2 = app.waitForDemoReaderState(timeout: 5, description: "back to match 2") {
|
||||
$0.page == pageAtMatch2
|
||||
}
|
||||
XCTAssertEqual(stateBack2.page, pageAtMatch2,
|
||||
"重排后搜索跳回第 2 个匹配应回到第 \(pageAtMatch2) 页,实际:\(stateBack2.page ?? -1)")
|
||||
|
||||
// 再翻到其他页
|
||||
if content.waitForExistence(timeout: 3) {
|
||||
for _ in 0..<4 {
|
||||
content.swipeLeft()
|
||||
Thread.sleep(forTimeInterval: 0.3)
|
||||
}
|
||||
Thread.sleep(forTimeInterval: 0.5)
|
||||
}
|
||||
|
||||
// 跳到第 3 个匹配,验证页码不变
|
||||
nextButton.tap()
|
||||
Thread.sleep(forTimeInterval: 0.5)
|
||||
let stateBack3 = app.waitForDemoReaderState(timeout: 5, description: "back to match 3") {
|
||||
$0.page == pageAtMatch3
|
||||
}
|
||||
XCTAssertEqual(stateBack3.page, pageAtMatch3,
|
||||
"重排后搜索跳到第 3 个匹配应回到第 \(pageAtMatch3) 页,实际:\(stateBack3.page ?? -1)")
|
||||
}
|
||||
|
||||
// MARK: - 辅助方法
|
||||
|
||||
private func openSearchBar() {
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,6 @@ final class SelectionMenuTests: XCTestCase {
|
||||
let copyMenuItem = app.menuItems["拷贝"]
|
||||
if copyMenuItem.waitForExistence(timeout: 3) { copyMenuItem.tap() }
|
||||
|
||||
app.waitForReaderState(containing: "reader=opened", timeout: 5)
|
||||
app.waitForDemoReaderState(timeout: 5, description: "reader=opened") { $0.isOpened }
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ final class SettingsEffectTests: XCTestCase {
|
||||
func testFontSizeChangeUpdatesContent() throws {
|
||||
app.launchAndOpenSampleBook()
|
||||
app.waitForReader(timeout: 12)
|
||||
app.waitForReaderState(containing: "page=", timeout: 5)
|
||||
app.waitForDemoReaderState(timeout: 5, description: "page exists") { $0.page != nil }
|
||||
|
||||
app.showReaderChromeIfNeeded()
|
||||
XCTAssertTrue(app.buttons[IDs.readerSettings].waitForExistence(timeout: 3), "设置按钮不存在")
|
||||
@@ -23,7 +23,7 @@ final class SettingsEffectTests: XCTestCase {
|
||||
XCTAssertTrue(app.buttons[IDs.settingsDone].waitForExistence(timeout: 3), "完成按钮不存在")
|
||||
app.buttons[IDs.settingsDone].tap()
|
||||
|
||||
app.waitForReaderState(containing: "reader=opened", timeout: 8)
|
||||
app.waitForDemoReaderState(timeout: 8, description: "reader=opened") { $0.isOpened }
|
||||
}
|
||||
|
||||
func testThemeSwitchChangesBackground() throws {
|
||||
@@ -59,7 +59,7 @@ final class SettingsEffectTests: XCTestCase {
|
||||
}
|
||||
|
||||
app.buttons[IDs.settingsDone].tap()
|
||||
app.waitForReaderState(containing: "reader=opened", timeout: 8)
|
||||
app.waitForDemoReaderState(timeout: 8, description: "reader=opened") { $0.isOpened }
|
||||
}
|
||||
|
||||
func testLineHeightChangeUpdatesContent() throws {
|
||||
@@ -76,13 +76,13 @@ final class SettingsEffectTests: XCTestCase {
|
||||
}
|
||||
|
||||
app.buttons[IDs.settingsDone].tap()
|
||||
app.waitForReaderState(containing: "reader=opened", timeout: 8)
|
||||
app.waitForDemoReaderState(timeout: 8, description: "reader=opened") { $0.isOpened }
|
||||
}
|
||||
|
||||
func testDisplayTypeChangeRePaginates() throws {
|
||||
app.launchAndOpenSampleBook(displayType: "horizontalscroll")
|
||||
app.waitForReader(timeout: 12)
|
||||
app.waitForReaderState(containing: "display=horizontalScroll", timeout: 5)
|
||||
app.waitForDemoReaderState(timeout: 5, description: "display=horizontalScroll") { $0.display == "horizontalScroll" }
|
||||
|
||||
app.showReaderChromeIfNeeded()
|
||||
app.buttons[IDs.readerSettings].tap()
|
||||
@@ -94,7 +94,7 @@ final class SettingsEffectTests: XCTestCase {
|
||||
}
|
||||
|
||||
app.buttons[IDs.settingsDone].tap()
|
||||
app.waitForReaderState(containing: "display=", timeout: 8)
|
||||
app.waitForDemoReaderState(timeout: 8, description: "display changed") { $0.display != nil }
|
||||
}
|
||||
|
||||
func testDefaultBodyPaginationDoesNotEnableWidowOrphanCompaction() throws {
|
||||
@@ -141,4 +141,60 @@ final class SettingsEffectTests: XCTestCase {
|
||||
|
||||
app.buttons[IDs.settingsDone].tap()
|
||||
}
|
||||
|
||||
func testThemeSelectionPersistsAfterReopen() throws {
|
||||
app.launchAndOpenSampleBook(resetsReaderState: true)
|
||||
app.waitForReader(timeout: 12)
|
||||
|
||||
app.showReaderChromeIfNeeded()
|
||||
app.buttons[IDs.readerSettings].tap()
|
||||
|
||||
let darkThemeButton = app.buttons[IDs.settingsTheme(5)]
|
||||
XCTAssertTrue(darkThemeButton.waitForExistence(timeout: 5))
|
||||
darkThemeButton.tap()
|
||||
XCTAssertEqual(darkThemeButton.value as? String, "selected", "切换主题后当前主题按钮应为选中态")
|
||||
app.buttons[IDs.settingsDone].tap()
|
||||
|
||||
app.showReaderChromeIfNeeded()
|
||||
app.buttons[IDs.readerBack].tap()
|
||||
|
||||
app.launchAndOpenSampleBook(resetsReaderState: false)
|
||||
app.waitForReader(timeout: 12)
|
||||
app.showReaderChromeIfNeeded()
|
||||
app.buttons[IDs.readerSettings].tap()
|
||||
|
||||
let reopenedDarkThemeButton = app.buttons[IDs.settingsTheme(5)]
|
||||
XCTAssertTrue(reopenedDarkThemeButton.waitForExistence(timeout: 5))
|
||||
XCTAssertEqual(reopenedDarkThemeButton.value as? String, "selected", "主题应在重启后保持选中")
|
||||
app.buttons[IDs.settingsDone].tap()
|
||||
}
|
||||
|
||||
func testFontChoiceChangePersistsAfterReopen() throws {
|
||||
app.launchAndOpenSampleBook(resetsReaderState: true)
|
||||
app.waitForReader(timeout: 12)
|
||||
|
||||
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()
|
||||
}
|
||||
XCTAssertEqual(fontChoiceControl.value as? String, "宋体", "切换字体后当前字体值应更新")
|
||||
app.buttons[IDs.settingsDone].tap()
|
||||
|
||||
app.showReaderChromeIfNeeded()
|
||||
app.buttons[IDs.readerBack].tap()
|
||||
|
||||
app.launchAndOpenSampleBook(resetsReaderState: false)
|
||||
app.waitForReader(timeout: 12)
|
||||
app.showReaderChromeIfNeeded()
|
||||
app.buttons[IDs.readerSettings].tap()
|
||||
|
||||
let reopenedFontChoiceControl = app.segmentedControls[IDs.settingsFontChoice]
|
||||
XCTAssertTrue(reopenedFontChoiceControl.waitForExistence(timeout: 5))
|
||||
XCTAssertEqual(reopenedFontChoiceControl.value as? String, "宋体", "字体选择应在重启后保持")
|
||||
app.buttons[IDs.settingsDone].tap()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ final class SettingsExtendedTests: XCTestCase {
|
||||
|
||||
displayType.buttons["横滑"].tap()
|
||||
app.buttons[IDs.settingsDone].tap()
|
||||
app.waitForReaderState(containing: "display=horizontalScroll", timeout: 5)
|
||||
app.waitForDemoReaderState(timeout: 5, description: "display=horizontalScroll") { $0.display == "horizontalScroll" }
|
||||
}
|
||||
|
||||
func testAllThemeButtonsExist() {
|
||||
|
||||
@@ -47,7 +47,7 @@ final class TOCInteractionTests: XCTestCase {
|
||||
XCTAssertNotEqual(updatedPage, initialPage, "目录跳转后页码应变化")
|
||||
} else {
|
||||
tocTable.cells.element(boundBy: 0).tap()
|
||||
app.waitForReaderState(containing: "reader=opened", timeout: 8)
|
||||
app.waitForDemoReaderState(timeout: 8, description: "reader=opened") { $0.isOpened }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ final class TableOfContentsTests: XCTestCase {
|
||||
func testTocButtonExists() {
|
||||
app.launchAndOpenSampleBook(bookTitleQuery: "宝山辽墓材料与释读")
|
||||
app.waitForReader()
|
||||
app.showReaderChromeIfNeeded()
|
||||
app.showReaderChromeIfNeeded(requiredButtonIDs: [IDs.readerToc])
|
||||
|
||||
let tocButton = app.buttons[IDs.readerToc]
|
||||
XCTAssertTrue(tocButton.waitForExistence(timeout: 5), "目录按钮未出现")
|
||||
@@ -19,7 +19,7 @@ final class TableOfContentsTests: XCTestCase {
|
||||
func testTocButtonTappable() {
|
||||
app.launchAndOpenSampleBook(bookTitleQuery: "宝山辽墓材料与释读")
|
||||
app.waitForReader()
|
||||
app.showReaderChromeIfNeeded()
|
||||
app.showReaderChromeIfNeeded(requiredButtonIDs: [IDs.readerToc])
|
||||
|
||||
let tocButton = app.buttons[IDs.readerToc]
|
||||
XCTAssertTrue(tocButton.waitForExistence(timeout: 5))
|
||||
|
||||
@@ -35,7 +35,7 @@ final class ToolbarStateTests: XCTestCase {
|
||||
app.showReaderChromeIfNeeded()
|
||||
XCTAssertTrue(app.buttons[IDs.readerBack].waitForExistence(timeout: 3))
|
||||
|
||||
app.waitForReaderState(containing: "reader=opened", timeout: 5)
|
||||
app.waitForDemoReaderState(timeout: 5, description: "reader=opened") { $0.isOpened }
|
||||
}
|
||||
|
||||
func testAddHighlightButtonDisabledWithoutSelection() throws {
|
||||
@@ -66,6 +66,6 @@ final class ToolbarStateTests: XCTestCase {
|
||||
let paging = app.collectionViews[IDs.readerPaging]
|
||||
if paging.waitForExistence(timeout: 5) { paging.swipeLeft() }
|
||||
|
||||
app.waitForReaderState(containing: "reader=opened", timeout: 5)
|
||||
app.waitForDemoReaderState(timeout: 5, description: "reader=opened") { $0.isOpened }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user