Merge branch 'feature' of http://192.168.21.200:8418/4v5u09Z5a4Yuc/ReadViewSDK into feature
# Conflicts: # .gitignore
This commit is contained in:
+1698
-1694
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,11 @@
|
||||
1A2B3C4D00000005AABBCC01 /* SettingsExtendedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2B3C4D00000006AABBCC01 /* SettingsExtendedTests.swift */; };
|
||||
1A2B3C4D00000007AABBCC01 /* PageNavigationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2B3C4D00000008AABBCC01 /* PageNavigationTests.swift */; };
|
||||
1A2B3C4D00000009AABBCC01 /* ReaderAnnotationExtendedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2B3C4D0000000AAABBCC01 /* ReaderAnnotationExtendedTests.swift */; };
|
||||
1A2B3C4D0000000BAABBCC01 /* ConfigurableWindowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2B3C4D0000000CAABBCC01 /* ConfigurableWindowTests.swift */; };
|
||||
1A2B3C4D0000000DAABBCC01 /* ConcurrentParsingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2B3C4D0000000EAABBCC01 /* ConcurrentParsingTests.swift */; };
|
||||
1A2B3C4D0000000FAABBCC01 /* MetadataParseBenchmarkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2B3C4D00000010AABBCC01 /* MetadataParseBenchmarkTests.swift */; };
|
||||
1A2B3C4D00000011AABBCC01 /* DemoReaderState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2B3C4D00000012AABBCC01 /* DemoReaderState.swift */; };
|
||||
1A2B3C4D00000014AABBCC01 /* FanrenParseTimeTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2B3C4D00000013AABBCC01 /* FanrenParseTimeTest.swift */; };
|
||||
23BB1155EA379786DAA10A89 /* DisplayTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB49AFCCBC2BE04C82B8F286 /* DisplayTypeTests.swift */; };
|
||||
3BC5C96D7A0ACF35F2192CC7 /* XCUIApplication+Launch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74B4C44287820D68ED6570F8 /* XCUIApplication+Launch.swift */; };
|
||||
4509ED928F228F43888E063D /* ReaderToolbarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BADF0A18AD034B74A482A4C1 /* ReaderToolbarTests.swift */; };
|
||||
@@ -21,10 +26,7 @@
|
||||
C08FF8D030048DC5147729E9 /* ReaderOpenCloseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EDF066BA12974E6CFBE519F /* ReaderOpenCloseTests.swift */; };
|
||||
DE1437A969DA1C5F0CBB047D /* Pods_ReadViewDemo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 792DF85CE4A3DD80D67843C7 /* Pods_ReadViewDemo.framework */; };
|
||||
FEDB5937CEB858CB06E38E2D /* SettingsPanelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 201C2B482287866487EFAE66 /* SettingsPanelTests.swift */; };
|
||||
1A2B3C4D0000000BAABBCC01 /* ConfigurableWindowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2B3C4D0000000CAABBCC01 /* ConfigurableWindowTests.swift */; };
|
||||
1A2B3C4D0000000DAABBCC01 /* ConcurrentParsingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2B3C4D0000000EAABBCC01 /* ConcurrentParsingTests.swift */; };
|
||||
1A2B3C4D0000000FAABBCC01 /* MetadataParseBenchmarkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2B3C4D00000010AABBCC01 /* MetadataParseBenchmarkTests.swift */; };
|
||||
1A2B3C4D00000011AABBCC01 /* DemoReaderState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2B3C4D00000012AABBCC01 /* DemoReaderState.swift */; };
|
||||
369C9658D870DCAFC17EB7F7 /* SearchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFE01DBDCB8D790832A4DE3F /* SearchTests.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -44,6 +46,11 @@
|
||||
1A2B3C4D00000006AABBCC01 /* SettingsExtendedTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SettingsExtendedTests.swift; sourceTree = "<group>"; };
|
||||
1A2B3C4D00000008AABBCC01 /* PageNavigationTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PageNavigationTests.swift; sourceTree = "<group>"; };
|
||||
1A2B3C4D0000000AAABBCC01 /* ReaderAnnotationExtendedTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReaderAnnotationExtendedTests.swift; sourceTree = "<group>"; };
|
||||
1A2B3C4D0000000CAABBCC01 /* ConfigurableWindowTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConfigurableWindowTests.swift; sourceTree = "<group>"; };
|
||||
1A2B3C4D0000000EAABBCC01 /* ConcurrentParsingTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConcurrentParsingTests.swift; sourceTree = "<group>"; };
|
||||
1A2B3C4D00000010AABBCC01 /* MetadataParseBenchmarkTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MetadataParseBenchmarkTests.swift; sourceTree = "<group>"; };
|
||||
1A2B3C4D00000012AABBCC01 /* DemoReaderState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DemoReaderState.swift; sourceTree = "<group>"; };
|
||||
1A2B3C4D00000013AABBCC01 /* FanrenParseTimeTest.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = FanrenParseTimeTest.swift; sourceTree = "<group>"; };
|
||||
201C2B482287866487EFAE66 /* SettingsPanelTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SettingsPanelTests.swift; sourceTree = "<group>"; };
|
||||
20BD15E5D8F04E7E9A239E14 /* ReaderAnnotationTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReaderAnnotationTests.swift; sourceTree = "<group>"; };
|
||||
3A43AED288BFCA3ADBA97DD7 /* Pods-ReadViewDemo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReadViewDemo.release.xcconfig"; path = "Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo.release.xcconfig"; sourceTree = "<group>"; };
|
||||
@@ -54,12 +61,9 @@
|
||||
8FFD606A5A1CBDCC3CA87F1C /* ReadViewDemoUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ReadViewDemoUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
9EDF066BA12974E6CFBE519F /* ReaderOpenCloseTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReaderOpenCloseTests.swift; sourceTree = "<group>"; };
|
||||
BADF0A18AD034B74A482A4C1 /* ReaderToolbarTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReaderToolbarTests.swift; sourceTree = "<group>"; };
|
||||
1A2B3C4D0000000CAABBCC01 /* ConfigurableWindowTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConfigurableWindowTests.swift; sourceTree = "<group>"; };
|
||||
1A2B3C4D0000000EAABBCC01 /* ConcurrentParsingTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConcurrentParsingTests.swift; sourceTree = "<group>"; };
|
||||
1A2B3C4D00000010AABBCC01 /* MetadataParseBenchmarkTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MetadataParseBenchmarkTests.swift; sourceTree = "<group>"; };
|
||||
1A2B3C4D00000012AABBCC01 /* DemoReaderState.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DemoReaderState.swift; sourceTree = "<group>"; };
|
||||
DE070C1D2FBF0CC900ED065F /* ReadViewDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ReadViewDemo.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
FB49AFCCBC2BE04C82B8F286 /* DisplayTypeTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DisplayTypeTests.swift; sourceTree = "<group>"; };
|
||||
CFE01DBDCB8D790832A4DE3F /* SearchTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SearchTests.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
@@ -184,6 +188,8 @@
|
||||
1A2B3C4D0000000CAABBCC01 /* ConfigurableWindowTests.swift */,
|
||||
1A2B3C4D0000000EAABBCC01 /* ConcurrentParsingTests.swift */,
|
||||
1A2B3C4D00000010AABBCC01 /* MetadataParseBenchmarkTests.swift */,
|
||||
1A2B3C4D00000013AABBCC01 /* FanrenParseTimeTest.swift */,
|
||||
CFE01DBDCB8D790832A4DE3F /* SearchTests.swift */,
|
||||
);
|
||||
path = ReaderUITests;
|
||||
sourceTree = "<group>";
|
||||
@@ -314,14 +320,10 @@
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo-frameworks.sh\"\n";
|
||||
@@ -350,6 +352,8 @@
|
||||
1A2B3C4D0000000BAABBCC01 /* ConfigurableWindowTests.swift in Sources */,
|
||||
1A2B3C4D0000000DAABBCC01 /* ConcurrentParsingTests.swift in Sources */,
|
||||
1A2B3C4D0000000FAABBCC01 /* MetadataParseBenchmarkTests.swift in Sources */,
|
||||
1A2B3C4D00000014AABBCC01 /* FanrenParseTimeTest.swift in Sources */,
|
||||
369C9658D870DCAFC17EB7F7 /* SearchTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@ final class ViewController: UIViewController {
|
||||
let windowSize: Int?
|
||||
let concurrency: Int?
|
||||
let clearsCache: Bool
|
||||
let searchKeyword: String?
|
||||
|
||||
nonisolated private static func parseDisplayType(_ rawValue: String) -> RDReaderView.DisplayType? {
|
||||
switch rawValue.lowercased() {
|
||||
@@ -56,6 +57,7 @@ final class ViewController: UIViewController {
|
||||
let windowSize = value(after: "--demo-window-size").flatMap(Int.init)
|
||||
let concurrency = value(after: "--demo-concurrency").flatMap(Int.init)
|
||||
let clearsCache = arguments.contains("--demo-clear-cache")
|
||||
let searchKeyword = value(after: "--demo-search-keyword")
|
||||
|
||||
return LaunchAutomationPlan(
|
||||
bookTitleQuery: bookTitleQuery,
|
||||
@@ -66,7 +68,8 @@ final class ViewController: UIViewController {
|
||||
stepDelay: stepDelay,
|
||||
windowSize: windowSize,
|
||||
concurrency: concurrency,
|
||||
clearsCache: clearsCache
|
||||
clearsCache: clearsCache,
|
||||
searchKeyword: searchKeyword
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -229,6 +232,11 @@ final class ViewController: UIViewController {
|
||||
stepDelay: automationPlan.stepDelay
|
||||
)
|
||||
}
|
||||
if let searchKeyword = automationPlan.searchKeyword {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
controller?.performDemoSearch(keyword: searchKeyword)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,14 @@ enum IDs {
|
||||
static let readerSelectionText = "epub.reader.selection.text"
|
||||
static let readerSelectionHighlight = "epub.reader.selection.高亮"
|
||||
|
||||
static let readerSearch = "epub.reader.search"
|
||||
static let searchBar = "epub.reader.search.bar"
|
||||
static let searchField = "epub.reader.search.field"
|
||||
static let searchPrevious = "epub.reader.search.previous"
|
||||
static let searchNext = "epub.reader.search.next"
|
||||
static let searchClose = "epub.reader.search.close"
|
||||
static let searchCount = "epub.reader.search.count"
|
||||
|
||||
static let settingsScroll = "epub.reader.settings.scroll"
|
||||
static let settingsBrightness = "epub.reader.settings.brightness"
|
||||
static let settingsFontIncrease = "epub.reader.settings.font.increase"
|
||||
|
||||
@@ -26,6 +26,7 @@ struct DemoReaderState {
|
||||
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? { fields["href"] }
|
||||
var progression: Double? { fields["progression"].flatMap(Double.init) }
|
||||
|
||||
@@ -8,7 +8,8 @@ extension XCUIApplication {
|
||||
resetsReaderState: Bool = true,
|
||||
windowSize: Int? = nil,
|
||||
concurrency: Int? = nil,
|
||||
clearsCache: Bool = false
|
||||
clearsCache: Bool = false,
|
||||
searchKeyword: String? = nil
|
||||
) {
|
||||
var args = ["--demo-book-title", bookTitleQuery]
|
||||
if resetsReaderState {
|
||||
@@ -29,6 +30,9 @@ extension XCUIApplication {
|
||||
if let concurrency {
|
||||
args += ["--demo-concurrency", "\(concurrency)"]
|
||||
}
|
||||
if let searchKeyword {
|
||||
args += ["--demo-search-keyword", searchKeyword]
|
||||
}
|
||||
launchArguments = args
|
||||
launch()
|
||||
}
|
||||
|
||||
@@ -56,23 +56,30 @@ final class BookmarkTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testBookmarksPanelIsAccessible() {
|
||||
app.launchAndOpenSampleBook()
|
||||
// 不重置状态,利用已有的持久化书签来测试面板
|
||||
app.launchAndOpenSampleBook(resetsReaderState: false)
|
||||
app.waitForReader()
|
||||
app.showReaderChromeIfNeeded()
|
||||
|
||||
let bookmarkButton = app.buttons[IDs.readerBookmark]
|
||||
XCTAssertTrue(bookmarkButton.waitForExistence(timeout: 5), "书签按钮未出现")
|
||||
bookmarkButton.tap()
|
||||
// 如果没有书签,先添加一个
|
||||
let state = app.currentDemoReaderState()
|
||||
if state?.bookmarks == 0 || state?.bookmarks == nil {
|
||||
let bookmarkButton = app.buttons[IDs.readerBookmark]
|
||||
if bookmarkButton.waitForExistence(timeout: 5) {
|
||||
bookmarkButton.tap()
|
||||
RunLoop.current.run(until: Date().addingTimeInterval(1.5))
|
||||
}
|
||||
}
|
||||
|
||||
app.showReaderChromeIfNeeded()
|
||||
let bookmarksListButton = app.buttons[IDs.readerBookmarks]
|
||||
XCTAssertTrue(bookmarksListButton.waitForExistence(timeout: 5), "书签列表按钮未出现")
|
||||
|
||||
// 等待按钮变为可用(有书签时)
|
||||
let enabledPredicate = NSPredicate(format: "isEnabled == true")
|
||||
expectation(for: enabledPredicate, evaluatedWith: bookmarksListButton)
|
||||
waitForExpectations(timeout: 10)
|
||||
|
||||
XCTAssertTrue(bookmarksListButton.isEnabled, "书签列表按钮未启用")
|
||||
bookmarksListButton.tap()
|
||||
|
||||
let bookmarksPanel = app.tables[IDs.readerBookmarksTable]
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import XCTest
|
||||
|
||||
final class FanrenParseTimeTest: XCTestCase {
|
||||
private let app = XCUIApplication()
|
||||
|
||||
override func setUpWithError() throws {
|
||||
continueAfterFailure = false
|
||||
}
|
||||
|
||||
/// 测试凡人修仙传后台解析耗时
|
||||
func testFanrenParseTime() throws {
|
||||
let cpuCount = ProcessInfo.processInfo.activeProcessorCount
|
||||
print("[Fanren] 开始测试 - 设备CPU核心数: \(cpuCount)")
|
||||
|
||||
// 打开凡人修仙传,清缓存,使用默认并发数
|
||||
app.launchAndOpenSampleBook(
|
||||
bookTitleQuery: "凡人修仙传",
|
||||
resetsReaderState: true,
|
||||
concurrency: cpuCount,
|
||||
clearsCache: true
|
||||
)
|
||||
|
||||
// 等待阅读器打开
|
||||
app.waitForReader(timeout: 20)
|
||||
|
||||
// 等待首屏可读
|
||||
_ = app.waitForDemoReaderState(timeout: 30, description: "首屏加载") { state in
|
||||
state.mode == "bookPageMap" && (state.page ?? 0) >= 1
|
||||
}
|
||||
print("[Fanren] 首屏加载完成")
|
||||
|
||||
// 等待后台解析完成(parseMs > 0)
|
||||
let completed = app.waitForDemoReaderState(timeout: 900, description: "后台解析完成") { state in
|
||||
(state.parseMs ?? 0) > 0
|
||||
}
|
||||
|
||||
let parseMs = completed.parseMs ?? 0
|
||||
let concurrency = completed.parseConcurrency ?? 0
|
||||
|
||||
print("[Fanren] ---- 测试结果 ----")
|
||||
print("[Fanren] 后台解析耗时: \(parseMs)ms")
|
||||
print("[Fanren] 并发数: \(concurrency)")
|
||||
print("[Fanren] CPU核心数: \(cpuCount)")
|
||||
|
||||
// 验证
|
||||
XCTAssertGreaterThan(parseMs, 0, "parseMs应大于0")
|
||||
|
||||
// 返回书架
|
||||
app.showReaderChromeIfNeeded()
|
||||
if app.buttons[IDs.readerBack].waitForExistence(timeout: 5) {
|
||||
app.buttons[IDs.readerBack].tap()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,6 +70,79 @@ final class LargeBookOnDemandTests: XCTestCase {
|
||||
XCTAssertEqual(reopenedState.knownChapters, reopenedState.buildableChapters)
|
||||
}
|
||||
|
||||
func testMiddleChapterOpenPositionStableAfterFullParse() throws {
|
||||
// 从中间章节打开,等后台解析完成,验证阅读位置不跳转
|
||||
app.launchAndOpenSampleBook(
|
||||
bookTitleQuery: largeBookQuery,
|
||||
pageNumber: 50,
|
||||
resetsReaderState: true
|
||||
)
|
||||
app.waitForReader(timeout: 20)
|
||||
|
||||
let partialState = app.waitForDemoReaderState(timeout: 15, description: "中间章节首开进入局部分页") { state in
|
||||
state.mode == "bookPageMap" && state.pagination == "partial" && (state.page ?? 0) >= 1
|
||||
}
|
||||
let pageAfterOpen = partialState.page ?? 0
|
||||
XCTAssertGreaterThan(pageAfterOpen, 1, "应从中间章节打开,当前页=\(pageAfterOpen)")
|
||||
|
||||
// 等待后台解析完成
|
||||
let fullState = app.waitForDemoReaderState(timeout: 90, description: "后台解析完成") { state in
|
||||
state.pagination == "full"
|
||||
}
|
||||
XCTAssertEqual(fullState.pagination, "full", "后台解析应完成")
|
||||
|
||||
// 验证页码未跳转(方案 C:pending map 不会在解析完成时应用)
|
||||
let pageAfterParse = fullState.page ?? 0
|
||||
XCTAssertEqual(pageAfterParse, pageAfterOpen,
|
||||
"后台解析完成后页码不应跳转:解析前=\(pageAfterOpen) 解析后=\(pageAfterParse)")
|
||||
|
||||
// 验证翻页功能正常(触发 pending map 应用)
|
||||
let paging = app.collectionViews[IDs.readerPaging]
|
||||
if paging.waitForExistence(timeout: 5) {
|
||||
paging.swipeLeft()
|
||||
}
|
||||
|
||||
let afterSwipe = app.waitForDemoReaderState(timeout: 10, description: "翻页后页码推进") { state in
|
||||
state.page != nil && state.page != pageAfterParse
|
||||
}
|
||||
XCTAssertGreaterThan(afterSwipe.page ?? 0, pageAfterParse,
|
||||
"翻页后页码应前进:翻页前=\(pageAfterParse) 翻页后=\(afterSwipe.page ?? 0)")
|
||||
}
|
||||
|
||||
func testMiddleChapterOpenSwipeAfterFullParseResolvesCorrectly() throws {
|
||||
// 从中间章节打开,等解析完成,连续翻页验证页码连续性
|
||||
app.launchAndOpenSampleBook(
|
||||
bookTitleQuery: largeBookQuery,
|
||||
pageNumber: 30,
|
||||
resetsReaderState: true
|
||||
)
|
||||
app.waitForReader(timeout: 20)
|
||||
|
||||
_ = app.waitForDemoReaderState(timeout: 15, description: "局部分页就绪") { state in
|
||||
state.mode == "bookPageMap" && state.pagination == "partial"
|
||||
}
|
||||
|
||||
// 等待解析完成
|
||||
_ = app.waitForDemoReaderState(timeout: 90, description: "后台解析完成") { state in
|
||||
state.pagination == "full"
|
||||
}
|
||||
|
||||
// 连续翻 3 页,验证页码连续递增
|
||||
let paging = app.collectionViews[IDs.readerPaging]
|
||||
XCTAssertTrue(paging.waitForExistence(timeout: 5), "分页视图不存在")
|
||||
|
||||
var previousPage = app.currentDemoReaderState()?.page ?? 0
|
||||
for i in 0..<3 {
|
||||
paging.swipeLeft()
|
||||
RunLoop.current.run(until: Date().addingTimeInterval(1))
|
||||
let currentState = app.currentDemoReaderState()
|
||||
let currentPage = currentState?.page ?? 0
|
||||
XCTAssertGreaterThan(currentPage, previousPage,
|
||||
"第 \(i + 1) 次翻页后页码应递增:前=\(previousPage) 后=\(currentPage)")
|
||||
previousPage = currentPage
|
||||
}
|
||||
}
|
||||
|
||||
func testLargeBookContinuousPagingExtendsKnownPageMap() throws {
|
||||
app.launchAndOpenSampleBook(bookTitleQuery: largeBookQuery, displayType: "scroll", resetsReaderState: true)
|
||||
app.waitForReader(timeout: 20)
|
||||
|
||||
@@ -18,8 +18,8 @@ final class ReaderAnnotationTests: XCTestCase {
|
||||
|
||||
app.waitForReaderState(containing: "toolbar=hidden", timeout: 5)
|
||||
|
||||
let start = content.coordinate(withNormalizedOffset: CGVector(dx: 0.35, dy: 0.45))
|
||||
let end = content.coordinate(withNormalizedOffset: CGVector(dx: 0.68, dy: 0.45))
|
||||
let start = content.coordinate(withNormalizedOffset: CGVector(dx: 0.2, dy: 0.5))
|
||||
let end = content.coordinate(withNormalizedOffset: CGVector(dx: 0.8, dy: 0.5))
|
||||
start.press(forDuration: 0.8, thenDragTo: end)
|
||||
|
||||
app.waitForReaderState(containing: "selection=1", timeout: 5)
|
||||
@@ -48,8 +48,8 @@ final class ReaderAnnotationTests: XCTestCase {
|
||||
|
||||
app.waitForReaderState(containing: "toolbar=hidden", timeout: 5)
|
||||
|
||||
let start = content.coordinate(withNormalizedOffset: CGVector(dx: 0.35, dy: 0.45))
|
||||
let end = content.coordinate(withNormalizedOffset: CGVector(dx: 0.68, dy: 0.45))
|
||||
let start = content.coordinate(withNormalizedOffset: CGVector(dx: 0.2, dy: 0.5))
|
||||
let end = content.coordinate(withNormalizedOffset: CGVector(dx: 0.8, dy: 0.5))
|
||||
start.press(forDuration: 0.8, thenDragTo: end)
|
||||
|
||||
app.waitForReaderState(containing: "selection=1", timeout: 8)
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
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), "搜索栏应出现")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user