- 新增字体选择(系统/宋体/圆体/等宽)与暗色图片柔化配置 - 文本选择改为自定义手势+操作栏(拷贝/高亮/批注) - 添加 accessibilityIdentifier 支持自动化 UI 测试 - 新增 UITests 覆盖阅读器打开/关闭、工具栏、设置面板、批注等 - 添加 Demo 测试用 EPUB 书源(宝山辽墓材料与释读) - 新增文档:UI 自动化测试、功能开发计划、阅读器规划
416 lines
16 KiB
Markdown
416 lines
16 KiB
Markdown
# 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 迁移到真实 App,Demo-only 的 `demo.reader.state` 不应进入 SDK 公共 API。
|