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)
75 lines
2.9 KiB
Swift
75 lines
2.9 KiB
Swift
import XCTest
|
|
|
|
struct DemoReaderState {
|
|
let rawValue: String
|
|
let fields: [String: String]
|
|
|
|
init(rawValue: String) {
|
|
self.rawValue = rawValue
|
|
var parsed: [String: String] = [:]
|
|
let separators = CharacterSet.whitespacesAndNewlines.union(CharacterSet(charactersIn: ";"))
|
|
for token in rawValue.components(separatedBy: separators) where !token.isEmpty {
|
|
guard let delimiterIndex = token.firstIndex(of: "=") else { continue }
|
|
let key = String(token[..<delimiterIndex])
|
|
let value = String(token[token.index(after: delimiterIndex)...])
|
|
parsed[key] = value
|
|
}
|
|
fields = parsed
|
|
}
|
|
|
|
subscript(key: String) -> String? {
|
|
fields[key]
|
|
}
|
|
|
|
var isOpened: Bool { fields["reader"] == "opened" }
|
|
var page: Int? { fields["page"].flatMap(Int.init) }
|
|
var display: String? { fields["display"] }
|
|
var toolbar: String? { fields["toolbar"] }
|
|
var highlights: Int? { fields["highlights"].flatMap(Int.init) }
|
|
var selection: Int? { fields["selection"].flatMap(Int.init) }
|
|
var href: String? { fields["href"] }
|
|
var progression: Double? { fields["progression"].flatMap(Double.init) }
|
|
var mode: String? { fields["mode"] }
|
|
var pagination: String? { fields["pagination"] }
|
|
var knownPages: Int? { fields["knownPages"].flatMap(Int.init) }
|
|
var knownChapters: Int? { fields["knownChapters"].flatMap(Int.init) }
|
|
var buildableChapters: Int? { fields["buildableChapters"].flatMap(Int.init) }
|
|
var avoidWidows: Int? { fields["avoidWidows"].flatMap(Int.init) }
|
|
var avoidOrphans: Int? { fields["avoidOrphans"].flatMap(Int.init) }
|
|
var windowSize: Int? { fields["windowSize"].flatMap(Int.init) }
|
|
var parseMs: Int? { fields["parseMs"].flatMap(Int.init) }
|
|
var parseConcurrency: Int? { fields["parseConcurrency"].flatMap(Int.init) }
|
|
}
|
|
|
|
extension XCUIApplication {
|
|
func currentDemoReaderState() -> DemoReaderState? {
|
|
let state = staticTexts[IDs.demoReaderState]
|
|
guard state.exists else { return nil }
|
|
return DemoReaderState(rawValue: state.label)
|
|
}
|
|
|
|
@discardableResult
|
|
func waitForDemoReaderState(
|
|
timeout: TimeInterval = 8,
|
|
description: String,
|
|
where predicate: (DemoReaderState) -> Bool
|
|
) -> DemoReaderState {
|
|
let stateElement = staticTexts[IDs.demoReaderState]
|
|
let deadline = Date().addingTimeInterval(timeout)
|
|
var lastState = DemoReaderState(rawValue: "<missing>")
|
|
|
|
while Date() < deadline {
|
|
if stateElement.exists {
|
|
lastState = DemoReaderState(rawValue: stateElement.label)
|
|
if predicate(lastState) {
|
|
return lastState
|
|
}
|
|
}
|
|
RunLoop.current.run(until: Date().addingTimeInterval(0.1))
|
|
}
|
|
|
|
XCTFail("阅读器状态未满足条件: \(description),当前:\(lastState.rawValue)")
|
|
return lastState
|
|
}
|
|
}
|