feat(reader): 增强阅读器功能与 UI 测试支持

- 新增字体选择(系统/宋体/圆体/等宽)与暗色图片柔化配置
- 文本选择改为自定义手势+操作栏(拷贝/高亮/批注)
- 添加 accessibilityIdentifier 支持自动化 UI 测试
- 新增 UITests 覆盖阅读器打开/关闭、工具栏、设置面板、批注等
- 添加 Demo 测试用 EPUB 书源(宝山辽墓材料与释读)
- 新增文档:UI 自动化测试、功能开发计划、阅读器规划
This commit is contained in:
shen
2026-05-31 23:56:54 +08:00
parent 44202357c0
commit 1efb9d172f
268 changed files with 4991 additions and 44 deletions
@@ -0,0 +1,26 @@
import Foundation
enum IDs {
static let demoStatus = "demo.status"
static let demoBooksTable = "demo.books.table"
static func demoBook(_ index: Int) -> String { "demo.book.\(index)" }
static let demoReaderState = "demo.reader.state"
static let readerBack = "epub.reader.back"
static let readerTitle = "epub.reader.title"
static let readerTopToolbar = "epub.reader.topToolbar"
static let readerBottomToolbar = "epub.reader.bottomToolbar"
static let readerContent = "epub.reader.content"
static let readerPaging = "epub.reader.paging"
static let readerSettings = "epub.reader.settings"
static let readerSelectionText = "epub.reader.selection.text"
static let readerSelectionHighlight = "epub.reader.selection.高亮"
static let settingsScroll = "epub.reader.settings.scroll"
static let settingsFontIncrease = "epub.reader.settings.font.increase"
static let settingsFontDecrease = "epub.reader.settings.font.decrease"
static let settingsFontValue = "epub.reader.settings.font.value"
static let settingsFontChoice = "epub.reader.settings.font.choice"
static let settingsDone = "epub.reader.settings.done"
static func settingsTheme(_ index: Int) -> String { "epub.reader.settings.theme.\(index)" }
}
@@ -0,0 +1,51 @@
import XCTest
extension XCUIApplication {
func launchAndOpenSampleBook(displayType: String? = nil, pageNumber: Int? = nil) {
var args = ["--demo-book-title", "回归验证样本"]
if let displayType {
args += ["--demo-display-type", displayType]
}
if let pageNumber {
args += ["--demo-page", "\(pageNumber)"]
}
launchArguments = args
launch()
}
@discardableResult
func waitForReader(timeout: TimeInterval = 12) -> XCUIElement {
let state = staticTexts[IDs.demoReaderState]
XCTAssertTrue(state.waitForExistence(timeout: timeout), "阅读器状态标签未出现")
XCTAssertTrue(state.label.contains("reader=opened"), "阅读器未进入 opened 状态:\(state.label)")
return state
}
func showReaderChromeIfNeeded() {
if buttons[IDs.readerBack].exists && buttons[IDs.readerSettings].exists {
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()
}
}
func waitForReaderState(containing expectedText: String, timeout: TimeInterval = 8) {
let state = staticTexts[IDs.demoReaderState]
let deadline = Date().addingTimeInterval(timeout)
while Date() < deadline {
if state.exists && state.label.contains(expectedText) {
return
}
RunLoop.current.run(until: Date().addingTimeInterval(0.1))
}
let currentLabel = state.exists ? state.label : "<missing>"
XCTFail("阅读器状态未包含 \(expectedText),当前:\(currentLabel)")
}
}
@@ -0,0 +1,26 @@
import XCTest
final class DisplayTypeTests: XCTestCase {
private let app = XCUIApplication()
override func setUpWithError() throws {
continueAfterFailure = false
}
func testOpenWithPageCurl() {
app.launchAndOpenSampleBook(displayType: "pagecurl")
app.waitForReader()
}
func testOpenWithHorizontalScroll() {
app.launchAndOpenSampleBook(displayType: "scroll")
app.waitForReader()
app.waitForReaderState(containing: "display=horizontalScroll")
}
func testOpenWithVerticalScroll() {
app.launchAndOpenSampleBook(displayType: "vertical")
app.waitForReader()
app.waitForReaderState(containing: "display=verticalScroll")
}
}
@@ -0,0 +1,37 @@
import XCTest
final class ReaderAnnotationTests: XCTestCase {
private let app = XCUIApplication()
override func setUpWithError() throws {
continueAfterFailure = false
}
func testSelectionMenuCreatesHighlight() {
app.launchAndOpenSampleBook(pageNumber: 2)
app.waitForReader()
let content = app.textViews[IDs.readerSelectionText].firstMatch
XCTAssertTrue(content.waitForExistence(timeout: 5), "阅读内容区域未出现")
let start = content.coordinate(withNormalizedOffset: CGVector(dx: 0.35, dy: 0.42))
let end = content.coordinate(withNormalizedOffset: CGVector(dx: 0.68, dy: 0.42))
start.press(forDuration: 0.6, thenDragTo: end)
app.waitForReaderState(containing: "selection=1", timeout: 5)
let highlightMenuItem = app.menuItems["高亮"]
let highlightButton = app.buttons["高亮"]
let highlightActionButton = app.buttons[IDs.readerSelectionHighlight]
if highlightMenuItem.waitForExistence(timeout: 3) {
highlightMenuItem.tap()
} else if highlightActionButton.waitForExistence(timeout: 3) {
highlightActionButton.tap()
} else {
XCTAssertTrue(highlightButton.waitForExistence(timeout: 3), "选中文本后未出现高亮菜单")
highlightButton.tap()
}
app.waitForReaderState(containing: "highlights=1", timeout: 8)
}
}
@@ -0,0 +1,31 @@
import XCTest
final class ReaderOpenCloseTests: XCTestCase {
private let app = XCUIApplication()
override func setUpWithError() throws {
continueAfterFailure = false
}
func testBookListShowsSampleBooks() {
app.launch()
XCTAssertTrue(app.tables[IDs.demoBooksTable].waitForExistence(timeout: 5))
XCTAssertTrue(app.cells[IDs.demoBook(0)].waitForExistence(timeout: 5))
}
func testLaunchArgumentOpensReader() {
app.launchAndOpenSampleBook()
app.waitForReader()
app.showReaderChromeIfNeeded()
XCTAssertTrue(app.staticTexts[IDs.readerTitle].waitForExistence(timeout: 5))
}
func testBackButtonReturnsToBookList() {
app.launchAndOpenSampleBook()
app.waitForReader()
app.showReaderChromeIfNeeded()
XCTAssertTrue(app.buttons[IDs.readerBack].waitForExistence(timeout: 5))
app.buttons[IDs.readerBack].tap()
XCTAssertTrue(app.tables[IDs.demoBooksTable].waitForExistence(timeout: 5))
}
}
@@ -0,0 +1,21 @@
import XCTest
final class ReaderToolbarTests: XCTestCase {
private let app = XCUIApplication()
override func setUpWithError() throws {
continueAfterFailure = false
}
func testTapCenterShowsToolbars() {
app.launchAndOpenSampleBook()
app.waitForReader()
app.showReaderChromeIfNeeded()
XCTAssertTrue(app.otherElements[IDs.readerTopToolbar].waitForExistence(timeout: 5))
XCTAssertTrue(app.otherElements[IDs.readerBottomToolbar].waitForExistence(timeout: 5))
XCTAssertTrue(app.buttons[IDs.readerBack].exists)
XCTAssertTrue(app.buttons[IDs.readerSettings].exists)
}
}
@@ -0,0 +1,38 @@
import XCTest
final class SettingsPanelTests: XCTestCase {
private let app = XCUIApplication()
override func setUpWithError() throws {
continueAfterFailure = false
}
func testOpenChangeFontAndCloseSettings() {
app.launchAndOpenSampleBook()
app.waitForReader()
app.showReaderChromeIfNeeded()
XCTAssertTrue(app.buttons[IDs.readerSettings].waitForExistence(timeout: 5))
app.buttons[IDs.readerSettings].tap()
XCTAssertTrue(app.scrollViews[IDs.settingsScroll].waitForExistence(timeout: 5))
let fontValue = app.staticTexts[IDs.settingsFontValue]
XCTAssertTrue(fontValue.waitForExistence(timeout: 3))
let before = fontValue.label
app.buttons[IDs.settingsFontIncrease].tap()
XCTAssertNotEqual(before, fontValue.label)
app.buttons[IDs.settingsFontDecrease].tap()
let fontChoice = app.segmentedControls[IDs.settingsFontChoice]
XCTAssertTrue(fontChoice.waitForExistence(timeout: 3))
fontChoice.buttons["宋体"].tap()
let darkTheme = app.buttons[IDs.settingsTheme(5)]
XCTAssertTrue(darkTheme.waitForExistence(timeout: 3))
darkTheme.tap()
app.buttons[IDs.settingsDone].tap()
XCTAssertTrue(app.buttons[IDs.readerBack].waitForExistence(timeout: 5))
}
}