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[.. String? { fields[key] } private func decodedField(_ key: String) -> String? { guard let rawValue = fields[key], rawValue != "nil" else { return nil } return rawValue.removingPercentEncoding ?? rawValue } 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 bookmarks: Int? { fields["bookmarks"].flatMap(Int.init) } var selection: Int? { fields["selection"].flatMap(Int.init) } var href: String? { decodedField("href") } var progression: Double? { fields["progression"].flatMap(Double.init) } var cfi: String? { decodedField("cfi") } var lastCFI: String? { decodedField("lastCFI") } var rangeCFI: String? { decodedField("rangeCFI") } 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) } var inspectable: Int? { fields["inspectable"].flatMap(Int.init) } var streamedResources: Int? { fields["streamedResources"].flatMap(Int.init) } var inMemoryResources: Int? { fields["inMemoryResources"].flatMap(Int.init) } var resourceFailures: Int? { fields["resourceFailures"].flatMap(Int.init) } var cacheFiles: Int? { fields["cacheFiles"].flatMap(Int.init) } var cacheBytes: Int? { fields["cacheBytes"].flatMap(Int.init) } var externalLinks: Int? { fields["externalLinks"].flatMap(Int.init) } var lastExternalURL: String? { decodedField("lastExternalURL") } var lastError: String? { decodedField("lastError") } var searchMatchText: String? { decodedField("searchMatchText") } var footprintMB: Double? { fields["footprintMB"].flatMap(Double.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: "") 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 } }