Files
ReadViewSDK/ReadViewDemo/ReadViewDemoUITests/Helpers/XCUIApplication+Launch.swift
T
shenlei 3e60bf1869 修复阅读器资源、朗读与持久化稳定性
阅读器在多会话朗读、加密 EPUB 资源、并发打开同一本书和多 UserDefaults 容器等场景下,存在旧异步结果覆盖新状态、锁屏控制串会话、大型明文资源被误拒绝、解压半成品被复用及标注跨容器混用的风险;同时高亮、书签、搜索和设置面板的空状态与自动化可访问性入口不完整。\n\n本次为朗读会话引入代次和当前 utterance 校验,远程控制改为仅响应当前会话并按自身 token 清理;加密资源 provider 先判定是否实际返回解密数据,明文大资源继续流式读取;解压缓存串行化以避免并发复用未完成目录。书签与高亮迁移至受保护文件,按 UserDefaults 容器隔离命名空间,保留标准容器旧文件兼容并确保新副本成功落盘后才删除历史数据。\n\n同步调整缓存淘汰、FoundationModels 弱链接、搜索/标注/设置面板交互与 UI 测试,并更新 Pod 生成配置及 API、风险文档。已执行 git diff --check 和 ReadViewDemo Debug Simulator 构建,结果为 BUILD SUCCEEDED。
2026-07-30 20:52:21 +09:00

243 lines
8.7 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
extension XCUIApplication {
func launchAndOpenSamplePDF(
bookTitleQuery: String = "PDF阅读器示例",
resetsReaderState: Bool = true,
pdfTextSource: String? = nil
) {
launchAndOpenSampleBook(
bookTitleQuery: bookTitleQuery,
displayType: "pagecurl",
resetsReaderState: resetsReaderState,
pdfTextSource: pdfTextSource
)
}
func launchAndOpenSampleBook(
bookTitleQuery: String = "回归验证样本",
displayType: String? = nil,
pageNumber: Int? = nil,
resetsReaderState: Bool = true,
windowSize: Int? = nil,
concurrency: Int? = nil,
clearsCache: Bool = false,
corruptsCache: Bool = false,
allowsInspectableWebViews: Bool = false,
enablesVerboseWebLogs: Bool = false,
searchKeyword: String? = nil,
pdfTextSource: String? = nil
) {
var args = ["--demo-book-title", bookTitleQuery]
if resetsReaderState {
args.append("--demo-reset-state")
}
if clearsCache {
args.append("--demo-clear-cache")
}
if corruptsCache {
args.append("--demo-corrupt-cache")
}
if allowsInspectableWebViews {
args.append("--demo-allow-inspectable-webviews")
}
if enablesVerboseWebLogs {
args.append("--demo-enable-verbose-weblogs")
}
if let displayType {
args += ["--demo-display-type", displayType]
}
if let pageNumber {
args += ["--demo-page", "\(pageNumber)"]
}
if let windowSize {
args += ["--demo-window-size", "\(windowSize)"]
}
if let concurrency {
args += ["--demo-concurrency", "\(concurrency)"]
}
if let searchKeyword {
args += ["--demo-search-keyword", searchKeyword]
}
if let pdfTextSource {
args += ["--demo-pdf-text-source", pdfTextSource]
}
launchArguments = args
launch()
}
@discardableResult
func waitForReader(timeout: TimeInterval = 12) -> XCUIElement {
let state = staticTexts[IDs.demoReaderState]
XCTAssertTrue(state.waitForExistence(timeout: timeout), "阅读器状态标签未出现")
let parsed = DemoReaderState(rawValue: state.label)
XCTAssertTrue(parsed.isOpened, "阅读器未进入 opened 状态:\(state.label)")
return state
}
func waitForReaderPage(_ pageNumber: Int, timeout: TimeInterval = 12) {
_ = waitForDemoReaderState(timeout: timeout, description: "page=\(pageNumber)") { state in
state.page == pageNumber
}
}
func showReaderChromeIfNeeded(
requiredButtonIDs: [String] = [IDs.readerBack, IDs.readerSettings],
timeout: TimeInterval = 5
) {
if readerChromeIsVisible(requiredButtonIDs: requiredButtonIDs) {
return
}
let deadline = Date().addingTimeInterval(timeout)
while Date() < deadline {
let content = otherElements[IDs.readerContent].firstMatch
if content.waitForExistence(timeout: 1) {
content.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
} else {
windows.firstMatch.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
}
RunLoop.current.run(until: Date().addingTimeInterval(0.35))
if readerChromeIsVisible(requiredButtonIDs: requiredButtonIDs) {
return
}
}
}
func hideReaderChromeIfNeeded() {
for _ in 0..<3 {
guard buttons[IDs.readerBack].exists else { return }
let content = otherElements[IDs.readerContent].firstMatch
if content.waitForExistence(timeout: 2) {
content.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.6)).tap()
}
RunLoop.current.run(until: Date().addingTimeInterval(0.5))
}
}
func waitForReaderState(containing expectedText: String, timeout: TimeInterval = 8) {
let state = staticTexts[IDs.demoReaderState]
let deadline = Date().addingTimeInterval(timeout)
var lastLabel = "<missing>"
while Date() < deadline {
if state.exists {
lastLabel = state.label
if lastLabel.contains(expectedText) {
return
}
}
RunLoop.current.run(until: Date().addingTimeInterval(0.1))
}
XCTFail("阅读器状态未包含 \(expectedText),当前:\(lastLabel)")
}
@discardableResult
func requireSelectionText(
expectedPage: Int? = nil,
timeout: TimeInterval = 12
) throws -> XCUIElement {
let selectionText = otherElements[IDs.readerSelectionText].firstMatch
let deadline = Date().addingTimeInterval(timeout)
while Date() < deadline {
if let expectedPage {
let state = currentDemoReaderState()
if state?.page != expectedPage {
RunLoop.current.run(until: Date().addingTimeInterval(0.1))
continue
}
}
if selectionText.exists {
return selectionText
}
let content = otherElements[IDs.readerContent].firstMatch
if content.exists {
content.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
}
RunLoop.current.run(until: Date().addingTimeInterval(0.2))
}
throw XCTSkip("文本选区元素未出现,当前渲染路径未暴露 \(IDs.readerSelectionText)")
}
private func readerChromeIsVisible(requiredButtonIDs: [String]) -> Bool {
requiredButtonIDs.allSatisfy { buttons[$0].exists }
}
// MARK: - 设置面板
/// 等 EPUB 设置面板出现。面板已改成固定高度布局,所有控件一屏可见,
/// 既没有 scrollView 也不需要滚动才能触达。
@discardableResult
func waitForSettingsPanel(timeout: TimeInterval = 5) -> Bool {
if otherElements[IDs.settingsPanel].waitForExistence(timeout: timeout) {
return true
}
// 面板根视图偶尔不作为 otherElement 暴露,退回用「完成」按钮判定
return buttons[IDs.settingsDone].waitForExistence(timeout: 1)
}
/// 设置面板的字体/行距/翻页方式都是 pill 按钮 + action sheet,不是
/// segmentedControl:点开 pill 后再点选项标题。
@discardableResult
func selectSettingsPillOption(
pillIdentifier: String,
optionTitle: String,
timeout: TimeInterval = 5
) -> Bool {
let pill = buttons[pillIdentifier]
guard pill.waitForExistence(timeout: timeout) else { return false }
pill.tap()
// action sheet 的选项在 iOS 上是 button,取消项同名风险由标题唯一性保证
let option = sheets.buttons[optionTitle].exists
? sheets.buttons[optionTitle]
: buttons[optionTitle]
guard option.waitForExistence(timeout: timeout) else {
// 收起 sheet,避免污染后续断言
if sheets.buttons["取消"].exists {
sheets.buttons["取消"].tap()
}
return false
}
option.tap()
return true
}
/// 字号是 slider`epub.reader.settings.font.size`),没有增大/减小按钮。
/// 把滑块推到指定归一化位置以触发重排;返回是否成功。
@discardableResult
func adjustSettingsFontSize(toNormalizedPosition position: CGFloat, timeout: TimeInterval = 5) -> Bool {
let slider = sliders[IDs.settingsFontSize]
guard slider.waitForExistence(timeout: timeout) else { return false }
slider.adjust(toNormalizedSliderPosition: position)
return true
}
/// 在当前值基础上增减字号,替代早已不存在的 font.increase / font.decrease 按钮。
@discardableResult
func nudgeSettingsFontSize(by delta: CGFloat, timeout: TimeInterval = 5) -> Bool {
let slider = sliders[IDs.settingsFontSize]
guard slider.waitForExistence(timeout: timeout) else { return false }
let target = min(max(slider.normalizedSliderPosition + delta, 0), 1)
slider.adjust(toNormalizedSliderPosition: target)
return true
}
@discardableResult
func increaseSettingsFontSize(timeout: TimeInterval = 5) -> Bool {
nudgeSettingsFontSize(by: 0.25, timeout: timeout)
}
@discardableResult
func decreaseSettingsFontSize(timeout: TimeInterval = 5) -> Bool {
nudgeSettingsFontSize(by: -0.25, timeout: timeout)
}
}