import XCTest final class ReaderToolbarTests: XCTestCase { private let app = XCUIApplication() override func setUpWithError() throws { continueAfterFailure = false } func testTapCenterShowsToolbars() { app.launchAndOpenSampleBook() app.waitForReader() app.showReaderChromeIfNeeded() XCTAssertTrue(app.otherElements[IDs.readerTopToolbar].waitForExistence(timeout: 5)) XCTAssertTrue(app.otherElements[IDs.readerBottomToolbar].waitForExistence(timeout: 5)) XCTAssertTrue(app.buttons[IDs.readerBack].exists) XCTAssertTrue(app.buttons[IDs.readerSettings].exists) } func testPDFTapCenterShowsAndHidesToolbars() { app.launchAndOpenSamplePDF() app.waitForReader() app.waitForDemoReaderState(timeout: 5, description: "mode=pdf") { $0.mode == "pdf" } app.showReaderChromeIfNeeded() XCTAssertTrue(app.otherElements[IDs.readerTopToolbar].waitForExistence(timeout: 3)) XCTAssertTrue(app.otherElements[IDs.readerBottomToolbar].waitForExistence(timeout: 3)) app.hideReaderChromeIfNeeded() XCTAssertFalse(app.buttons[IDs.readerBack].waitForExistence(timeout: 2), "PDF 点击内容区后工具栏应隐藏") app.waitForDemoReaderState(timeout: 5, description: "toolbar=hidden") { $0.toolbar == "hidden" } } func testPDFTocButtonOpensNavigationSheet() { app.launchAndOpenSamplePDF() app.waitForReader() app.waitForDemoReaderState(timeout: 5, description: "mode=pdf") { $0.mode == "pdf" } app.showReaderChromeIfNeeded(requiredButtonIDs: [IDs.readerToc]) let tocButton = app.buttons[IDs.readerToc] XCTAssertTrue(tocButton.waitForExistence(timeout: 5)) tocButton.tap() XCTAssertTrue(app.tables[IDs.readerTocTable].waitForExistence(timeout: 5), "PDF 目录面板未出现") } func testPDFSettingsPanelProvidesUsableOptions() { app.launchAndOpenSamplePDF() app.waitForReader() app.waitForDemoReaderState(timeout: 5, description: "mode=pdf") { $0.mode == "pdf" } app.showReaderChromeIfNeeded(requiredButtonIDs: [IDs.readerSettings]) let settingsButton = app.buttons[IDs.readerSettings] XCTAssertTrue(settingsButton.waitForExistence(timeout: 5)) settingsButton.tap() let settingsPanel = app.scrollViews[IDs.settingsScroll] XCTAssertTrue(settingsPanel.waitForExistence(timeout: 5), "PDF 设置面板未出现") XCTAssertTrue(app.sliders[IDs.settingsBrightness].waitForExistence(timeout: 3), "PDF 亮度设置未出现") XCTAssertTrue(app.segmentedControls[IDs.settingsDisplayType].waitForExistence(timeout: 3), "PDF 翻页方式设置未出现") XCTAssertTrue(app.buttons[IDs.settingsTheme(5)].waitForExistence(timeout: 3), "PDF 主题设置未出现") app.segmentedControls[IDs.settingsDisplayType].buttons["竖滑"].tap() app.buttons[IDs.settingsTheme(5)].tap() app.buttons[IDs.settingsDone].tap() app.waitForDemoReaderState(timeout: 5, description: "display changed") { $0.display == "verticalscroll" } app.waitForDemoReaderState(timeout: 5, description: "theme changed") { $0.fields["theme"] == "5" } } func testPDFLaunchHonorsDisplayTypeArgument() { app.launchAndOpenSampleBook(bookTitleQuery: "PDF测试文件", displayType: "verticalscroll") app.waitForReader() app.waitForDemoReaderState(timeout: 5, description: "PDF vertical launch mode") { $0.mode == "pdf" && $0.display == "verticalscroll" } } func testPDFPageCurlSupportsPinchAndDraggingAfterDoubleTapZoom() { app.launchAndOpenSamplePDF(bookTitleQuery: "PDF测试文件") app.waitForReader() app.waitForDemoReaderState(timeout: 5, description: "PDF page curl") { $0.mode == "pdf" && $0.display == "pagecurl" } let content = app.otherElements[IDs.readerContent].firstMatch XCTAssertTrue(content.waitForExistence(timeout: 5), "PDF 阅读页面未出现") XCTAssertTrue(waitForViewport(content, timeout: 3) { $0.scale <= 1.05 }, "PDF 初始页面不是适配状态") content.pinch(withScale: 1.8, velocity: 1) XCTAssertTrue(waitForViewport(content, timeout: 3) { $0.scale >= 1.4 }, "仿真翻页下双指缩放未生效") assertReaderPageRemains(1, duration: 0.4) content.doubleTap() XCTAssertTrue(waitForViewport(content, timeout: 3) { $0.scale <= 1.05 }, "双击未能还原 PDF 页面") content.doubleTap() guard let beforeDrag = waitForStableViewport(content, timeout: 3, stableDuration: 0.3, where: { $0.scale >= 1.8 }) else { XCTFail("双击后 PDF 未进入稳定的放大状态") return } let dragStart = content.coordinate(withNormalizedOffset: CGVector(dx: 0.75, dy: 0.5)) let dragEnd = content.coordinate(withNormalizedOffset: CGVector(dx: 0.25, dy: 0.5)) dragStart.press(forDuration: 0.05, thenDragTo: dragEnd) let didMoveContent = waitForViewport(content, timeout: 3) { $0.scale >= 1.8 && $0.offsetX - beforeDrag.offsetX > 30 } XCTAssertTrue( didMoveContent, "PDF 双击放大后无法拖动内容;拖动前=\(beforeDrag),拖动后=\(String(describing: content.value))" ) assertReaderPageRemains(1, duration: 0.4) } func testPDFPageCurlSingleFingerTurnsPageAtFittedScale() { app.launchAndOpenSamplePDF(bookTitleQuery: "PDF测试文件") app.waitForReader() app.waitForDemoReaderState(timeout: 5, description: "PDF page curl at page 1") { $0.mode == "pdf" && $0.display == "pagecurl" && $0.page == 1 } let content = app.otherElements[IDs.readerContent].firstMatch XCTAssertTrue(content.waitForExistence(timeout: 5), "PDF 阅读页面未出现") XCTAssertTrue(waitForViewport(content, timeout: 3) { $0.scale <= 1.05 }, "PDF 初始页面不是适配状态") let dragStart = content.coordinate(withNormalizedOffset: CGVector(dx: 0.60, dy: 0.5)) let dragEnd = content.coordinate(withNormalizedOffset: CGVector(dx: 0.20, dy: 0.5)) dragStart.press(forDuration: 0.05, thenDragTo: dragEnd) app.waitForReaderPage(2, timeout: 5) } func testPDFSwitchingFromZoomedCurlKeepsScrollModeInteractive() { app.launchAndOpenSamplePDF(bookTitleQuery: "PDF测试文件") app.waitForReader() app.waitForDemoReaderState(timeout: 5, description: "PDF page curl at page 1") { $0.mode == "pdf" && $0.display == "pagecurl" && $0.page == 1 } let content = app.otherElements[IDs.readerContent].firstMatch XCTAssertTrue(content.waitForExistence(timeout: 5), "PDF 阅读页面未出现") content.doubleTap() XCTAssertNotNil( waitForStableViewport(content, timeout: 3, stableDuration: 0.3, where: { $0.scale >= 1.8 }), "PDF 双击后未放大" ) app.showReaderChromeIfNeeded(requiredButtonIDs: [IDs.readerSettings]) app.buttons[IDs.readerSettings].tap() XCTAssertTrue(app.scrollViews[IDs.settingsScroll].waitForExistence(timeout: 5), "PDF 设置面板未出现") app.segmentedControls[IDs.settingsDisplayType].buttons["横滑"].tap() app.buttons[IDs.settingsDone].tap() app.waitForDemoReaderState(timeout: 5, description: "display=horizontalscroll") { $0.display == "horizontalscroll" && $0.page == 1 } let paging = app.collectionViews[IDs.readerPaging] XCTAssertTrue(paging.waitForExistence(timeout: 5), "PDF 横滑视图未出现") paging.swipeLeft() app.waitForReaderPage(2, timeout: 5) } private func waitForStableViewport( _ element: XCUIElement, timeout: TimeInterval, stableDuration: TimeInterval, where predicate: ((scale: Double, offsetX: Double, offsetY: Double)) -> Bool ) -> (scale: Double, offsetX: Double, offsetY: Double)? { let deadline = Date().addingTimeInterval(timeout) var previous: (scale: Double, offsetX: Double, offsetY: Double)? var stableSince: Date? repeat { guard let current = viewport(of: element), predicate(current) else { previous = nil stableSince = nil RunLoop.current.run(until: Date().addingTimeInterval(0.05)) continue } if let previous, abs(current.scale - previous.scale) <= 0.01, abs(current.offsetX - previous.offsetX) <= 1, abs(current.offsetY - previous.offsetY) <= 1 { if let stableSince, Date().timeIntervalSince(stableSince) >= stableDuration { return current } } else { stableSince = Date() } previous = current RunLoop.current.run(until: Date().addingTimeInterval(0.05)) } while Date() < deadline return nil } private func assertReaderPageRemains( _ page: Int, duration: TimeInterval, file: StaticString = #filePath, line: UInt = #line ) { let deadline = Date().addingTimeInterval(duration) repeat { guard let state = app.currentDemoReaderState(), state.page == page else { XCTFail( "PDF 手势后发生了意外翻页,当前状态:\(app.currentDemoReaderState()?.rawValue ?? "")", file: file, line: line ) return } RunLoop.current.run(until: Date().addingTimeInterval(0.05)) } while Date() < deadline } private func waitForViewport( _ element: XCUIElement, timeout: TimeInterval, predicate: ((scale: Double, offsetX: Double, offsetY: Double)) -> Bool ) -> Bool { let deadline = Date().addingTimeInterval(timeout) repeat { if let viewport = viewport(of: element), predicate(viewport) { return true } RunLoop.current.run(until: Date().addingTimeInterval(0.1)) } while Date() < deadline return false } private func viewport(of element: XCUIElement) -> (scale: Double, offsetX: Double, offsetY: Double)? { guard let value = element.value as? String else { return nil } let pattern = #"zoom=([0-9.]+) offset=(-?[0-9.]+),(-?[0-9.]+)"# guard let regex = try? NSRegularExpression(pattern: pattern), let match = regex.firstMatch(in: value, range: NSRange(value.startIndex..., in: value)), let scaleRange = Range(match.range(at: 1), in: value), let xRange = Range(match.range(at: 2), in: value), let yRange = Range(match.range(at: 3), in: value), let scale = Double(value[scaleRange]), let offsetX = Double(value[xRange]), let offsetY = Double(value[yRange]) else { return nil } return (scale, offsetX, offsetY) } }