ReadViewSDK/ReadViewDemo/ReadViewDemoUITests/ReaderUITests/PDFAnnotationTests.swift
shenlei 1422461224 将 PDF 成品阅读控制器下沉为 RDPDFReaderView SDK,并修复手机横屏单页连续竖滑
SDK 化收尾:
- 新增 RDPDFReaderView 本地 pod(阅读容器、成品控制器、划线/注释、画笔、
  目录/设置面板、主题与持久化契约),Demo 精简为 PDFKit 页面提供者 + 持久化宿主
- Podfile/工程接入 RDPDFReaderView 与 SnapKit,示例书改用 PDF 阅读器示例文件

手机横屏单页竖滑修复:
- cell 高度改为宽度适配后的纸张高度(sizeForItemAt + 宿主按页提供纵横比),
  纵向超出一屏的内容由外层 collectionView 连续滚动承接,不再被裁掉
- 页面内部不再开启同向嵌套的兜底竖向拖动,修复外层滚动被吞、无法滚到下一页
- 修复 applyContentSize 先评估拖动开关后写 contentSize 的顺序缺陷
- 页码按视口中心命中 cell 计算,补 didEndDragging 兜底;旋转重锚点与
  transitionToPage 改用真实布局位置
- 横竖屏同为竖滑时也重建 cell 并刷新全部可见页的适配配置
- 横屏单页点击不再跳页,只显隐工具栏;翻页交给连续竖滑

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 18:49:44 +09:00

241 lines
10 KiB
Swift
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.

import XCTest
/// PDF
///
/// `host-fixture` 1
/// `(0.18, 0.20)...(0.65, 0.20)`
/// `none` /OCR 使 `(0.15, 0.70)...(0.60, 0.84)`
final class PDFAnnotationTests: XCTestCase {
private let app = XCUIApplication()
override func setUpWithError() throws {
continueAfterFailure = false
}
func testPDFHostFixtureLongPressSelectsTextWithoutTurningPage() {
let textLayer = openPDF(textSource: "host-fixture")
selectFixtureText(in: textLayer)
let fields = waitForTextLayer(textLayer, description: "host text selection") {
$0["source"] == "text" && ($0["rects"].flatMap(Int.init) ?? 0) > 0
}
XCTAssertFalse(fields["text", default: ""].isEmpty, "文字选区应公开已选文本")
assertReaderPageRemains(1, duration: 0.6)
}
func testPDFHostFixtureCopiesSelectedText() {
let textLayer = openPDF(textSource: "host-fixture")
selectFixtureText(in: textLayer)
let copyMenuItem = app.menuItems[IDs.pdfSelectionMenuCopy]
XCTAssertTrue(copyMenuItem.waitForExistence(timeout: 5), "选区菜单缺少复制选项")
copyMenuItem.tap()
let state = app.waitForDemoReaderState(timeout: 5, description: "copy=success and copiedText") {
$0["copy"] == "success" && !($0["copiedText"] ?? "").isEmpty
}
XCTAssertFalse((state["copiedText"] ?? "").isEmpty, "复制成功后应记录被复制的文本")
}
func testPDFHostFixtureCreatesTextHighlight() {
let textLayer = openPDF(textSource: "host-fixture")
selectFixtureText(in: textLayer)
let highlightMenuItem = app.menuItems[IDs.pdfSelectionMenuHighlight]
XCTAssertTrue(highlightMenuItem.waitForExistence(timeout: 5), "选区菜单缺少高亮选项")
highlightMenuItem.tap()
let fields = waitForTextLayer(textLayer, description: "one saved text highlight") {
($0["annotations"].flatMap(Int.init) ?? 0) == 1
}
XCTAssertEqual(fields["notes"], "0", "纯高亮不应额外创建笔记")
}
func testPDFAnnotationPersistsAfterClosingAndReopeningBook() {
let textLayer = openPDF(textSource: "host-fixture")
selectFixtureText(in: textLayer)
let highlightMenuItem = app.menuItems[IDs.pdfSelectionMenuHighlight]
XCTAssertTrue(highlightMenuItem.waitForExistence(timeout: 5))
highlightMenuItem.tap()
_ = waitForTextLayer(textLayer, description: "saved annotation before reopening") {
($0["annotations"].flatMap(Int.init) ?? 0) == 1
}
// reset
app.terminate()
app.launchAndOpenSamplePDF(
bookTitleQuery: "PDF测试文件",
resetsReaderState: false,
pdfTextSource: "host-fixture"
)
app.waitForReader()
let reopenedTextLayer = app.otherElements[IDs.pdfTextLayer].firstMatch
XCTAssertTrue(reopenedTextLayer.waitForExistence(timeout: 8))
_ = waitForTextLayer(reopenedTextLayer, description: "persisted annotation after reopening") {
($0["annotations"].flatMap(Int.init) ?? 0) == 1
}
}
func testPDFAnnotationsPanelListsSavedHighlight() {
let textLayer = openPDF(textSource: "host-fixture")
selectFixtureText(in: textLayer)
let highlightMenuItem = app.menuItems[IDs.pdfSelectionMenuHighlight]
XCTAssertTrue(highlightMenuItem.waitForExistence(timeout: 5))
highlightMenuItem.tap()
_ = waitForTextLayer(textLayer, description: "saved highlight before opening annotations") {
($0["annotations"].flatMap(Int.init) ?? 0) == 1
}
app.showReaderChromeIfNeeded(requiredButtonIDs: [IDs.pdfAnnotations])
let annotationsButton = app.buttons[IDs.pdfAnnotations]
XCTAssertTrue(annotationsButton.waitForExistence(timeout: 5), "底部工具栏缺少笔记入口")
annotationsButton.tap()
let table = app.tables[IDs.pdfAnnotationsTable]
XCTAssertTrue(table.waitForExistence(timeout: 5), "笔记列表页未打开")
XCTAssertGreaterThan(table.cells.count, 0, "笔记列表应显示刚创建的高亮")
}
func testPDFHostFixtureCreatesAnnotation() {
let textLayer = openPDF(textSource: "host-fixture")
selectFixtureText(in: textLayer)
let annotateMenuItem = app.menuItems[IDs.pdfSelectionMenuAnnotate]
XCTAssertTrue(annotateMenuItem.waitForExistence(timeout: 5), "选区菜单缺少注释选项")
annotateMenuItem.tap()
let editor = app.otherElements[IDs.pdfAnnotationEditor]
XCTAssertTrue(editor.waitForExistence(timeout: 5), "注释编辑页未出现")
let input = app.textViews[IDs.pdfAnnotationInput]
XCTAssertTrue(input.waitForExistence(timeout: 5), "注释输入框未出现")
input.tap()
input.typeText("fixture note")
let saveButton = app.buttons[IDs.pdfAnnotationSave]
XCTAssertTrue(saveButton.waitForExistence(timeout: 5), "注释编辑页缺少保存按钮")
saveButton.tap()
let fields = waitForTextLayer(textLayer, description: "one saved note annotation") {
($0["annotations"].flatMap(Int.init) ?? 0) == 1
&& ($0["notes"].flatMap(Int.init) ?? 0) == 1
}
XCTAssertEqual(fields["notes"], "1", "保存注释后页面应显示笔记标记")
}
func testPDFWithoutOCRCreatesRegionHighlightWithoutCopy() {
let textLayer = openPDF(textSource: "none")
_ = waitForTextLayer(textLayer, description: "no OCR text runs") {
($0["textRuns"].flatMap(Int.init) ?? -1) == 0
}
let start = textLayer.coordinate(withNormalizedOffset: CGVector(dx: 0.15, dy: 0.70))
let end = textLayer.coordinate(withNormalizedOffset: CGVector(dx: 0.60, dy: 0.84))
start.press(forDuration: 0.55, thenDragTo: end)
_ = waitForTextLayer(textLayer, description: "region selection") {
$0["source"] == "region" && ($0["rects"].flatMap(Int.init) ?? 0) > 0
}
let highlightMenuItem = app.menuItems[IDs.pdfSelectionMenuHighlight]
XCTAssertTrue(highlightMenuItem.waitForExistence(timeout: 5), "区域选区应弹出带高亮选项的菜单")
XCTAssertFalse(
app.menuItems[IDs.pdfSelectionMenuCopy].waitForExistence(timeout: 1),
"无 OCR 的区域选区不应提供复制选项"
)
highlightMenuItem.tap()
_ = waitForTextLayer(textLayer, description: "one saved region highlight") {
($0["annotations"].flatMap(Int.init) ?? 0) == 1
}
assertReaderPageRemains(1, duration: 0.4)
}
@discardableResult
private func openPDF(textSource: String) -> XCUIElement {
app.launchAndOpenSamplePDF(
bookTitleQuery: "PDF测试文件",
resetsReaderState: true,
pdfTextSource: textSource
)
app.waitForReader()
app.waitForDemoReaderState(timeout: 8, description: "PDF page 1") {
$0.mode == "pdf" && $0.page == 1
}
let textLayer = app.otherElements[IDs.pdfTextLayer].firstMatch
XCTAssertTrue(textLayer.waitForExistence(timeout: 8), "PDF 图片文字层未出现")
return textLayer
}
private func selectFixtureText(in textLayer: XCUIElement) {
_ = waitForTextLayer(textLayer, description: "host fixture text runs") {
($0["textRuns"].flatMap(Int.init) ?? 0) >= 2
}
let start = textLayer.coordinate(withNormalizedOffset: CGVector(dx: 0.18, dy: 0.20))
let end = textLayer.coordinate(withNormalizedOffset: CGVector(dx: 0.65, dy: 0.20))
start.press(forDuration: 0.55, thenDragTo: end)
XCTAssertTrue(
app.menuItems[IDs.pdfSelectionMenuHighlight].waitForExistence(timeout: 5),
"长按拖选文字后未弹出选区菜单"
)
}
@discardableResult
private func waitForTextLayer(
_ textLayer: XCUIElement,
timeout: TimeInterval = 6,
description: String,
where predicate: ([String: String]) -> Bool
) -> [String: String] {
let deadline = Date().addingTimeInterval(timeout)
var lastFields: [String: String] = [:]
while Date() < deadline {
lastFields = textLayerFields(textLayer)
if predicate(lastFields) {
return lastFields
}
RunLoop.current.run(until: Date().addingTimeInterval(0.1))
}
XCTFail("PDF 文本层未满足 \(description),当前:\(String(describing: textLayer.value))")
return lastFields
}
private func textLayerFields(_ textLayer: XCUIElement) -> [String: String] {
guard let rawValue = textLayer.value as? String else { return [:] }
return rawValue
.split(separator: ";", omittingEmptySubsequences: true)
.reduce(into: [:]) { fields, token in
let parts = token.split(separator: "=", maxSplits: 1, omittingEmptySubsequences: false)
guard parts.count == 2 else { return }
fields[String(parts[0])] = String(parts[1])
}
}
private func assertReaderPageRemains(
_ page: Int,
duration: TimeInterval,
file: StaticString = #filePath,
line: UInt = #line
) {
let deadline = Date().addingTimeInterval(duration)
repeat {
guard app.currentDemoReaderState()?.page == page else {
XCTFail(
"PDF 选择手势意外翻页,当前状态:\(app.currentDemoReaderState()?.rawValue ?? "<missing>")",
file: file,
line: line
)
return
}
RunLoop.current.run(until: Date().addingTimeInterval(0.1))
} while Date() < deadline
}
}