ReadViewSDK/Doc/UI自动化测试工作文档.md
shen 1efb9d172f feat(reader): 增强阅读器功能与 UI 测试支持
- 新增字体选择(系统/宋体/圆体/等宽)与暗色图片柔化配置
- 文本选择改为自定义手势+操作栏(拷贝/高亮/批注)
- 添加 accessibilityIdentifier 支持自动化 UI 测试
- 新增 UITests 覆盖阅读器打开/关闭、工具栏、设置面板、批注等
- 添加 Demo 测试用 EPUB 书源(宝山辽墓材料与释读)
- 新增文档:UI 自动化测试、功能开发计划、阅读器规划
2026-05-31 23:56:54 +08:00

416 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`
```bash
--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 测试状态设计
建议增加一个仅用于自动化的状态标签:
```swift
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
1. 打开 `ReadViewDemo/ReadViewDemo.xcworkspace`
2. File -> New -> Target -> iOS UI Testing Bundle
3. Product Name: `ReadViewDemoUITests`
4. Target to be Tested: `ReadViewDemo`
5. 新增测试目录:
```text
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 启动辅助
```swift
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 书库与打开关闭
```swift
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 工具栏显示
```swift
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 设置面板
```swift
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
```swift
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 手动命令
```bash
xcodebuild test \
-workspace ReadViewDemo/ReadViewDemo.xcworkspace \
-scheme ReadViewDemo \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-only-testing:ReadViewDemoUITests
```
如果本机没有 `iPhone 16`,先查看可用模拟器:
```bash
xcrun simctl list devices available
```
### 9.2 GitHub Actions
```yaml
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 迁移到真实 AppDemo-only 的 `demo.reader.state` 不应进入 SDK 公共 API。