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:
shen
2026-06-13 22:48:56 +08:00
parent 27e9b85ddb
commit 6f75b083f7
83 changed files with 4824 additions and 1879 deletions
@@ -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 }
}
}