# 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 --demo-page <页码> --demo-display-sequence --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=` - 切换翻页模式后:`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。