feat: complete large-book pagination runtime and UI coverage
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user