- 新增字体选择(系统/宋体/圆体/等宽)与暗色图片柔化配置 - 文本选择改为自定义手势+操作栏(拷贝/高亮/批注) - 添加 accessibilityIdentifier 支持自动化 UI 测试 - 新增 UITests 覆盖阅读器打开/关闭、工具栏、设置面板、批注等 - 添加 Demo 测试用 EPUB 书源(宝山辽墓材料与释读) - 新增文档:UI 自动化测试、功能开发计划、阅读器规划
16 KiB
ReadViewSDK UI 自动化测试可执行方案
1. 目标与边界
本文档描述当前架构下可落地的 XCUITest 方案。目标不是一次性覆盖所有 SDK API,而是先为 Demo App 的核心阅读链路建立稳定回归网:
- 书库页面能展示样本书。
- 样本书能打开阅读器。
- 点击阅读区域中部能显示顶部和底部工具栏。
- 顶部返回按钮能关闭阅读器并回到书库。
- 设置面板能打开、操作、关闭。
- 三种翻页模式能通过启动参数切换并保持阅读器可用。
XCUITest 运行在独立进程,不能直接调用 RDEPUBReaderController 的 Swift 公共 API。因此需要通过 UI 元素、launch arguments、Demo 测试状态标签或截图附件验证结果。SDK 公共 API 可以由单元测试或 Demo 自动化入口间接驱动,不应写成 XCUITest 的直接依赖。
2. 当前可用基础
2.1 已有 accessibilityIdentifier
| 标识符 | 当前文件 | 用途 |
|---|---|---|
epub.reader.back |
Sources/RDReaderView/EPUBUI/RDEPUBReaderTopToolView.swift |
顶部返回按钮 |
epub.reader.bookmark |
Sources/RDReaderView/EPUBUI/RDEPUBReaderTopToolView.swift |
顶部书签按钮 |
epub.reader.title |
Sources/RDReaderView/EPUBUI/RDEPUBReaderTopToolView.swift |
顶部标题 |
epub.reader.toc |
Sources/RDReaderView/EPUBUI/RDEPUBReaderBottomToolView.swift |
目录按钮 |
epub.reader.bookmarks |
Sources/RDReaderView/EPUBUI/RDEPUBReaderBottomToolView.swift |
书签列表按钮 |
epub.reader.highlights |
Sources/RDReaderView/EPUBUI/RDEPUBReaderBottomToolView.swift |
高亮列表按钮 |
epub.reader.add-highlight |
Sources/RDReaderView/EPUBUI/RDEPUBReaderBottomToolView.swift |
新建高亮按钮 |
epub.reader.settings |
Sources/RDReaderView/EPUBUI/RDEPUBReaderBottomToolView.swift |
设置按钮 |
demo.root |
ReadViewDemo/ReadViewDemo/ViewController.swift |
Demo 根视图 |
demo.status |
ReadViewDemo/ReadViewDemo/ViewController.swift |
Demo 状态标签 |
demo.books.table |
ReadViewDemo/ReadViewDemo/ViewController.swift |
书库表格 |
demo.books.empty |
ReadViewDemo/ReadViewDemo/ViewController.swift |
空书库提示 |
demo.book.{n} |
ReadViewDemo/ReadViewDemo/ViewController.swift |
书库第 n 本书 |
demo.reader.host |
ReadViewDemo/ReadViewDemo/ViewController.swift |
modal 场景下的阅读器宿主 |
2.2 已有 Demo 启动参数
ReadViewDemo/ReadViewDemo/ViewController.swift 已有 LaunchAutomationPlan:
--demo-book-title <关键词>
--demo-display-type <pagecurl|scroll|vertical>
--demo-page <页码>
--demo-display-sequence <pagecurl,scroll,vertical>
--demo-step-delay <秒>
首批测试优先使用 --demo-book-title 回归验证样本。该样本目前存在于 ReadViewDemo/ReadViewDemo/book/回归验证样本.txt,适合作为稳定自动化入口。
3. 当前架构下的落点
不要把测试辅助职责重新塞回 RDEPUBReaderController.swift。identifier 和测试状态应按真实 UI/职责归属放置:
| 能力 | 落点 | 原因 |
|---|---|---|
| 顶部工具栏容器 | RDEPUBReaderTopToolView.swift |
工具栏自身创建和维护顶部按钮 |
| 底部工具栏容器 | RDEPUBReaderBottomToolView.swift |
工具栏自身创建和维护底部按钮 |
| 阅读点击区域 | RDReaderView.swift |
点击中区、翻页手势都由 ReaderView 处理 |
| 滚动翻页容器 | RDReaderView.swift 的 collectionView |
横滑/竖滑模式使用 collection view |
| 单页内容 cell | RDReaderContentCell.swift 或当前分页 cell 文件 |
验证 cell 存在,不验证文本排版细节 |
| 设置面板控件 | EPUBUI/Settings/RDEPUBReaderSettingsViewController.swift |
设置面板已从 EPUBUI 根目录迁移到 Settings |
| 目录列表 | RDEPUBReaderChapterListController.swift |
目录是独立 UITableViewController |
| 高亮/书签列表 | RDEPUBReaderHighlightsViewController.swift |
高亮和书签管理器在该文件中 |
| Demo 自动化状态 | ViewController.swift 或 RDURLReaderController.swift |
XCUITest 通过文本/identifier 读取状态 |
4. 需要新增的最小标识符
4.1 P0 必需
| 标识符 | 建议文件 | 用途 |
|---|---|---|
epub.reader.topToolbar |
RDEPUBReaderTopToolView.swift |
断言顶部工具栏显示/隐藏 |
epub.reader.bottomToolbar |
RDEPUBReaderBottomToolView.swift |
断言底部工具栏显示/隐藏 |
epub.reader.content |
RDReaderView.swift |
点击阅读区域中部、滑动翻页 |
epub.reader.paging |
RDReaderView.swift 的 collectionView |
横滑/竖滑容器存在性 |
epub.reader.settings.scroll |
RDEPUBReaderSettingsViewController.swift |
设置面板已打开 |
epub.reader.settings.brightness |
RDEPUBReaderSettingsViewController.swift |
亮度 slider |
epub.reader.settings.font.decrease |
RDEPUBReaderSettingsViewController.swift |
字号减 |
epub.reader.settings.font.increase |
RDEPUBReaderSettingsViewController.swift |
字号加 |
epub.reader.settings.font.value |
RDEPUBReaderSettingsViewController.swift |
字号值 |
epub.reader.settings.displayType |
RDEPUBReaderSettingsViewController.swift |
翻页模式分段控件 |
epub.reader.settings.done |
RDEPUBReaderSettingsViewController.swift |
完成按钮 |
demo.reader.state |
Demo 层 | 输出当前打开状态、页码、翻页模式 |
4.2 P1 后续补充
| 标识符 | 建议文件 | 用途 |
|---|---|---|
epub.reader.toc.list |
RDEPUBReaderChapterListController.swift |
目录列表 |
epub.reader.toc.cell.{n} |
RDEPUBReaderChapterListController.swift |
目录项 |
epub.reader.bookmark.list |
RDEPUBReaderHighlightsViewController.swift 的书签控制器 |
书签列表 |
epub.reader.bookmark.cell.{n} |
RDEPUBReaderHighlightsViewController.swift 的书签控制器 |
书签项 |
epub.reader.highlight.list |
RDEPUBReaderHighlightsViewController.swift |
高亮列表 |
epub.reader.highlight.cell.{n} |
RDEPUBReaderHighlightsViewController.swift |
高亮项 |
epub.reader.settings.lineHeight |
RDEPUBReaderSettingsViewController.swift |
行距分段控件 |
epub.reader.settings.columns |
RDEPUBReaderSettingsViewController.swift |
栏数分段控件 |
epub.reader.settings.theme.{n} |
RDEPUBReaderSettingsViewController.swift |
主题按钮 |
epub.reader.pageIndicator 暂不列为必需项,因为当前没有稳定的页码指示器 UI。若需要断言页码,优先通过 demo.reader.state 暴露 page=...,或者给 RDReaderView 设置 accessibilityValue。
5. Demo 测试状态设计
建议增加一个仅用于自动化的状态标签:
stateLabel.accessibilityIdentifier = "demo.reader.state"
stateLabel.isHidden = true
stateLabel.text = "reader=opened page=1 display=pagecurl toolbar=hidden"
状态来源可以在 Demo 层更新,不要求 SDK 为测试暴露内部对象:
- 打开阅读器后:
reader=opened - 返回书库后:
reader=closed - 跳页或翻页后:
page=<n> - 切换翻页模式后:
display=pagecurl|scroll|vertical - 工具栏显示后:
toolbar=visible
这个标签能显著减少 XCUITest 对动画、布局和截图的猜测,是当前架构下最稳的可执行方案。
6. UI Test Target 创建方式
使用 workspace,不使用单独的 xcodeproj:
- 打开
ReadViewDemo/ReadViewDemo.xcworkspace - File -> New -> Target -> iOS UI Testing Bundle
- Product Name:
ReadViewDemoUITests - Target to be Tested:
ReadViewDemo - 新增测试目录:
ReadViewDemo/
ReadViewDemoUITests/
Helpers/
AccessibilityIdentifiers.swift
XCUIApplication+Launch.swift
XCUIElement+Wait.swift
ReaderUITests/
BookListTests.swift
ReaderOpenCloseTests.swift
ReaderToolbarTests.swift
SettingsPanelTests.swift
DisplayTypeTests.swift
SmokeScreenshotTests.swift
首批不要拆太多测试类,避免还没稳定就出现维护成本。
7. P0 测试用例
7.1 启动辅助
import XCTest
enum IDs {
static let demoStatus = "demo.status"
static let demoBooksTable = "demo.books.table"
static func demoBook(_ n: Int) -> String { "demo.book.\(n)" }
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 readerSettings = "epub.reader.settings"
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 settingsDone = "epub.reader.settings.done"
}
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 = 10) -> XCUIElement {
let backButton = buttons[IDs.readerBack]
XCTAssertTrue(backButton.waitForExistence(timeout: timeout))
return backButton
}
}
7.2 书库与打开关闭
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()
XCTAssertTrue(app.staticTexts[IDs.readerTitle].exists)
}
func testBackButtonReturnsToBookList() {
app.launchAndOpenSampleBook()
app.waitForReader().tap()
XCTAssertTrue(app.tables[IDs.demoBooksTable].waitForExistence(timeout: 5))
}
}
7.3 工具栏显示
final class ReaderToolbarTests: XCTestCase {
private let app = XCUIApplication()
override func setUpWithError() throws {
continueAfterFailure = false
}
func testTapCenterShowsToolbars() {
app.launchAndOpenSampleBook()
app.waitForReader()
let content = app.otherElements[IDs.readerContent]
XCTAssertTrue(content.waitForExistence(timeout: 5))
content.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
XCTAssertTrue(app.otherElements[IDs.readerTopToolbar].waitForExistence(timeout: 3))
XCTAssertTrue(app.otherElements[IDs.readerBottomToolbar].waitForExistence(timeout: 3))
}
}
7.4 设置面板
final class SettingsPanelTests: XCTestCase {
private let app = XCUIApplication()
override func setUpWithError() throws {
continueAfterFailure = false
}
func testOpenChangeFontAndCloseSettings() {
app.launchAndOpenSampleBook()
app.waitForReader()
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()
app.buttons[IDs.settingsDone].tap()
XCTAssertTrue(app.buttons[IDs.readerBack].waitForExistence(timeout: 5))
}
}
7.5 翻页模式 Smoke Test
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()
}
func testOpenWithVerticalScroll() {
app.launchAndOpenSampleBook(displayType: "vertical")
app.waitForReader()
}
}
8. P1 测试用例
P0 稳定后再加入:
- 目录打开、目录列表存在、点击第一项返回阅读器。
- 添加书签后按钮状态变化,打开书签列表存在对应 cell。
- 设置面板切换行距、栏数、主题后阅读器仍可用。
- 横滑/竖滑模式下拖动内容区域后
demo.reader.state的 page 变化。 - 高亮列表打开和空态显示。
搜索和文本选择高亮不建议作为第一批 UI 自动化。它们依赖文本命中、长按选择、浮层菜单和异步渲染,flaky 风险更高。可以先用单元测试覆盖搜索引擎,用 P1/P2 再补 UI 冒烟。
9. CI 集成
9.1 手动命令
xcodebuild test \
-workspace ReadViewDemo/ReadViewDemo.xcworkspace \
-scheme ReadViewDemo \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-only-testing:ReadViewDemoUITests
如果本机没有 iPhone 16,先查看可用模拟器:
xcrun simctl list devices available
9.2 GitHub Actions
name: UI Tests
on:
pull_request:
branches: [main, develop]
push:
branches: [main, develop]
jobs:
ui-tests:
runs-on: macos-15
timeout-minutes: 40
steps:
- uses: actions/checkout@v4
- name: Install Pods
run: cd ReadViewDemo && pod install
- name: Run UI Tests
run: |
xcodebuild test \
-workspace ReadViewDemo/ReadViewDemo.xcworkspace \
-scheme ReadViewDemo \
-destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \
-only-testing:ReadViewDemoUITests \
-resultBundlePath ui-test-results.xcresult
- name: Upload xcresult
if: always()
uses: actions/upload-artifact@v4
with:
name: ui-test-results
path: ui-test-results.xcresult
CI 首批只跑 P0。P1 可以先本地或 nightly 跑,稳定后再进入 PR 必过。
10. 实施顺序
| 阶段 | 内容 | 验收标准 |
|---|---|---|
| 1 | 补 P0 identifiers | XCUITest 能定位内容区、工具栏、设置控件 |
| 2 | 增加 demo.reader.state |
能通过 hidden label 读到 opened/page/display/toolbar |
| 3 | 创建 ReadViewDemoUITests |
xcodebuild test 能发现测试 target |
| 4 | 实现 P0 用例 | 本地模拟器连续跑 3 次通过 |
| 5 | 接入 CI | PR 中 P0 UI Tests 可运行并上传 xcresult |
| 6 | 扩展 P1 | 目录、书签、主题、翻页断言逐步加入 |
11. 工时预估
| 工作 | 预估 |
|---|---|
| P0 identifiers + Demo 状态标签 | 2-3h |
| UI Test Target + helpers | 1-2h |
| P0 测试实现与稳定性调整 | 4-6h |
| CI 接入 | 1-2h |
| P1 首批扩展 | 4-8h |
首个可用版本建议按 1.5 到 2 天排期。后续每加入一组复杂交互,先本地观察稳定性,再进入 CI 必过集合。
12. 风险与约束
- UI 自动化不能依赖固定动画时间,优先使用
waitForExistence和状态标签。 - 不要用截图像素对比作为 PR 必过项;截图附件适合作为人工排查材料。
- 样本书必须稳定存在,优先使用
回归验证样本.txt。 - 工具栏默认可能隐藏,测试应先点击阅读区域中部再断言底部按钮。
- 设置面板、目录面板是导航/弹出结构,测试应等待面板根控件,而不是立即点击内部控件。
- 若未来 Reader UI 迁移到真实 App,Demo-only 的
demo.reader.state不应进入 SDK 公共 API。