feat: complete large-book pagination runtime and UI coverage

This commit is contained in:
shenlei
2026-06-03 17:47:16 +08:00
parent 584976ac5f
commit feb05eaf87
53 changed files with 6307 additions and 5126 deletions
@@ -0,0 +1,95 @@
import XCTest
final class BookmarkManagementTests: XCTestCase {
private let app = XCUIApplication()
override func setUpWithError() throws {
continueAfterFailure = false
}
func testBookmarksPanelShowsBookmarkAfterAdding() throws {
app.launchAndOpenSampleBook()
app.waitForReader(timeout: 12)
app.showReaderChromeIfNeeded()
let bookmarkButton = app.buttons[IDs.readerBookmark]
XCTAssertTrue(bookmarkButton.waitForExistence(timeout: 3), "书签按钮不存在")
bookmarkButton.tap()
app.waitForReaderState(containing: "bookmarks=1", timeout: 5)
app.showReaderChromeIfNeeded()
let bookmarksButton = app.buttons[IDs.readerBookmarks]
XCTAssertTrue(bookmarksButton.waitForExistence(timeout: 3), "书签列表按钮不存在")
bookmarksButton.tap()
let bookmarksTable = app.tables[IDs.readerBookmarksTable]
XCTAssertTrue(bookmarksTable.waitForExistence(timeout: 5), "书签表格未出现")
XCTAssertTrue(waitForCellCount(in: bookmarksTable, minimum: 1, timeout: 5), "添加书签后面板应有至少 1 行数据")
}
func testBookmarksPanelEmptyState() throws {
app.launchAndOpenSampleBook(resetsReaderState: true)
app.waitForReader(timeout: 12)
app.showReaderChromeIfNeeded()
let bookmarksButton = app.buttons[IDs.readerBookmarks]
XCTAssertTrue(bookmarksButton.waitForExistence(timeout: 3), "书签列表按钮不存在")
bookmarksButton.tap()
XCTAssertTrue(app.tables[IDs.readerBookmarksTable].waitForExistence(timeout: 5), "书签面板未出现")
XCTAssertTrue(app.staticTexts[IDs.readerBookmarksEmptyLabel].waitForExistence(timeout: 5), "空状态 label 应存在")
XCTAssertTrue(app.staticTexts[IDs.readerBookmarksEmptyLabel].label.contains("暂无"), "空状态应显示暂无书签")
}
func testDeleteBookmarkFromPanel() throws {
app.launchAndOpenSampleBook()
app.waitForReader(timeout: 12)
app.showReaderChromeIfNeeded()
app.buttons[IDs.readerBookmark].tap()
app.waitForReaderState(containing: "bookmarks=1", timeout: 5)
app.showReaderChromeIfNeeded()
app.buttons[IDs.readerBookmarks].tap()
let bookmarksTable = app.tables[IDs.readerBookmarksTable]
XCTAssertTrue(bookmarksTable.waitForExistence(timeout: 5))
XCTAssertTrue(waitForCellCount(in: bookmarksTable, minimum: 1, timeout: 5))
bookmarksTable.cells.element(boundBy: 0).tap()
let deleteButton = app.buttons["删除书签"]
if deleteButton.waitForExistence(timeout: 3) { deleteButton.tap() }
XCTAssertTrue(app.staticTexts[IDs.readerBookmarksEmptyLabel].waitForExistence(timeout: 5), "删除后应显示空状态")
}
func testBookmarkIconStateToggle() throws {
app.launchAndOpenSampleBook()
app.waitForReader(timeout: 12)
app.showReaderChromeIfNeeded()
let bookmarkButton = app.buttons[IDs.readerBookmark]
XCTAssertTrue(bookmarkButton.waitForExistence(timeout: 3), "书签按钮不存在")
bookmarkButton.tap()
XCTAssertTrue(bookmarkButton.waitForExistence(timeout: 3), "添加书签后按钮应仍然存在")
bookmarkButton.tap()
XCTAssertTrue(bookmarkButton.waitForExistence(timeout: 3), "移除书签后按钮应仍然存在")
}
private func waitForCellCount(
in table: XCUIElement,
minimum: Int,
timeout: TimeInterval
) -> Bool {
let deadline = Date().addingTimeInterval(timeout)
while Date() < deadline {
if table.cells.count >= minimum {
return true
}
RunLoop.current.run(until: Date().addingTimeInterval(0.1))
}
return table.cells.count >= minimum
}
}
@@ -0,0 +1,69 @@
import XCTest
final class ErrorAndEdgeCaseTests: XCTestCase {
private let app = XCUIApplication()
override func setUpWithError() throws {
continueAfterFailure = false
}
func testVerticalScrollSwipeNavigation() throws {
app.launchAndOpenSampleBook(displayType: "verticalscroll")
app.waitForReader(timeout: 12)
let paging = app.collectionViews[IDs.readerPaging]
if paging.waitForExistence(timeout: 5) { paging.swipeUp() }
app.waitForReaderState(containing: "reader=opened", timeout: 5)
}
func testOpenAndCloseMultipleTimes() throws {
for iteration in 1...3 {
app.launchAndOpenSampleBook()
app.waitForReader(timeout: 12)
app.showReaderChromeIfNeeded()
let backButton = app.buttons[IDs.readerBack]
XCTAssertTrue(backButton.waitForExistence(timeout: 3), "返回按钮不存在")
backButton.tap()
let booksTable = app.tables[IDs.demoBooksTable]
XCTAssertTrue(booksTable.waitForExistence(timeout: 5), "书列表未出现")
}
}
func testRapidPageNavigation() throws {
app.launchAndOpenSampleBook()
app.waitForReader(timeout: 12)
app.waitForReaderPage(1, timeout: 5)
let paging = app.collectionViews[IDs.readerPaging]
guard paging.waitForExistence(timeout: 5) else { throw XCTSkip("分页视图不存在") }
for _ in 1...5 { paging.swipeLeft() }
app.waitForReaderState(containing: "reader=opened", timeout: 8)
}
func testSettingsPanelScrollReachesAllControls() throws {
app.launchAndOpenSampleBook()
app.waitForReader(timeout: 12)
app.showReaderChromeIfNeeded()
app.buttons[IDs.readerSettings].tap()
let settingsScroll = app.scrollViews[IDs.settingsScroll]
XCTAssertTrue(settingsScroll.waitForExistence(timeout: 5), "设置面板未出现")
XCTAssertTrue(app.buttons[IDs.settingsFontIncrease].waitForExistence(timeout: 3))
XCTAssertTrue(app.buttons[IDs.settingsBrightness].waitForExistence(timeout: 3))
settingsScroll.swipeUp()
XCTAssertTrue(app.segmentedControls[IDs.settingsDisplayType].waitForExistence(timeout: 5))
settingsScroll.swipeUp()
XCTAssertTrue(app.buttons[IDs.settingsTheme(5)].waitForExistence(timeout: 5))
app.buttons[IDs.settingsDone].tap()
}
}
@@ -0,0 +1,142 @@
import XCTest
final class HighlightsManagementTests: XCTestCase {
private let app = XCUIApplication()
override func setUpWithError() throws {
continueAfterFailure = false
}
func testHighlightsPanelOpens() throws {
app.launchAndOpenSampleBook()
app.waitForReader(timeout: 12)
let selectionText = app.otherElements[IDs.readerSelectionText].firstMatch
guard selectionText.waitForExistence(timeout: 5) else {
throw XCTSkip("文本选区元素不存在")
}
let start = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.3, dy: 0.5))
let end = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.6, dy: 0.5))
start.press(forDuration: 0.8, thenDragTo: end)
let highlightMenuItem = app.menuItems["高亮"]
if highlightMenuItem.waitForExistence(timeout: 3) { highlightMenuItem.tap() }
else if app.buttons["高亮"].waitForExistence(timeout: 2) { app.buttons["高亮"].tap() }
app.waitForReaderState(containing: "highlights=", timeout: 5)
app.showReaderChromeIfNeeded()
XCTAssertTrue(app.buttons[IDs.readerHighlights].waitForExistence(timeout: 3), "高亮列表按钮不存在")
app.buttons[IDs.readerHighlights].tap()
XCTAssertTrue(app.tables[IDs.readerHighlightsTable].waitForExistence(timeout: 5), "高亮管理面板未出现")
}
func testHighlightsPanelShowsCreatedHighlight() throws {
app.launchAndOpenSampleBook()
app.waitForReader(timeout: 12)
let selectionText = app.otherElements[IDs.readerSelectionText].firstMatch
guard selectionText.waitForExistence(timeout: 5) else { throw XCTSkip("文本选区元素不存在") }
let start = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.3, dy: 0.5))
let end = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.6, dy: 0.5))
start.press(forDuration: 0.8, thenDragTo: end)
let highlightMenuItem = app.menuItems["高亮"]
if highlightMenuItem.waitForExistence(timeout: 3) { highlightMenuItem.tap() }
app.showReaderChromeIfNeeded()
app.buttons[IDs.readerHighlights].tap()
let highlightsTable = app.tables[IDs.readerHighlightsTable]
XCTAssertTrue(highlightsTable.waitForExistence(timeout: 5), "高亮表格未出现")
XCTAssertTrue(waitForCellCount(in: highlightsTable, minimum: 1, timeout: 5), "创建高亮后面板应至少有 1 行数据")
}
func testHighlightsPanelFilterAll() throws {
app.launchAndOpenSampleBook()
app.waitForReader(timeout: 12)
let selectionText = app.otherElements[IDs.readerSelectionText].firstMatch
guard selectionText.waitForExistence(timeout: 5) else { throw XCTSkip("文本选区元素不存在") }
let start = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.3, dy: 0.5))
let end = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.6, dy: 0.5))
start.press(forDuration: 0.8, thenDragTo: end)
let highlightMenuItem = app.menuItems["高亮"]
if highlightMenuItem.waitForExistence(timeout: 3) { highlightMenuItem.tap() }
app.showReaderChromeIfNeeded()
app.buttons[IDs.readerHighlights].tap()
let filterControl = app.segmentedControls[IDs.readerHighlightsFilter]
XCTAssertTrue(filterControl.waitForExistence(timeout: 5), "筛选控件未出现")
filterControl.buttons.element(boundBy: 0).tap()
let highlightsTable = app.tables[IDs.readerHighlightsTable]
XCTAssertTrue(highlightsTable.waitForExistence(timeout: 3))
XCTAssertTrue(waitForCellCount(in: highlightsTable, minimum: 1, timeout: 5), "筛选全部后应有高亮数据")
}
func testHighlightsPanelEmptyState() throws {
app.launchAndOpenSampleBook(resetsReaderState: true)
app.waitForReader(timeout: 12)
app.showReaderChromeIfNeeded()
let highlightsButton = app.buttons[IDs.readerHighlights]
XCTAssertTrue(highlightsButton.waitForExistence(timeout: 3), "高亮列表按钮不存在")
highlightsButton.tap()
XCTAssertTrue(app.tables[IDs.readerHighlightsTable].waitForExistence(timeout: 5), "高亮管理面板未出现")
XCTAssertTrue(app.staticTexts[IDs.readerHighlightsEmptyLabel].waitForExistence(timeout: 5), "空状态 label 应存在")
XCTAssertTrue(app.staticTexts[IDs.readerHighlightsEmptyLabel].label.contains("暂无"), "空状态应显示暂无标注")
}
func testDeleteHighlightFromPanel() throws {
app.launchAndOpenSampleBook()
app.waitForReader(timeout: 12)
let selectionText = app.otherElements[IDs.readerSelectionText].firstMatch
guard selectionText.waitForExistence(timeout: 5) else { throw XCTSkip("文本选区元素不存在") }
let start = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.3, dy: 0.5))
let end = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.6, dy: 0.5))
start.press(forDuration: 0.8, thenDragTo: end)
let highlightMenuItem = app.menuItems["高亮"]
if highlightMenuItem.waitForExistence(timeout: 3) { highlightMenuItem.tap() }
app.showReaderChromeIfNeeded()
app.buttons[IDs.readerHighlights].tap()
let highlightsTable = app.tables[IDs.readerHighlightsTable]
XCTAssertTrue(highlightsTable.waitForExistence(timeout: 5))
XCTAssertTrue(waitForCellCount(in: highlightsTable, minimum: 1, timeout: 5))
highlightsTable.cells.element(boundBy: 0).tap()
let deleteButton = app.buttons["删除标注"].firstMatch
let deleteHighlightButton = app.buttons["删除高亮"].firstMatch
if deleteButton.waitForExistence(timeout: 3) {
deleteButton.tap()
} else if deleteHighlightButton.waitForExistence(timeout: 1) {
deleteHighlightButton.tap()
}
XCTAssertTrue(app.staticTexts[IDs.readerHighlightsEmptyLabel].waitForExistence(timeout: 5), "删除后应显示空状态")
}
private func waitForCellCount(
in table: XCUIElement,
minimum: Int,
timeout: TimeInterval
) -> Bool {
let deadline = Date().addingTimeInterval(timeout)
while Date() < deadline {
if table.cells.count >= minimum {
return true
}
RunLoop.current.run(until: Date().addingTimeInterval(0.1))
}
return table.cells.count >= minimum
}
}
@@ -0,0 +1,104 @@
import XCTest
final class LargeBookOnDemandTests: XCTestCase {
private let app = XCUIApplication()
private let largeBookQuery = "凡人修仙传"
override func setUpWithError() throws {
continueAfterFailure = false
}
func testLargeBookInitialOpenIsReadableWhilePaginationIsPartial() throws {
app.launchAndOpenSampleBook(bookTitleQuery: largeBookQuery, 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
}
XCTAssertEqual(partialState.mode, "bookPageMap")
XCTAssertEqual(partialState.pagination, "partial")
XCTAssertTrue(app.otherElements[IDs.readerContent].waitForExistence(timeout: 5), "首开局部分页阶段应已可阅读")
}
func testLargeBookBackgroundPaginationProgresses() throws {
app.launchAndOpenSampleBook(bookTitleQuery: largeBookQuery, resetsReaderState: true)
app.waitForReader(timeout: 20)
let initialState = app.waitForDemoReaderState(timeout: 15, description: "获取初始分页进度") { state in
state.mode == "bookPageMap" && state.knownChapters != nil && state.buildableChapters != nil
}
let initialKnownChapters = initialState.knownChapters ?? 0
let initialKnownPages = initialState.knownPages ?? 0
let progressedState = app.waitForDemoReaderState(timeout: 45, description: "后台分页推进") { state in
guard state.mode == "bookPageMap" else { return false }
if state.pagination == "full" {
return true
}
return (state.knownChapters ?? 0) > initialKnownChapters || (state.knownPages ?? 0) > initialKnownPages
}
XCTAssertEqual(progressedState.mode, "bookPageMap")
XCTAssertTrue(
(progressedState.knownChapters ?? 0) >= initialKnownChapters ||
(progressedState.knownPages ?? 0) >= initialKnownPages
)
}
func testLargeBookSecondOpenRestoresFromCache() throws {
app.launchAndOpenSampleBook(bookTitleQuery: largeBookQuery, resetsReaderState: true)
app.waitForReader(timeout: 20)
_ = app.waitForDemoReaderState(timeout: 60, description: "首次打开完成全书缓存") { state in
state.mode == "bookPageMap" && state.pagination == "full" && (state.buildableChapters ?? 0) > 0
}
app.showReaderChromeIfNeeded()
XCTAssertTrue(app.buttons[IDs.readerBack].waitForExistence(timeout: 5), "返回按钮不存在")
app.buttons[IDs.readerBack].tap()
XCTAssertTrue(app.tables[IDs.demoBooksTable].waitForExistence(timeout: 5), "书架列表未出现")
app.launchAndOpenSampleBook(bookTitleQuery: largeBookQuery, resetsReaderState: false)
app.waitForReader(timeout: 15)
let reopenedState = app.waitForDemoReaderState(timeout: 10, description: "二开直接恢复完整缓存") { state in
state.mode == "bookPageMap" && state.pagination == "full" && (state.knownChapters ?? 0) == (state.buildableChapters ?? -1)
}
XCTAssertEqual(reopenedState.pagination, "full")
XCTAssertEqual(reopenedState.knownChapters, reopenedState.buildableChapters)
}
func testLargeBookContinuousPagingExtendsKnownPageMap() throws {
app.launchAndOpenSampleBook(bookTitleQuery: largeBookQuery, displayType: "scroll", resetsReaderState: true)
app.waitForReader(timeout: 20)
app.waitForReaderState(containing: "display=horizontalScroll", timeout: 8)
let initialState = app.waitForDemoReaderState(timeout: 15, description: "初始局部分页") { state in
state.mode == "bookPageMap" && state.knownPages != nil && state.page != nil
}
let initialKnownPages = initialState.knownPages ?? 0
let initialPage = initialState.page ?? 0
let paging = app.collectionViews[IDs.readerPaging]
XCTAssertTrue(paging.waitForExistence(timeout: 5), "分页视图不存在")
for _ in 0..<4 {
paging.swipeLeft()
}
let extendedState = app.waitForDemoReaderState(timeout: 20, description: "连续翻页触发扩窗") { state in
guard state.mode == "bookPageMap" else { return false }
let pageAdvanced = (state.page ?? 0) > initialPage
let pageMapExtended = (state.knownPages ?? 0) > initialKnownPages || state.pagination == "full"
return pageAdvanced && pageMapExtended
}
XCTAssertTrue((extendedState.page ?? 0) > initialPage, "连续翻页后当前页应向后推进")
XCTAssertTrue(
(extendedState.knownPages ?? 0) > initialKnownPages || extendedState.pagination == "full",
"连续翻页后已知页图应扩窗或直接完成全量分页"
)
}
}
@@ -0,0 +1,75 @@
import XCTest
final class LocationPersistenceTests: XCTestCase {
private let app = XCUIApplication()
override func setUpWithError() throws {
continueAfterFailure = false
}
func testReadingPositionRestoredOnReopen() throws {
app.launchAndOpenSampleBook()
app.waitForReader(timeout: 12)
_ = app.waitForDemoReaderState(timeout: 5, description: "存在页码") { $0.page != nil }
let paging = app.collectionViews[IDs.readerPaging]
if paging.waitForExistence(timeout: 5) {
paging.swipeLeft()
}
let stateAfterSwipe = app.waitForDemoReaderState(timeout: 8, description: "翻页后页码变化") { state in
(state.page ?? 0) > 1
}
let pageAfterSwipe = stateAfterSwipe.page ?? 0
guard pageAfterSwipe > 1 else {
throw XCTSkip("当前翻页模式下未能翻到第 2 页")
}
let backButton = app.buttons[IDs.readerBack]
if backButton.waitForExistence(timeout: 3) { backButton.tap() }
app.launchAndOpenSampleBook()
app.waitForReader(timeout: 12)
let restoredState = app.waitForDemoReaderState(timeout: 8, description: "恢复阅读位置") { state in
state.page != nil
}
let restoredPage = restoredState.page ?? 0
XCTAssertEqual(restoredPage, pageAfterSwipe, "重新打开后应恢复到上次阅读页码")
}
func testResetStateLaunchesAtFirstPage() throws {
app.launchAndOpenSampleBook(resetsReaderState: true)
app.waitForReader(timeout: 12)
let state = app.waitForDemoReaderState(timeout: 5, description: "首开第 1 页") { state in
state.page != nil
}
let page = state.page ?? 0
XCTAssertEqual(page, 1, "重置状态后应从第 1 页开始")
}
func testPositionSurvivesFontChange() throws {
app.launchAndOpenSampleBook()
app.waitForReader(timeout: 12)
let paging = app.collectionViews[IDs.readerPaging]
if paging.waitForExistence(timeout: 5) { paging.swipeLeft() }
app.waitForReaderState(containing: "page=", timeout: 5)
app.showReaderChromeIfNeeded()
if app.buttons[IDs.readerSettings].waitForExistence(timeout: 3) { app.buttons[IDs.readerSettings].tap() }
if app.buttons[IDs.settingsFontIncrease].waitForExistence(timeout: 5) { app.buttons[IDs.settingsFontIncrease].tap() }
if app.buttons[IDs.settingsDone].waitForExistence(timeout: 3) { app.buttons[IDs.settingsDone].tap() }
let backButton = app.buttons[IDs.readerBack]
if backButton.waitForExistence(timeout: 3) { backButton.tap() }
app.launchAndOpenSampleBook()
app.waitForReader(timeout: 12)
app.waitForReaderState(containing: "reader=opened", timeout: 8)
}
}
@@ -41,15 +41,14 @@ final class PageNavigationTests: XCTestCase {
let paging = app.collectionViews[IDs.readerPaging]
XCTAssertTrue(paging.waitForExistence(timeout: 5))
let state = app.staticTexts[IDs.demoReaderState]
let beforeLabel = state.label
let beforeState = app.waitForDemoReaderState(timeout: 5, description: "滑动前页码") { $0.page != nil }
let beforePage = beforeState.page ?? 0
paging.swipeLeft()
let deadline = Date().addingTimeInterval(5)
while Date() < deadline {
if state.exists && state.label != beforeLabel { break }
RunLoop.current.run(until: Date().addingTimeInterval(0.1))
let afterState = app.waitForDemoReaderState(timeout: 8, description: "水平滑动后页码变化") { state in
guard let page = state.page else { return false }
return page != beforePage
}
XCTAssertNotEqual(beforeLabel, state.label, "水平滑动后页码未变化")
XCTAssertNotEqual(beforePage, afterState.page, "水平滑动后页码未变化")
}
}
@@ -0,0 +1,76 @@
import XCTest
final class SelectionMenuTests: XCTestCase {
private let app = XCUIApplication()
override func setUpWithError() throws {
continueAfterFailure = false
}
func testSelectionMenuShowsCopyOption() throws {
app.launchAndOpenSampleBook()
app.waitForReader(timeout: 12)
let selectionText = app.otherElements[IDs.readerSelectionText].firstMatch
guard selectionText.waitForExistence(timeout: 5) else { throw XCTSkip("文本选区元素不存在") }
let start = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.3, dy: 0.5))
let end = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.6, dy: 0.5))
start.press(forDuration: 0.8, thenDragTo: end)
XCTAssertTrue(app.menuItems["拷贝"].waitForExistence(timeout: 3), "选区菜单应包含拷贝选项")
}
func testSelectionMenuShowsAnnotateOption() throws {
app.launchAndOpenSampleBook()
app.waitForReader(timeout: 12)
let selectionText = app.otherElements[IDs.readerSelectionText].firstMatch
guard selectionText.waitForExistence(timeout: 5) else { throw XCTSkip("文本选区元素不存在") }
let start = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.3, dy: 0.5))
let end = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.6, dy: 0.5))
start.press(forDuration: 0.8, thenDragTo: end)
XCTAssertTrue(app.menuItems["批注"].waitForExistence(timeout: 3), "选区菜单应包含批注选项")
}
func testTapBlankAreaClearsSelection() throws {
app.launchAndOpenSampleBook()
app.waitForReader(timeout: 12)
let selectionText = app.otherElements[IDs.readerSelectionText].firstMatch
guard selectionText.waitForExistence(timeout: 5) else { throw XCTSkip("文本选区元素不存在") }
let start = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.3, dy: 0.5))
let end = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.6, dy: 0.5))
start.press(forDuration: 0.8, thenDragTo: end)
XCTAssertTrue(app.menuItems["拷贝"].waitForExistence(timeout: 3), "选区菜单应出现")
let contentArea = app.otherElements[IDs.readerContentView].firstMatch
if contentArea.waitForExistence(timeout: 3) {
contentArea.coordinate(withNormalizedOffset: CGVector(dx: 0.1, dy: 0.1)).tap()
}
let menuGone = !app.menuItems["拷贝"].waitForExistence(timeout: 2)
XCTAssertTrue(menuGone, "点击空白区域后选区菜单应消失")
}
func testCopySelectedText() throws {
app.launchAndOpenSampleBook()
app.waitForReader(timeout: 12)
let selectionText = app.otherElements[IDs.readerSelectionText].firstMatch
guard selectionText.waitForExistence(timeout: 5) else { throw XCTSkip("文本选区元素不存在") }
let start = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.3, dy: 0.5))
let end = selectionText.coordinate(withNormalizedOffset: CGVector(dx: 0.6, dy: 0.5))
start.press(forDuration: 0.8, thenDragTo: end)
let copyMenuItem = app.menuItems["拷贝"]
if copyMenuItem.waitForExistence(timeout: 3) { copyMenuItem.tap() }
app.waitForReaderState(containing: "reader=opened", timeout: 5)
}
}
@@ -0,0 +1,144 @@
import XCTest
final class SettingsEffectTests: XCTestCase {
private let app = XCUIApplication()
override func setUpWithError() throws {
continueAfterFailure = false
}
func testFontSizeChangeUpdatesContent() throws {
app.launchAndOpenSampleBook()
app.waitForReader(timeout: 12)
app.waitForReaderState(containing: "page=", timeout: 5)
app.showReaderChromeIfNeeded()
XCTAssertTrue(app.buttons[IDs.readerSettings].waitForExistence(timeout: 3), "设置按钮不存在")
app.buttons[IDs.readerSettings].tap()
XCTAssertTrue(app.scrollViews[IDs.settingsScroll].waitForExistence(timeout: 5), "设置面板未出现")
XCTAssertTrue(app.buttons[IDs.settingsFontIncrease].waitForExistence(timeout: 3), "字号增大按钮不存在")
app.buttons[IDs.settingsFontIncrease].tap()
XCTAssertTrue(app.buttons[IDs.settingsDone].waitForExistence(timeout: 3), "完成按钮不存在")
app.buttons[IDs.settingsDone].tap()
app.waitForReaderState(containing: "reader=opened", timeout: 8)
}
func testThemeSwitchChangesBackground() throws {
app.launchAndOpenSampleBook()
app.waitForReader(timeout: 12)
app.showReaderChromeIfNeeded()
app.buttons[IDs.readerSettings].tap()
XCTAssertTrue(app.scrollViews[IDs.settingsScroll].waitForExistence(timeout: 5), "设置面板未出现")
app.scrollViews[IDs.settingsScroll].swipeUp()
let darkThemeButton = app.buttons[IDs.settingsTheme(5)]
if darkThemeButton.waitForExistence(timeout: 3) { darkThemeButton.tap() }
app.buttons[IDs.settingsDone].tap()
let contentView = app.otherElements[IDs.readerContentView]
XCTAssertTrue(contentView.waitForExistence(timeout: 5), "主题切换后内容区应存在")
}
func testColumnCountChangeUpdatesLayout() throws {
app.launchAndOpenSampleBook()
app.waitForReader(timeout: 12)
app.showReaderChromeIfNeeded()
app.buttons[IDs.readerSettings].tap()
XCTAssertTrue(app.scrollViews[IDs.settingsScroll].waitForExistence(timeout: 5))
app.scrollViews[IDs.settingsScroll].swipeUp()
let columnsControl = app.segmentedControls[IDs.settingsColumns]
if columnsControl.waitForExistence(timeout: 3) && columnsControl.buttons.count > 1 {
columnsControl.buttons.element(boundBy: 1).tap()
}
app.buttons[IDs.settingsDone].tap()
app.waitForReaderState(containing: "reader=opened", timeout: 8)
}
func testLineHeightChangeUpdatesContent() throws {
app.launchAndOpenSampleBook()
app.waitForReader(timeout: 12)
app.showReaderChromeIfNeeded()
app.buttons[IDs.readerSettings].tap()
XCTAssertTrue(app.scrollViews[IDs.settingsScroll].waitForExistence(timeout: 5))
let lineHeightControl = app.segmentedControls[IDs.settingsLineHeight]
if lineHeightControl.waitForExistence(timeout: 3) && lineHeightControl.buttons.count > 1 {
lineHeightControl.buttons.element(boundBy: 1).tap()
}
app.buttons[IDs.settingsDone].tap()
app.waitForReaderState(containing: "reader=opened", timeout: 8)
}
func testDisplayTypeChangeRePaginates() throws {
app.launchAndOpenSampleBook(displayType: "horizontalscroll")
app.waitForReader(timeout: 12)
app.waitForReaderState(containing: "display=horizontalScroll", timeout: 5)
app.showReaderChromeIfNeeded()
app.buttons[IDs.readerSettings].tap()
XCTAssertTrue(app.scrollViews[IDs.settingsScroll].waitForExistence(timeout: 5))
let displayTypeControl = app.segmentedControls[IDs.settingsDisplayType]
if displayTypeControl.waitForExistence(timeout: 3) && displayTypeControl.buttons.count > 0 {
displayTypeControl.buttons.element(boundBy: 0).tap()
}
app.buttons[IDs.settingsDone].tap()
app.waitForReaderState(containing: "display=", timeout: 8)
}
func testDefaultBodyPaginationDoesNotEnableWidowOrphanCompaction() throws {
app.launchAndOpenSampleBook()
app.waitForReader(timeout: 12)
let state = app.waitForDemoReaderState(timeout: 8, description: "默认正文分页策略") { state in
state.avoidWidows != nil && state.avoidOrphans != nil
}
XCTAssertEqual(state.avoidWidows, 0, "默认正文分页不应启用 widow compaction,避免页尾明显留白")
XCTAssertEqual(state.avoidOrphans, 0, "默认正文分页不应启用 orphan compaction,避免提前换页")
}
func testSettingsPersistAfterReopen() throws {
app.launchAndOpenSampleBook()
app.waitForReader(timeout: 12)
app.showReaderChromeIfNeeded()
app.buttons[IDs.readerSettings].tap()
XCTAssertTrue(app.buttons[IDs.settingsFontIncrease].waitForExistence(timeout: 5))
let fontValueLabel = app.staticTexts[IDs.settingsFontValue]
XCTAssertTrue(fontValueLabel.waitForExistence(timeout: 3))
let originalFontValue = fontValueLabel.label
app.buttons[IDs.settingsFontIncrease].tap()
let newFontValue = fontValueLabel.label
XCTAssertNotEqual(newFontValue, originalFontValue, "点击增大后字号 label 应变化")
app.buttons[IDs.settingsDone].tap()
app.buttons[IDs.readerBack].tap()
app.launchAndOpenSampleBook()
app.waitForReader(timeout: 12)
app.showReaderChromeIfNeeded()
app.buttons[IDs.readerSettings].tap()
let persistedFontValue = app.staticTexts[IDs.settingsFontValue]
if persistedFontValue.waitForExistence(timeout: 5) {
XCTAssertEqual(persistedFontValue.label, newFontValue, "重新打开后字号应持久化为之前的值")
}
app.buttons[IDs.settingsDone].tap()
}
}
@@ -0,0 +1,63 @@
import XCTest
final class TOCInteractionTests: XCTestCase {
private let app = XCUIApplication()
override func setUpWithError() throws {
continueAfterFailure = false
}
func testTOCPanelShowsChapterList() throws {
app.launchAndOpenSampleBook()
app.waitForReader(timeout: 12)
app.showReaderChromeIfNeeded()
let tocButton = app.buttons[IDs.readerToc]
XCTAssertTrue(tocButton.waitForExistence(timeout: 3), "目录按钮不存在")
tocButton.tap()
let tocTable = app.tables[IDs.readerTocTable]
XCTAssertTrue(tocTable.waitForExistence(timeout: 5), "目录表格未出现")
XCTAssertTrue(tocTable.cells.count > 0, "目录应至少有 1 个章节")
}
func testTOCNavigatesToChapter() throws {
app.launchAndOpenSampleBook()
app.waitForReader(timeout: 12)
let initialState = app.waitForDemoReaderState(timeout: 5, description: "初始页码") { state in
state.page != nil
}
let initialPage = initialState.page ?? 0
app.showReaderChromeIfNeeded()
app.buttons[IDs.readerToc].tap()
let tocTable = app.tables[IDs.readerTocTable]
XCTAssertTrue(tocTable.waitForExistence(timeout: 5), "目录表格未出现")
if tocTable.cells.count > 1 {
tocTable.cells.element(boundBy: 1).tap()
let updatedState = app.waitForDemoReaderState(timeout: 8, description: "目录跳转后页码变化") { state in
guard let page = state.page else { return false }
return page != initialPage
}
let updatedPage = updatedState.page ?? 0
XCTAssertNotEqual(updatedPage, initialPage, "目录跳转后页码应变化")
} else {
tocTable.cells.element(boundBy: 0).tap()
app.waitForReaderState(containing: "reader=opened", timeout: 8)
}
}
func testTOCEmptyHandling() throws {
app.launchAndOpenSampleBook()
app.waitForReader(timeout: 12)
app.showReaderChromeIfNeeded()
app.buttons[IDs.readerToc].tap()
XCTAssertTrue(app.otherElements[IDs.readerTocPanel].waitForExistence(timeout: 5), "目录面板应可打开")
}
}
@@ -0,0 +1,71 @@
import XCTest
final class ToolbarStateTests: XCTestCase {
private let app = XCUIApplication()
override func setUpWithError() throws {
continueAfterFailure = false
}
func testTapContentHidesToolbar() throws {
app.launchAndOpenSampleBook()
app.waitForReader(timeout: 12)
app.showReaderChromeIfNeeded()
XCTAssertTrue(app.otherElements[IDs.readerTopToolbar].waitForExistence(timeout: 3), "顶部工具栏应可见")
XCTAssertTrue(app.otherElements[IDs.readerBottomToolbar].waitForExistence(timeout: 3), "底部工具栏应可见")
app.hideReaderChromeIfNeeded()
let backButton = app.buttons[IDs.readerBack]
let hidden = !backButton.waitForExistence(timeout: 2)
XCTAssertTrue(hidden, "点击内容区后工具栏应隐藏")
}
func testToolbarShowHideToggle() throws {
app.launchAndOpenSampleBook()
app.waitForReader(timeout: 12)
app.showReaderChromeIfNeeded()
XCTAssertTrue(app.buttons[IDs.readerBack].waitForExistence(timeout: 3))
app.hideReaderChromeIfNeeded()
app.showReaderChromeIfNeeded()
XCTAssertTrue(app.buttons[IDs.readerBack].waitForExistence(timeout: 3))
app.waitForReaderState(containing: "reader=opened", timeout: 5)
}
func testAddHighlightButtonDisabledWithoutSelection() throws {
app.launchAndOpenSampleBook()
app.waitForReader(timeout: 12)
app.showReaderChromeIfNeeded()
let addHighlightButton = app.buttons[IDs.readerAddHighlight]
XCTAssertTrue(addHighlightButton.waitForExistence(timeout: 3), "标注按钮应存在")
let beforeState = app.waitForDemoReaderState(timeout: 3, description: "初始高亮数") { $0.highlights != nil }
let highlightsBefore = beforeState.highlights ?? 0
addHighlightButton.tap()
let afterState = app.waitForDemoReaderState(timeout: 3, description: "点击后的高亮数") { $0.highlights != nil }
let highlightsAfter = afterState.highlights ?? 0
XCTAssertEqual(highlightsAfter, highlightsBefore, "无选区时点击标注按钮不应创建标注")
}
func testPageCurlSwipeNavigation() throws {
app.launchAndOpenSampleBook(displayType: "pagecurl")
app.waitForReader(timeout: 12)
app.waitForReaderPage(1, timeout: 5)
let paging = app.collectionViews[IDs.readerPaging]
if paging.waitForExistence(timeout: 5) { paging.swipeLeft() }
app.waitForReaderState(containing: "reader=opened", timeout: 5)
}
}