feat: 支持 EPUB 阅读并改善规则兼容与阅读稳定性

支持 EPUB 2/3 导入、目录、封面和章节插图,保留本地书管理与阅读进度。
修复 JSON 模板、动态替换与请求参数兼容,以及 RSS 翻页和连续滚动定位。
更新编辑器工具栏和回归测试,发布版本为 0.1.12(build 18)。
This commit is contained in:
Aminark
2026-10-24 00:58:11 +08:00
parent 3322fd71db
commit 67bc212ffd
58 changed files with 2331 additions and 244 deletions
@@ -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)
}