import XCTest final class SearchTests: XCTestCase { private let app = XCUIApplication() override func setUpWithError() throws { continueAfterFailure = false } // MARK: - 搜索入口 func testSearchButtonExistsInToolbar() { app.launchAndOpenSampleBook() app.waitForReader() app.showReaderChromeIfNeeded() let searchButton = app.buttons[IDs.readerSearch] XCTAssertTrue(searchButton.waitForExistence(timeout: 5), "搜索按钮应出现在顶部工具栏") } // MARK: - 搜索栏显示/隐藏 func testTapSearchButtonShowsSearchBar() { app.launchAndOpenSampleBook() app.waitForReader() app.showReaderChromeIfNeeded() let searchButton = app.buttons[IDs.readerSearch] XCTAssertTrue(searchButton.waitForExistence(timeout: 5), "搜索按钮应存在") searchButton.tap() let searchBar = app.otherElements[IDs.searchBar] XCTAssertTrue(searchBar.waitForExistence(timeout: 3), "点击搜索按钮后搜索栏应出现") } func testSearchBarAutoFocusesTextField() { app.launchAndOpenSampleBook() app.waitForReader() app.showReaderChromeIfNeeded() let searchButton = app.buttons[IDs.readerSearch] XCTAssertTrue(searchButton.waitForExistence(timeout: 5)) searchButton.tap() let searchBar = app.otherElements[IDs.searchBar] XCTAssertTrue(searchBar.waitForExistence(timeout: 3), "搜索栏应出现") XCTAssertTrue(app.keyboards.firstMatch.waitForExistence(timeout: 3), "搜索栏出现后键盘应弹出") } func testCloseButtonHidesSearchBar() { app.launchAndOpenSampleBook() app.waitForReader() app.showReaderChromeIfNeeded() openSearchBar() let closeButton = app.buttons[IDs.searchClose] XCTAssertTrue(closeButton.waitForExistence(timeout: 3), "关闭按钮应存在") closeButton.tap() let searchBar = app.otherElements[IDs.searchBar] XCTAssertFalse(searchBar.waitForExistence(timeout: 2), "点击关闭后搜索栏应消失") } // MARK: - 搜索执行与匹配(通过 launch argument 触发搜索) func testSearchFindsMatchesAndShowsCount() { app.launchAndOpenSampleBook(searchKeyword: "的") app.waitForReader() app.showReaderChromeIfNeeded() let countLabel = app.staticTexts[IDs.searchCount] XCTAssertTrue(countLabel.waitForExistence(timeout: 10), "匹配计数标签应出现") let countText = countLabel.label XCTAssertTrue(countText.contains("/"), "计数格式应为 'N/M',实际:\(countText)") let parts = countText.split(separator: "/") XCTAssertEqual(parts.count, 2, "计数应包含两部分,实际:\(countText)") if let total = Int(parts[1]) { XCTAssertGreaterThan(total, 0, "关键词 '的' 应有匹配结果") } } func testSearchNavigatesToFirstMatch() { app.launchAndOpenSampleBook(searchKeyword: "的") app.waitForReader() app.showReaderChromeIfNeeded() let countLabel = app.staticTexts[IDs.searchCount] XCTAssertTrue(countLabel.waitForExistence(timeout: 10), "匹配计数应出现") let countText = countLabel.label XCTAssertTrue(countText.hasPrefix("1/"), "搜索后应自动跳转到第一个匹配,当前:\(countText)") } func testSearchNextButtonAdvancesMatch() { app.launchAndOpenSampleBook(searchKeyword: "的") app.waitForReader() app.showReaderChromeIfNeeded() let countLabel = app.staticTexts[IDs.searchCount] XCTAssertTrue(countLabel.waitForExistence(timeout: 10)) XCTAssertTrue(countLabel.label.hasPrefix("1/"), "初始应在第 1 个匹配") let nextButton = app.buttons[IDs.searchNext] XCTAssertTrue(nextButton.waitForExistence(timeout: 3)) nextButton.tap() XCTAssertTrue(countLabel.label.hasPrefix("2/"), "点击下一个后应变为第 2 个匹配,当前:\(countLabel.label)") } func testSearchPreviousButtonGoesBack() { app.launchAndOpenSampleBook(searchKeyword: "的") app.waitForReader() app.showReaderChromeIfNeeded() let countLabel = app.staticTexts[IDs.searchCount] XCTAssertTrue(countLabel.waitForExistence(timeout: 10)) // 前进到第 2 个 let nextButton = app.buttons[IDs.searchNext] XCTAssertTrue(nextButton.waitForExistence(timeout: 3)) nextButton.tap() XCTAssertTrue(countLabel.label.hasPrefix("2/")) // 后退到第 1 个 let previousButton = app.buttons[IDs.searchPrevious] XCTAssertTrue(previousButton.waitForExistence(timeout: 3)) previousButton.tap() XCTAssertTrue(countLabel.label.hasPrefix("1/"), "点击上一个后应回到第 1 个匹配,当前:\(countLabel.label)") } func testSearchNextWrapsAround() { app.launchAndOpenSampleBook(searchKeyword: "的") app.waitForReader() app.showReaderChromeIfNeeded() let countLabel = app.staticTexts[IDs.searchCount] XCTAssertTrue(countLabel.waitForExistence(timeout: 10)) let countText = countLabel.label let parts = countText.split(separator: "/") guard parts.count == 2, let total = Int(parts[1]), total > 1 else { XCTSkip("需要至少 2 个匹配才能测试循环") return } // 前进到最后一个 let nextButton = app.buttons[IDs.searchNext] for _ in 0..<(total - 1) { nextButton.tap() RunLoop.current.run(until: Date().addingTimeInterval(0.3)) } XCTAssertTrue(countLabel.label.hasPrefix("\(total)/"), "应到达最后一个匹配") // 再前进一次应循环到第 1 个 nextButton.tap() XCTAssertTrue(countLabel.label.hasPrefix("1/"), "超过最后一个后应循环到第 1 个,当前:\(countLabel.label)") } // MARK: - 空结果 func testSearchWithNoResultsShowsZeroCount() { app.launchAndOpenSampleBook(searchKeyword: "xyzzy_nonexistent_keyword_12345") app.waitForReader() app.showReaderChromeIfNeeded() let countLabel = app.staticTexts[IDs.searchCount] XCTAssertTrue(countLabel.waitForExistence(timeout: 10), "计数标签应出现") XCTAssertEqual(countLabel.label, "0/0", "无匹配时应显示 0/0") } func testSearchNavigationDisabledWhenNoResults() { app.launchAndOpenSampleBook(searchKeyword: "xyzzy_nonexistent_keyword_12345") app.waitForReader() app.showReaderChromeIfNeeded() let countLabel = app.staticTexts[IDs.searchCount] XCTAssertTrue(countLabel.waitForExistence(timeout: 10)) XCTAssertEqual(countLabel.label, "0/0") let prevButton = app.buttons[IDs.searchPrevious] let nextButton = app.buttons[IDs.searchNext] XCTAssertTrue(prevButton.waitForExistence(timeout: 3)) XCTAssertTrue(nextButton.waitForExistence(timeout: 3)) XCTAssertFalse(prevButton.isEnabled, "无结果时上一个按钮应禁用") XCTAssertFalse(nextButton.isEnabled, "无结果时下一个按钮应禁用") } // MARK: - 清除搜索 func testCloseSearchClearsState() { app.launchAndOpenSampleBook(searchKeyword: "的") app.waitForReader() app.showReaderChromeIfNeeded() let countLabel = app.staticTexts[IDs.searchCount] XCTAssertTrue(countLabel.waitForExistence(timeout: 10)) XCTAssertNotEqual(countLabel.label, "0/0", "应有匹配结果") // 关闭搜索 let closeButton = app.buttons[IDs.searchClose] XCTAssertTrue(closeButton.waitForExistence(timeout: 3)) closeButton.tap() RunLoop.current.run(until: Date().addingTimeInterval(0.5)) // 搜索栏应消失 let searchBar = app.otherElements[IDs.searchBar] XCTAssertFalse(searchBar.waitForExistence(timeout: 2), "关闭后搜索栏应消失") } // MARK: - 搜索栏与工具栏联动 func testSearchBarHidesWhenToolbarsHide() { app.launchAndOpenSampleBook(searchKeyword: "的") app.waitForReader() app.showReaderChromeIfNeeded() let searchBar = app.otherElements[IDs.searchBar] XCTAssertTrue(searchBar.waitForExistence(timeout: 10), "搜索栏应可见") // 隐藏工具栏 app.hideReaderChromeIfNeeded() XCTAssertFalse(searchBar.waitForExistence(timeout: 2), "工具栏隐藏时搜索栏也应隐藏") } func testSearchBarRestoresWhenToolbarsShow() { app.launchAndOpenSampleBook(searchKeyword: "的") app.waitForReader() app.showReaderChromeIfNeeded() let searchBar = app.otherElements[IDs.searchBar] XCTAssertTrue(searchBar.waitForExistence(timeout: 10)) // 隐藏工具栏 app.hideReaderChromeIfNeeded() XCTAssertFalse(searchBar.waitForExistence(timeout: 2)) // 重新显示工具栏 app.showReaderChromeIfNeeded() XCTAssertTrue(searchBar.waitForExistence(timeout: 3), "工具栏恢复时搜索栏也应恢复") // 搜索状态应保留 let countLabel = app.staticTexts[IDs.searchCount] XCTAssertTrue(countLabel.waitForExistence(timeout: 3)) XCTAssertNotEqual(countLabel.label, "0/0", "搜索状态应在工具栏恢复后保留") } // MARK: - 辅助方法 private func openSearchBar() { let searchButton = app.buttons[IDs.readerSearch] if searchButton.waitForExistence(timeout: 5) { searchButton.tap() } let searchBar = app.otherElements[IDs.searchBar] XCTAssertTrue(searchBar.waitForExistence(timeout: 3), "搜索栏应出现") } }