ReadViewSDK/ReadViewDemo/ReadViewDemoUITests/Helpers/XCUIApplication+Launch.swift
shen d20196ee34 feat: configurable chapter window & parallel metadata parsing with benchmark
1. Configurable chapter window size (onDemandChapterWindowSize: 3-15)
   - Parameterized window radius in RDEPUBChapterRuntimeStore
   - Updated RDEPUBChapterWindowCoordinator to use configurable radius
   - RDEPUBChapterWindowSnapshot.from() accepts chapter array instead of fixed prev/next
   - Even numbers round up to odd (4→5), min 3, max 15

2. Configurable metadata parsing concurrency (metadataParsingConcurrency)
   - Default equals CPU core count
   - Parallel execution via OperationQueue in paginateMetadataOnly
   - Each worker creates independent builder instance
   - NSLock protects result aggregation

3. Per-chapter and total wall-clock timing instrumentation
   - Separated render vs I/O timing per chapter
   - Summary log with wallClockMs, renderTotalMs, writeTotalMs, avgRenderMs
   - Timing stored in RDEPUBReaderContext for test access

4. UI automation test infrastructure
   - Added --demo-window-size, --demo-concurrency, --demo-clear-cache launch args
   - DemoReaderState exposes windowSize, parseMs, parseConcurrency
   - ConfigurableWindowTests: 5 test cases for window size 3/5/15
   - ConcurrentParsingTests: 4 test cases for concurrency 2/4
   - MetadataParseBenchmarkTests: serial vs parallel benchmark

5. Bug fixes
   - Fixed page snap-back during background parsing (isUserInteracting check)
   - Reduced BookPageMap refresh frequency from 16 to 32 chapters
   - Moved waitForReadingInteractionToSettle outside operation loop

6. Design doc: dual-layer PageMap (estimated + precise mixed)
2026-06-03 23:38:11 +08:00

94 lines
3.1 KiB
Swift

import XCTest
extension XCUIApplication {
func launchAndOpenSampleBook(
bookTitleQuery: String = "回归验证样本",
displayType: String? = nil,
pageNumber: Int? = nil,
resetsReaderState: Bool = true,
windowSize: Int? = nil,
concurrency: Int? = nil,
clearsCache: Bool = false
) {
var args = ["--demo-book-title", bookTitleQuery]
if resetsReaderState {
args.append("--demo-reset-state")
}
if clearsCache {
args.append("--demo-clear-cache")
}
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)"]
}
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() {
if buttons[IDs.readerBack].exists && buttons[IDs.readerSettings].exists {
return
}
let content = otherElements[IDs.readerContent]
if content.waitForExistence(timeout: 3) {
content.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
} else {
windows.firstMatch.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
}
}
func hideReaderChromeIfNeeded() {
for _ in 0..<3 {
guard buttons[IDs.readerBack].exists else { return }
let content = otherElements[IDs.readerContent]
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)")
}
}