mirror of
https://github.com/Aminark/legado-ios.git
synced 2026-10-24 21:01:22 +08:00
feat: 支持 EPUB 阅读并改善规则兼容与阅读稳定性
支持 EPUB 2/3 导入、目录、封面和章节插图,保留本地书管理与阅读进度。 修复 JSON 模板、动态替换与请求参数兼容,以及 RSS 翻页和连续滚动定位。 更新编辑器工具栏和回归测试,发布版本为 0.1.12(build 18)。
This commit is contained in:
@@ -80,11 +80,14 @@ final class DarkModeUITests: XCTestCase {
|
||||
let keyboard = try luminances(app.keyboards.firstMatch, in: app)
|
||||
XCTAssertLessThan(keyboard[keyboard.count / 2], 0.15)
|
||||
}
|
||||
let accessory = app.buttons["完成"]
|
||||
let accessoryFrame = accessory.frame.intersection(app.frame)
|
||||
if accessory.isHittable && !accessoryFrame.isNull && accessoryFrame.width > 20 && accessoryFrame.height > 20 {
|
||||
try assertReadableDarkFill(accessory, in: app)
|
||||
}
|
||||
let accessory = app.buttons["syntaxCodeEditor.done"]
|
||||
XCTAssertTrue(accessory.exists && accessory.isHittable)
|
||||
try assertReadableDarkFill(accessory, in: app)
|
||||
app.buttons["@"].tap()
|
||||
XCTAssertTrue((app.textViews["syntaxCodeEditor"].value as? String)?.contains("@") == true)
|
||||
accessory.tap()
|
||||
let dismissed = expectation(for: NSPredicate { _, _ in !accessory.exists || !accessory.isHittable }, evaluatedWith: nil)
|
||||
wait(for: [dismissed], timeout: 5)
|
||||
}
|
||||
|
||||
func testDarkSettingsRulesAndManagementPages() throws {
|
||||
@@ -146,8 +149,8 @@ final class DarkModeUITests: XCTestCase {
|
||||
let app = launch(["-uiDemoTab", "search", "-themeMode", "2", "-uiAutomationSearch", "深色外观检查"])
|
||||
let cancelSearch = app.buttons.matching(NSPredicate(format: "label IN %@", ["取消", "Cancel"])).firstMatch
|
||||
if cancelSearch.waitForExistence(timeout: 3) { cancelSearch.tap() }
|
||||
XCTAssertTrue(app.buttons["搜索选项"].waitForExistence(timeout: 10))
|
||||
app.buttons["搜索选项"].tap()
|
||||
XCTAssertTrue(app.buttons["search.options"].waitForExistence(timeout: 10))
|
||||
app.buttons["search.options"].tap()
|
||||
app.buttons["分组或书源"].tap()
|
||||
XCTAssertTrue(app.navigationBars["分组或书源"].waitForExistence(timeout: 5))
|
||||
try captureDarkPage(app, name: "Search group filter")
|
||||
|
||||
@@ -108,7 +108,10 @@ final class LiveReadingFlowUITests: XCTestCase {
|
||||
showReaderMenu()
|
||||
app.buttons["换源"].tap()
|
||||
XCTAssertTrue(app.navigationBars["为《极道天魔》换源"].waitForExistence(timeout: 10))
|
||||
let replacement = app.buttons["sourceSwitch.candidate." + replacementID]
|
||||
let replacement = app.buttons.matching(NSPredicate(
|
||||
format: "identifier BEGINSWITH %@ AND label CONTAINS %@",
|
||||
"sourceSwitch.candidate." + replacementID + "|", "滚开"
|
||||
)).firstMatch
|
||||
XCTAssertTrue(replacement.waitForExistence(timeout: 240))
|
||||
attach("07-source-candidates")
|
||||
replacement.tap()
|
||||
|
||||
@@ -354,8 +354,13 @@ final class ReaderScrollGestureTests: XCTestCase {
|
||||
app.launch()
|
||||
let reader = app.collectionViews["reader.continuous"]
|
||||
XCTAssertTrue(reader.waitForExistence(timeout: 20))
|
||||
var lastInitialState = "<not reported>"
|
||||
let ready = expectation(for: NSPredicate { _, _ in
|
||||
let text = reader.value as? String ?? ""
|
||||
let text = reader.value as? String ?? "<missing>"
|
||||
if text != lastInitialState {
|
||||
print("[ContinuousReaderInitial] \(text)")
|
||||
lastInitialState = text
|
||||
}
|
||||
return text.contains("chapter=20 ") && !text.contains("chapters=1 ")
|
||||
}, evaluatedWith: nil)
|
||||
wait(for: [ready], timeout: 20)
|
||||
|
||||
@@ -2,34 +2,89 @@ import XCTest
|
||||
|
||||
@MainActor
|
||||
final class RuleEditorUITests: XCTestCase {
|
||||
func testBookshelfTXTFilePickerImportsTXTAndDisablesJSON() throws {
|
||||
try exerciseLocalTXTImport(fromFileManagement: false)
|
||||
}
|
||||
|
||||
func testFileManagementTXTFilePickerImportsTXTAndDisablesJSON() throws {
|
||||
try exerciseLocalTXTImport(fromFileManagement: true)
|
||||
}
|
||||
|
||||
private func exerciseLocalTXTImport(fromFileManagement: Bool) throws {
|
||||
func testRSSSearchSubmissionPaginationAndFavorites() throws {
|
||||
guard let fixture = ProcessInfo.processInfo.environment["LEGADO_RSS_UI_FIXTURE_URL"] else {
|
||||
throw XCTSkip("Set LEGADO_RSS_UI_FIXTURE_URL to the local RSS fixture source URL")
|
||||
}
|
||||
continueAfterFailure = false
|
||||
let app = XCUIApplication()
|
||||
app.launchArguments = ["-uiDemo", "-themeMode", "1", "-bookshelfLayout", "grid3"]
|
||||
app.launchArguments = ["-uiDemo", "-uiDemoTab", "rss", "-themeMode", "1"]
|
||||
app.launch()
|
||||
XCTAssertTrue(app.navigationBars["RSS"].waitForExistence(timeout: 20))
|
||||
app.buttons["RSS操作"].tap()
|
||||
app.buttons["URL或粘贴JSON"].tap()
|
||||
let editor = app.textViews.firstMatch
|
||||
XCTAssertTrue(editor.waitForExistence(timeout: 5))
|
||||
editor.tap()
|
||||
editor.typeText(fixture)
|
||||
app.navigationBars["导入RSS源"].buttons["导入"].tap()
|
||||
let source = app.staticTexts["RSS 全流程测试"]
|
||||
XCTAssertTrue(source.waitForExistence(timeout: 10))
|
||||
source.tap()
|
||||
XCTAssertTrue(app.staticTexts["首页文章"].waitForExistence(timeout: 10))
|
||||
let search = app.searchFields["源内搜索"]
|
||||
if !search.exists { app.swipeDown() }
|
||||
XCTAssertTrue(search.waitForExistence(timeout: 5))
|
||||
search.tap()
|
||||
search.typeText("needle\n")
|
||||
let result = app.staticTexts["搜索结果 needle 第1页"]
|
||||
XCTAssertTrue(result.waitForExistence(timeout: 10))
|
||||
let nextPage = app.buttons["加载下一页"]
|
||||
let ready = expectation(for: NSPredicate { _, _ in nextPage.exists && nextPage.isEnabled }, evaluatedWith: nil)
|
||||
wait(for: [ready], timeout: 10)
|
||||
nextPage.tap()
|
||||
XCTAssertTrue(app.staticTexts["搜索结果 needle 第2页"].waitForExistence(timeout: 10))
|
||||
result.tap()
|
||||
XCTAssertTrue(app.navigationBars["文章"].waitForExistence(timeout: 5))
|
||||
if app.buttons["取消收藏"].exists { app.buttons["取消收藏"].tap() }
|
||||
XCTAssertTrue(app.buttons["收藏"].waitForExistence(timeout: 5))
|
||||
app.buttons["收藏"].tap()
|
||||
XCTAssertTrue(app.buttons["取消收藏"].waitForExistence(timeout: 5))
|
||||
app.navigationBars["文章"].buttons.element(boundBy: 0).tap()
|
||||
let cancelSearch = app.navigationBars["RSS 全流程测试"].buttons.matching(NSPredicate(
|
||||
format: "label IN %@", ["Cancel", "取消", "Close", "关闭"]
|
||||
)).firstMatch
|
||||
if cancelSearch.exists { cancelSearch.tap() }
|
||||
app.navigationBars["RSS 全流程测试"].buttons["RSS"].tap()
|
||||
XCTAssertTrue(app.navigationBars["RSS"].waitForExistence(timeout: 5))
|
||||
app.segmentedControls.buttons["收藏"].tap()
|
||||
XCTAssertTrue(result.waitForExistence(timeout: 5))
|
||||
attach(app, name: "RSS search article saved to favorites")
|
||||
app.terminate()
|
||||
app.launch()
|
||||
XCTAssertTrue(app.staticTexts["RSS 全流程测试"].waitForExistence(timeout: 10))
|
||||
app.segmentedControls.buttons["收藏"].tap()
|
||||
XCTAssertTrue(result.waitForExistence(timeout: 5))
|
||||
}
|
||||
|
||||
func testBookshelfFilePickerImportsTXTAndEPUBAndDisablesJSON() throws {
|
||||
try exerciseLocalBookImport(fromFileManagement: false)
|
||||
}
|
||||
|
||||
func testFileManagementFilePickerImportsTXTAndEPUBAndDisablesJSON() throws {
|
||||
try exerciseLocalBookImport(fromFileManagement: true)
|
||||
}
|
||||
|
||||
private func exerciseLocalBookImport(fromFileManagement: Bool) throws {
|
||||
continueAfterFailure = false
|
||||
let app = XCUIApplication()
|
||||
app.launchArguments = ["-uiDemo", "-themeMode", "1", "-bookshelfLayout", "grid3", "-pageAnim", "0"]
|
||||
if fromFileManagement { app.launchArguments += ["-uiDemoTab", "settings", "-uiDemoFiles"] }
|
||||
app.launch()
|
||||
let importTitle = fromFileManagement ? "导入 TXT 文件" : "本地书"
|
||||
let importTitle = fromFileManagement ? "导入 TXT / EPUB" : "本地书"
|
||||
let more = fromFileManagement
|
||||
? app.navigationBars["文件管理"].buttons["更多"].firstMatch
|
||||
: app.buttons["更多"].firstMatch
|
||||
XCTAssertTrue(more.waitForExistence(timeout: 20))
|
||||
more.tap()
|
||||
app.buttons[importTitle].tap()
|
||||
app.buttons[importTitle].firstMatch.tap()
|
||||
let cancel = app.buttons.matching(NSPredicate(format: "label IN %@", ["Cancel", "取消"])).firstMatch
|
||||
XCTAssertTrue(cancel.waitForExistence(timeout: 10))
|
||||
cancel.tap()
|
||||
let closed = expectation(for: NSPredicate { _, _ in !cancel.exists }, evaluatedWith: nil)
|
||||
wait(for: [closed], timeout: 10)
|
||||
more.tap()
|
||||
app.buttons[importTitle].tap()
|
||||
app.buttons[importTitle].firstMatch.tap()
|
||||
XCTAssertTrue(cancel.waitForExistence(timeout: 10))
|
||||
|
||||
let pickerFiles = app.collectionViews["File View"].cells
|
||||
@@ -66,6 +121,10 @@ final class RuleEditorUITests: XCTestCase {
|
||||
let uppercase = pickerFiles.matching(NSPredicate(format: "label BEGINSWITH %@", "TXT-Uppercase")).firstMatch
|
||||
XCTAssertTrue(uppercase.waitForExistence(timeout: 5))
|
||||
uppercase.tap()
|
||||
let epub = pickerFiles.matching(NSPredicate(format: "label BEGINSWITH %@", "EPUB-Book")).firstMatch
|
||||
XCTAssertTrue(epub.waitForExistence(timeout: 5))
|
||||
XCTAssertTrue(epub.isEnabled)
|
||||
epub.tap()
|
||||
XCTAssertTrue(open.waitForExistence(timeout: 5))
|
||||
XCTAssertTrue(open.isEnabled)
|
||||
attach(app, name: "TXT selectable and JSON excluded")
|
||||
@@ -78,14 +137,34 @@ final class RuleEditorUITests: XCTestCase {
|
||||
XCTAssertTrue(app.navigationBars["导入本地书"].waitForExistence(timeout: 15))
|
||||
XCTAssertFalse(app.staticTexts["JSON-Source.json"].exists)
|
||||
XCTAssertFalse(app.staticTexts["JSON-Uppercase.JSON"].exists)
|
||||
XCTAssertTrue(app.staticTexts["TXT-Book.txt"].exists)
|
||||
XCTAssertTrue(app.staticTexts["TXT-Uppercase.TXT"].exists)
|
||||
XCTAssertTrue(app.staticTexts["TXT-Book.txt"].waitForExistence(timeout: 10), app.debugDescription)
|
||||
XCTAssertTrue(app.staticTexts["TXT-Uppercase.TXT"].waitForExistence(timeout: 10), app.debugDescription)
|
||||
app.buttons["导入"].tap()
|
||||
}
|
||||
// Repeated runs preserve managed files and correctly add a numeric suffix.
|
||||
XCTAssertTrue(app.staticTexts.matching(NSPredicate(format: "label BEGINSWITH %@", "TXT-Book")).firstMatch.waitForExistence(timeout: 15))
|
||||
XCTAssertTrue(app.staticTexts.matching(NSPredicate(format: "label BEGINSWITH %@", "TXT-Uppercase")).firstMatch.waitForExistence(timeout: 15))
|
||||
attach(app, name: "Local TXT books imported onto shelf")
|
||||
let epubBook = app.staticTexts["EPUB 导入测试"].firstMatch
|
||||
XCTAssertTrue(epubBook.waitForExistence(timeout: 15))
|
||||
attach(app, name: "Local TXT and EPUB books imported onto shelf")
|
||||
epubBook.tap()
|
||||
let reader = app.textViews["reader.text"]
|
||||
XCTAssertTrue(reader.waitForExistence(timeout: 15))
|
||||
XCTAssertTrue((reader.value as? String)?.contains("这是 EPUB 导入后的第一章正文") == true)
|
||||
reader.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
|
||||
app.buttons["更多"].firstMatch.tap()
|
||||
app.buttons["本章插图"].tap()
|
||||
XCTAssertTrue(app.navigationBars["本章插图"].waitForExistence(timeout: 5))
|
||||
XCTAssertTrue(app.images["书籍插图"].waitForExistence(timeout: 10))
|
||||
app.buttons["完成"].tap()
|
||||
reader.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
|
||||
app.buttons["目录"].tap()
|
||||
app.staticTexts["第二章 EPUB"].firstMatch.tap()
|
||||
let loaded = expectation(for: NSPredicate { _, _ in
|
||||
(reader.value as? String)?.contains("目录锚点跳转成功") == true
|
||||
}, evaluatedWith: nil)
|
||||
wait(for: [loaded], timeout: 10)
|
||||
attach(app, name: "EPUB second chapter after TOC navigation")
|
||||
}
|
||||
|
||||
func testJSONFilePickerOffersOpenAndImportsSelectedFiles() throws {
|
||||
@@ -146,7 +225,7 @@ final class RuleEditorUITests: XCTestCase {
|
||||
attach(app, name: "JSON import review after file picker dismissal")
|
||||
app.buttons["导入"].tap()
|
||||
if app.alerts.firstMatch.waitForExistence(timeout: 5) { app.alerts.buttons["好"].tap() }
|
||||
XCTAssertTrue(app.staticTexts["文件导入 JSON 测试"].waitForExistence(timeout: 10))
|
||||
XCTAssertTrue(app.staticTexts["文件导入 JSON 测试"].waitForExistence(timeout: 10), app.debugDescription)
|
||||
XCTAssertTrue(app.staticTexts["文件导入大写扩展名测试"].exists)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user