feat: improve epub reader controls and annotations

This commit is contained in:
shen
2026-07-12 12:23:10 +08:00
parent 063a493b18
commit 8ccb7157b2
36 changed files with 2959 additions and 2160 deletions
@@ -54,7 +54,6 @@ enum IDs {
static let settingsFontValue = "epub.reader.settings.font.value"
static let settingsFontChoice = "epub.reader.settings.font.choice"
static let settingsLineHeight = "epub.reader.settings.lineHeight"
static let settingsColumns = "epub.reader.settings.columns"
static let settingsDisplayType = "epub.reader.settings.displayType"
static let settingsDone = "epub.reader.settings.done"
static func settingsTheme(_ index: Int) -> String { "epub.reader.settings.theme.\(index)" }
@@ -28,6 +28,7 @@ struct DemoReaderState {
var isOpened: Bool { fields["reader"] == "opened" }
var page: Int? { fields["page"].flatMap(Int.init) }
var pagesPerScreen: Int? { fields["pagesPerScreen"].flatMap(Int.init) }
var display: String? { fields["display"] }
var toolbar: String? { fields["toolbar"] }
var highlights: Int? { fields["highlights"].flatMap(Int.init) }
@@ -23,6 +23,11 @@ final class FixedLayoutRotationTests: XCTestCase {
for _ in 0..<3 {
XCUIDevice.shared.orientation = .landscapeLeft
RunLoop.current.run(until: Date().addingTimeInterval(2))
let landscapeState = app.waitForDemoReaderState(
timeout: 5,
description: "固定版式横屏保持单页"
) { $0.pagesPerScreen == 1 }
XCTAssertEqual(landscapeState.pagesPerScreen, 1, "交互型固定版式书横屏不应启用双页")
XCUIDevice.shared.orientation = .portrait
RunLoop.current.run(until: Date().addingTimeInterval(2))
}
@@ -30,4 +35,29 @@ final class FixedLayoutRotationTests: XCTestCase {
XCTAssertEqual(app.state, .runningForeground, "反复旋转后 app 已退出(疑似崩溃)")
app.waitForReader()
}
func testFixedLayoutCachedPageCanReturnAfterPageTurn() {
app.launchAndOpenSampleBook(bookTitleQuery: "熊爷爷")
app.waitForReader()
let content = app.otherElements[IDs.readerContent]
XCTAssertTrue(content.waitForExistence(timeout: 5), "固定版式阅读区域未出现")
let initialState = app.waitForDemoReaderState(timeout: 5, description: "固定版式初始页") {
$0.page != nil
}
let initialPage = initialState.page ?? 0
content.swipeLeft()
let nextState = app.waitForDemoReaderState(timeout: 8, description: "固定版式向后翻页") { state in
guard let page = state.page else { return false }
return page != initialPage
}
content.swipeRight()
let returnedState = app.waitForDemoReaderState(timeout: 8, description: "固定版式返回缓存页") {
$0.page == initialPage
}
XCTAssertNotEqual(nextState.page, returnedState.page, "固定版式返回缓存页后页码未恢复")
XCTAssertTrue(content.exists, "返回缓存页后阅读区域不应消失")
}
}
@@ -19,9 +19,9 @@ final class HighlightsManagementTests: XCTestCase {
let end = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.6, dy: 0.5))
start.press(forDuration: 0.8, thenDragTo: end)
let highlightMenuItem = app.menuItems["高亮"]
let highlightMenuItem = app.menuItems["划线"]
if highlightMenuItem.waitForExistence(timeout: 3) { highlightMenuItem.tap() }
else if app.buttons["高亮"].waitForExistence(timeout: 2) { app.buttons["高亮"].tap() }
else if app.buttons["划线"].waitForExistence(timeout: 2) { app.buttons["划线"].tap() }
app.waitForDemoReaderState(timeout: 5, description: "highlights exists") { $0.highlights != nil }
@@ -42,7 +42,7 @@ final class HighlightsManagementTests: XCTestCase {
let end = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.6, dy: 0.5))
start.press(forDuration: 0.8, thenDragTo: end)
let highlightMenuItem = app.menuItems["高亮"]
let highlightMenuItem = app.menuItems["划线"]
if highlightMenuItem.waitForExistence(timeout: 3) { highlightMenuItem.tap() }
app.showReaderChromeIfNeeded()
@@ -63,7 +63,7 @@ final class HighlightsManagementTests: XCTestCase {
let end = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.6, dy: 0.5))
start.press(forDuration: 0.8, thenDragTo: end)
let highlightMenuItem = app.menuItems["高亮"]
let highlightMenuItem = app.menuItems["划线"]
if highlightMenuItem.waitForExistence(timeout: 3) { highlightMenuItem.tap() }
app.showReaderChromeIfNeeded()
@@ -102,7 +102,7 @@ final class HighlightsManagementTests: XCTestCase {
let end = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.6, dy: 0.5))
start.press(forDuration: 0.8, thenDragTo: end)
let highlightMenuItem = app.menuItems["高亮"]
let highlightMenuItem = app.menuItems["划线"]
if highlightMenuItem.waitForExistence(timeout: 3) { highlightMenuItem.tap() }
app.showReaderChromeIfNeeded()
@@ -112,15 +112,14 @@ final class HighlightsManagementTests: XCTestCase {
XCTAssertTrue(highlightsTable.waitForExistence(timeout: 5))
XCTAssertTrue(waitForCellCount(in: highlightsTable, minimum: 1, timeout: 5))
highlightsTable.cells.element(boundBy: 0).tap()
let editButton = app.navigationBars.buttons["编辑"].firstMatch
XCTAssertTrue(editButton.waitForExistence(timeout: 3), "标注列表应提供编辑入口")
editButton.tap()
let deleteButton = app.buttons["删除标注"].firstMatch
let deleteHighlightButton = app.buttons["删除高亮"].firstMatch
if deleteButton.waitForExistence(timeout: 3) {
deleteButton.tap()
} else if deleteHighlightButton.waitForExistence(timeout: 1) {
deleteHighlightButton.tap()
}
let deleteControl = highlightsTable.buttons["删除"].firstMatch
XCTAssertTrue(deleteControl.waitForExistence(timeout: 3), "编辑状态应显示删除控件")
deleteControl.tap()
app.buttons["删除"].lastMatch.tap()
XCTAssertTrue(app.staticTexts[IDs.readerHighlightsEmptyLabel].waitForExistence(timeout: 5), "删除后应显示空状态")
}
@@ -136,8 +136,8 @@ final class LocationPersistenceTests: XCTestCase {
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()
XCTAssertTrue(app.menuItems["划线"].waitForExistence(timeout: 3))
app.menuItems["划线"].tap()
app.waitForDemoReaderState(timeout: 8, description: "highlights=1") { $0.highlights == 1 }
app.showReaderChromeIfNeeded()
@@ -167,7 +167,7 @@ final class LocationPersistenceTests: XCTestCase {
let end = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.6, dy: 0.5))
start.press(forDuration: 0.8, thenDragTo: end)
let highlightItem = app.menuItems["高亮"]
let highlightItem = app.menuItems["划线"]
XCTAssertTrue(highlightItem.waitForExistence(timeout: 3), "应出现高亮菜单")
highlightItem.tap()
app.waitForDemoReaderState(timeout: 8, description: "highlights=1") { $0.highlights == 1 }
@@ -22,7 +22,7 @@ final class ReaderAnnotationTests: XCTestCase {
app.waitForDemoReaderState(timeout: 5, description: "selection=1") { $0.selection == 1 }
let highlightMenuItem = app.menuItems["高亮"]
let highlightMenuItem = app.menuItems["划线"]
let highlightButton = app.buttons[IDs.readerSelectionHighlight]
if highlightMenuItem.waitForExistence(timeout: 3) {
highlightMenuItem.tap()
@@ -65,7 +65,7 @@ final class ReaderAnnotationTests: XCTestCase {
app.waitForDemoReaderState(timeout: 5, description: "selection=1") { $0.selection == 1 }
let annotateMenuItem = app.menuItems[""]
let annotateMenuItem = app.menuItems[""]
XCTAssertTrue(annotateMenuItem.waitForExistence(timeout: 3), "选中文本后未出现批注菜单")
annotateMenuItem.tap()
@@ -29,7 +29,7 @@ final class SelectionAnnotateTests: XCTestCase {
Thread.sleep(forTimeInterval: 0.5)
//
let annotateItem = app.menuItems[""]
let annotateItem = app.menuItems[""]
guard annotateItem.waitForExistence(timeout: 3) else {
throw XCTSkip("批注菜单项不存在")
}
@@ -56,7 +56,7 @@ final class SelectionAnnotateTests: XCTestCase {
Thread.sleep(forTimeInterval: 0.5)
let annotateItem = app.menuItems[""]
let annotateItem = app.menuItems[""]
guard annotateItem.waitForExistence(timeout: 3) else {
throw XCTSkip("批注菜单项不存在")
}
@@ -101,7 +101,7 @@ final class SelectionAnnotateTests: XCTestCase {
start.press(forDuration: 0.8, thenDragTo: end)
Thread.sleep(forTimeInterval: 0.5)
let highlightItem = app.menuItems["高亮"]
let highlightItem = app.menuItems["划线"]
guard highlightItem.waitForExistence(timeout: 3) else {
throw XCTSkip("高亮菜单项不存在")
}
@@ -18,7 +18,7 @@ final class SelectionMenuTests: XCTestCase {
let end = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.6, dy: 0.5))
start.press(forDuration: 0.8, thenDragTo: end)
XCTAssertTrue(app.menuItems["拷贝"].waitForExistence(timeout: 3), "选区菜单应包含拷贝选项")
XCTAssertTrue(app.menuItems["复制"].waitForExistence(timeout: 3), "选区菜单应包含拷贝选项")
}
func testSelectionMenuShowsAnnotateOption() throws {
@@ -32,7 +32,7 @@ final class SelectionMenuTests: XCTestCase {
let end = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.6, dy: 0.5))
start.press(forDuration: 0.8, thenDragTo: end)
XCTAssertTrue(app.menuItems[""].waitForExistence(timeout: 3), "选区菜单应包含批注选项")
XCTAssertTrue(app.menuItems[""].waitForExistence(timeout: 3), "选区菜单应包含批注选项")
}
func testTapBlankAreaClearsSelection() throws {
@@ -46,14 +46,14 @@ final class SelectionMenuTests: XCTestCase {
let end = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.6, dy: 0.5))
start.press(forDuration: 0.8, thenDragTo: end)
XCTAssertTrue(app.menuItems["拷贝"].waitForExistence(timeout: 3), "选区菜单应出现")
XCTAssertTrue(app.menuItems["复制"].waitForExistence(timeout: 3), "选区菜单应出现")
let contentArea = app.otherElements[IDs.readerContentView].firstMatch
if contentArea.waitForExistence(timeout: 3) {
contentArea.coordinate(withNormalizedOffset: CGVector(dx: 0.1, dy: 0.1)).tap()
}
let menuGone = !app.menuItems["拷贝"].waitForExistence(timeout: 2)
let menuGone = !app.menuItems["复制"].waitForExistence(timeout: 2)
XCTAssertTrue(menuGone, "点击空白区域后选区菜单应消失")
}
@@ -68,7 +68,7 @@ final class SelectionMenuTests: XCTestCase {
let end = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.6, dy: 0.5))
start.press(forDuration: 0.8, thenDragTo: end)
let copyMenuItem = app.menuItems["拷贝"]
let copyMenuItem = app.menuItems["复制"]
if copyMenuItem.waitForExistence(timeout: 3) { copyMenuItem.tap() }
app.waitForDemoReaderState(timeout: 5, description: "reader=opened") { $0.isOpened }
@@ -44,24 +44,6 @@ final class SettingsEffectTests: XCTestCase {
XCTAssertTrue(contentView.waitForExistence(timeout: 5), "主题切换后内容区应存在")
}
func testColumnCountChangeUpdatesLayout() throws {
app.launchAndOpenSampleBook()
app.waitForReader(timeout: 12)
app.showReaderChromeIfNeeded()
app.buttons[IDs.readerSettings].tap()
XCTAssertTrue(app.scrollViews[IDs.settingsScroll].waitForExistence(timeout: 5))
app.scrollViews[IDs.settingsScroll].swipeUp()
let columnsControl = app.segmentedControls[IDs.settingsColumns]
if columnsControl.waitForExistence(timeout: 3) && columnsControl.buttons.count > 1 {
columnsControl.buttons.element(boundBy: 1).tap()
}
app.buttons[IDs.settingsDone].tap()
app.waitForDemoReaderState(timeout: 8, description: "reader=opened") { $0.isOpened }
}
func testLineHeightChangeUpdatesContent() throws {
app.launchAndOpenSampleBook()
app.waitForReader(timeout: 12)
@@ -36,18 +36,6 @@ final class SettingsExtendedTests: XCTestCase {
XCTAssertTrue(app.buttons[IDs.readerBack].waitForExistence(timeout: 5))
}
func testColumnCountControl() {
openSettings()
let columns = app.segmentedControls[IDs.settingsColumns]
XCTAssertTrue(columns.waitForExistence(timeout: 3), "分栏控件未出现")
columns.buttons["单栏"].tap()
columns.buttons["双栏"].tap()
app.buttons[IDs.settingsDone].tap()
XCTAssertTrue(app.buttons[IDs.readerBack].waitForExistence(timeout: 5))
}
func testDisplayTypeFromSettings() {
openSettings()
let displayType = app.segmentedControls[IDs.settingsDisplayType]
@@ -73,7 +61,6 @@ final class SettingsExtendedTests: XCTestCase {
XCTAssertTrue(app.sliders[IDs.settingsBrightness].waitForExistence(timeout: 3))
XCTAssertTrue(app.segmentedControls[IDs.settingsLineHeight].waitForExistence(timeout: 3))
XCTAssertTrue(app.segmentedControls[IDs.settingsColumns].waitForExistence(timeout: 3))
XCTAssertTrue(app.segmentedControls[IDs.settingsDisplayType].waitForExistence(timeout: 3))
}
}