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

16 KiB
Raw Blame History

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.swiftcollectionView 横滑/竖滑模式使用 collection view
单页内容 cell RDReaderContentCell.swift 或当前分页 cell 文件 验证 cell 存在,不验证文本排版细节
设置面板控件 EPUBUI/Settings/RDEPUBReaderSettingsViewController.swift 设置面板已从 EPUBUI 根目录迁移到 Settings
目录列表 RDEPUBReaderChapterListController.swift 目录是独立 UITableViewController
高亮/书签列表 RDEPUBReaderHighlightsViewController.swift 高亮和书签管理器在该文件中
Demo 自动化状态 ViewController.swiftRDURLReaderController.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.swiftcollectionView 横滑/竖滑容器存在性
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

  1. 打开 ReadViewDemo/ReadViewDemo.xcworkspace
  2. File -> New -> Target -> iOS UI Testing Bundle
  3. Product Name: ReadViewDemoUITests
  4. Target to be Tested: ReadViewDemo
  5. 新增测试目录:
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 迁移到真实 AppDemo-only 的 demo.reader.state 不应进入 SDK 公共 API。