mirror of
https://github.com/Aminark/legado-ios.git
synced 2026-10-24 12:51:22 +08:00
fix(appearance): 完善深色模式和阅读页面对比度
- 区分按钮填充与图标颜色,统一书架、书源、搜索和阅读设置的选中态 - 调整编辑器语法高亮、键盘工具栏及统计卡片的浅深色显示 - 适配 RSS 正文和登录网页背景,保留书源自定义样式和页面状态 - 补充外观切换、页面对比度和管理页面的回归测试
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
import WebKit
|
||||
import XCTest
|
||||
@testable import Legado
|
||||
|
||||
@MainActor
|
||||
final class AppearanceContrastTests: XCTestCase {
|
||||
func testLoginWebViewUsesAdaptiveBackgroundBeforeContentArrives() async throws {
|
||||
var source = BookSource(bookSourceUrl: "https://appearance.invalid", bookSourceName: "外观测试")
|
||||
source.loginUrl = "about:blank"
|
||||
let controller = UIHostingController(rootView: SourceLoginView(source: source))
|
||||
let window = UIWindow(frame: CGRect(x: 0, y: 0, width: 390, height: 844))
|
||||
window.overrideUserInterfaceStyle = .dark
|
||||
window.rootViewController = controller
|
||||
window.makeKeyAndVisible()
|
||||
defer { window.isHidden = true }
|
||||
func webView(in view: UIView) -> WKWebView? {
|
||||
if let view = view as? WKWebView { return view }
|
||||
return view.subviews.lazy.compactMap { webView(in: $0) }.first
|
||||
}
|
||||
window.layoutIfNeeded()
|
||||
for _ in 0..<20 {
|
||||
if webView(in: controller.view) != nil { break }
|
||||
try await Task.sleep(nanoseconds: 50_000_000)
|
||||
}
|
||||
let web = try XCTUnwrap(webView(in: controller.view))
|
||||
XCTAssertFalse(web.isOpaque)
|
||||
for style in [UIUserInterfaceStyle.dark, .light] {
|
||||
window.overrideUserInterfaceStyle = style
|
||||
window.layoutIfNeeded()
|
||||
let traits = UITraitCollection(userInterfaceStyle: style)
|
||||
let background = try XCTUnwrap(web.backgroundColor).resolvedColor(with: traits)
|
||||
let scroll = try XCTUnwrap(web.scrollView.backgroundColor).resolvedColor(with: traits)
|
||||
if style == .dark {
|
||||
XCTAssertLessThan(luminance(background), 0.15)
|
||||
XCTAssertLessThan(luminance(scroll), 0.15)
|
||||
} else {
|
||||
XCTAssertGreaterThan(luminance(background), 0.6)
|
||||
XCTAssertGreaterThan(luminance(scroll), 0.6)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testSyntaxTokensHaveReadableContrastInBaseAndElevatedAppearances() {
|
||||
let samples: [(SyntaxCodeEditor.Language, String)] = [
|
||||
(.json, #"{"title":"正文","count":12,"enabled":true,"empty":null}"#),
|
||||
(.rule, "@css:div && $.title {{key}} ##"),
|
||||
(.regex, #"\d+.*(word){2,4}"#),
|
||||
(.javascript, "// comment\nconst value = 'text'; return 123; /* comment */")
|
||||
]
|
||||
for style in [UIUserInterfaceStyle.light, .dark] {
|
||||
for level in [UIUserInterfaceLevel.base, .elevated] {
|
||||
let traits = UITraitCollection(traitsFrom: [
|
||||
UITraitCollection(userInterfaceStyle: style),
|
||||
UITraitCollection(userInterfaceLevel: level)
|
||||
])
|
||||
for (language, text) in samples {
|
||||
let editor = SyntaxCodeEditor(text: .constant(text), language: language)
|
||||
let view = UITextView()
|
||||
view.overrideUserInterfaceStyle = style
|
||||
view.text = text
|
||||
editor.makeCoordinator().highlight(view)
|
||||
let background = UIColor.secondarySystemGroupedBackground.resolvedColor(with: traits)
|
||||
view.textStorage.enumerateAttribute(.foregroundColor, in: NSRange(location: 0, length: view.textStorage.length)) { color, _, _ in
|
||||
guard let color = color as? UIColor else { return XCTFail("Missing syntax color") }
|
||||
let foreground = color.resolvedColor(with: traits)
|
||||
let values = [luminance(foreground), luminance(background)].sorted()
|
||||
XCTAssertGreaterThanOrEqual((values[1] + 0.05) / (values[0] + 0.05), 4.5,
|
||||
"Low contrast for \(language), \(style), \(level)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func luminance(_ color: UIColor) -> Double {
|
||||
var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
|
||||
XCTAssertTrue(color.getRed(&r, green: &g, blue: &b, alpha: &a))
|
||||
func linear(_ value: CGFloat) -> Double {
|
||||
let value = Double(value)
|
||||
return value <= 0.04045 ? value / 12.92 : pow((value + 0.055) / 1.055, 2.4)
|
||||
}
|
||||
return 0.2126 * linear(r) + 0.7152 * linear(g) + 0.0722 * linear(b)
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,56 @@ import XCTest
|
||||
|
||||
@MainActor
|
||||
final class WebReaderBridgeTests: XCTestCase {
|
||||
func testGeneratedRSSContentFollowsAppearanceWithoutReloading() async throws {
|
||||
let source = RssSource(sourceUrl: "https://appearance-regression.test", sourceName: "测试")
|
||||
let loaded = expectation(description: "HTML loaded")
|
||||
let surface = RSSWebReaderView(
|
||||
mode: .html(content: "<body><p>订阅正文</p><a href='#'>链接</a><button>操作</button></body>", baseURL: nil),
|
||||
source: source, onTextChange: { _ in loaded.fulfill() }
|
||||
)
|
||||
let coordinator = surface.makeCoordinator()
|
||||
let webView = WKWebView(frame: CGRect(x: 0, y: 0, width: 390, height: 844))
|
||||
webView.navigationDelegate = coordinator
|
||||
let window = UIWindow(frame: webView.frame)
|
||||
let controller = UIViewController()
|
||||
controller.overrideUserInterfaceStyle = .dark
|
||||
controller.view.addSubview(webView)
|
||||
window.rootViewController = controller
|
||||
window.makeKeyAndVisible()
|
||||
defer {
|
||||
RSSWebReaderView.dismantleUIView(webView, coordinator: coordinator)
|
||||
window.isHidden = true
|
||||
}
|
||||
coordinator.installRulesAndLoad(mode: surface.mode, in: webView)
|
||||
await fulfillment(of: [loaded], timeout: 20)
|
||||
let appearanceScript = """
|
||||
[matchMedia('(prefers-color-scheme:dark)').matches,
|
||||
getComputedStyle(document.body).backgroundColor,
|
||||
getComputedStyle(document.body).color,
|
||||
getComputedStyle(document.querySelector('a')).color,
|
||||
getComputedStyle(document.documentElement).colorScheme].map(String).join('|')
|
||||
"""
|
||||
let dark = try await webView.evaluateJavaScript(appearanceScript)
|
||||
XCTAssertEqual(dark as? String, "true|rgb(17, 17, 19)|rgb(242, 242, 242)|rgb(158, 203, 255)|light dark")
|
||||
_ = try await webView.evaluateJavaScript("window.appearanceMarker = 'preserved'")
|
||||
controller.overrideUserInterfaceStyle = .light
|
||||
window.layoutIfNeeded()
|
||||
XCTAssertEqual(webView.traitCollection.userInterfaceStyle, .light)
|
||||
var light = try await webView.evaluateJavaScript(appearanceScript)
|
||||
for _ in 0..<50 {
|
||||
if (light as? String)?.hasPrefix("false|") == true { break }
|
||||
try await Task.sleep(nanoseconds: 100_000_000)
|
||||
light = try await webView.evaluateJavaScript(appearanceScript)
|
||||
}
|
||||
XCTAssertEqual(light as? String, "false|rgb(252, 252, 252)|rgb(23, 23, 23)|rgb(0, 0, 238)|light dark")
|
||||
let marker = try await webView.evaluateJavaScript("window.appearanceMarker")
|
||||
XCTAssertEqual(marker as? String, "preserved")
|
||||
// A source may intentionally override the generated article palette.
|
||||
_ = try await webView.evaluateJavaScript("document.head.insertAdjacentHTML('beforeend', '<style>body{color:rgb(120,30,40)}</style>')")
|
||||
let custom = try await webView.evaluateJavaScript("getComputedStyle(document.body).color")
|
||||
XCTAssertEqual(custom as? String, "rgb(120, 30, 40)")
|
||||
}
|
||||
|
||||
func testPageJavaScriptCanStoreVariablesAndEvaluateContentRules() async throws {
|
||||
let source = RssSource(sourceUrl: "https://bridge-regression.test/\(UUID().uuidString)", sourceName: "测试")
|
||||
let loaded = expectation(description: "HTML loaded")
|
||||
|
||||
Reference in New Issue
Block a user