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, corruptsCache: Bool = false, allowsInspectableWebViews: Bool = false, enablesVerboseWebLogs: Bool = false, searchKeyword: 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] } 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] 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] 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 = "" 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] 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 } } }