feat(rules): 建立共享排版契约并收束 WebKit 与 Unicode 基线
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
import AppKit
|
||||
import Darwin
|
||||
import Foundation
|
||||
import WebKit
|
||||
|
||||
@MainActor
|
||||
final class WkWebViewHarness: NSObject, WKNavigationDelegate
|
||||
{
|
||||
private let app: NSApplication
|
||||
private let window: NSWindow
|
||||
private let webView: WKWebView
|
||||
private var finished = false
|
||||
|
||||
init(app: NSApplication, pagePath: String, readRoot: String)
|
||||
{
|
||||
self.app = app
|
||||
|
||||
let configuration = WKWebViewConfiguration()
|
||||
configuration.websiteDataStore = .nonPersistent()
|
||||
webView = WKWebView(
|
||||
frame: NSRect(x: 0, y: 0, width: 1224, height: 768),
|
||||
configuration: configuration
|
||||
)
|
||||
window = NSWindow(
|
||||
contentRect: webView.frame,
|
||||
styleMask: [ .titled, .closable, .resizable ],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
|
||||
super.init()
|
||||
|
||||
webView.navigationDelegate = self
|
||||
window.title = "Juzhen WKWebView E2E"
|
||||
window.contentView = webView
|
||||
window.center()
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
|
||||
let pageUrl = URL(fileURLWithPath: pagePath)
|
||||
let readUrl = URL(fileURLWithPath: readRoot, isDirectory: true)
|
||||
webView.loadFileURL(pageUrl, allowingReadAccessTo: readUrl)
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 20)
|
||||
{ [weak self] in
|
||||
self?.finish(code: 2, reason: "timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!)
|
||||
{
|
||||
evaluate("navigator.userAgent")
|
||||
{ value in
|
||||
print("WKWEBVIEW_USER_AGENT=\(value)")
|
||||
}
|
||||
click(buttonId: "run-core")
|
||||
pollTitle(expectedPrefix: "CORE PASS", timeoutAt: Date().addingTimeInterval(8))
|
||||
{ [weak self] coreTitle in
|
||||
guard let self else { return }
|
||||
guard coreTitle.hasPrefix("CORE PASS") else
|
||||
{
|
||||
self.collectAndFinish(code: 3, reason: "core title: \(coreTitle)")
|
||||
return
|
||||
}
|
||||
self.click(buttonId: "run-observer")
|
||||
self.pollTitle(expectedPrefix: "PASS", timeoutAt: Date().addingTimeInterval(8))
|
||||
{ [weak self] finalTitle in
|
||||
guard let self else { return }
|
||||
let success = finalTitle.hasPrefix("PASS")
|
||||
self.collectAndFinish(
|
||||
code: success ? 0 : 4,
|
||||
reason: "final title: \(finalTitle)"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func webView(
|
||||
_ webView: WKWebView,
|
||||
didFail navigation: WKNavigation!,
|
||||
withError error: Error
|
||||
)
|
||||
{
|
||||
finish(code: 5, reason: "navigation failed: \(error)")
|
||||
}
|
||||
|
||||
func webView(
|
||||
_ webView: WKWebView,
|
||||
didFailProvisionalNavigation navigation: WKNavigation!,
|
||||
withError error: Error
|
||||
)
|
||||
{
|
||||
finish(code: 6, reason: "provisional navigation failed: \(error)")
|
||||
}
|
||||
|
||||
private func click(buttonId: String)
|
||||
{
|
||||
let script = "document.getElementById('\(buttonId)')?.click(); document.title;"
|
||||
evaluate(script)
|
||||
{ value in
|
||||
print("WKWEBVIEW_CLICK_\(buttonId)=\(value)")
|
||||
}
|
||||
}
|
||||
|
||||
private func pollTitle(
|
||||
expectedPrefix: String,
|
||||
timeoutAt: Date,
|
||||
completion: @escaping (String) -> Void
|
||||
)
|
||||
{
|
||||
evaluate("document.title")
|
||||
{ [weak self] value in
|
||||
guard let self else { return }
|
||||
if value.hasPrefix(expectedPrefix) || value.hasPrefix("FAIL") || Date() >= timeoutAt
|
||||
{
|
||||
completion(value)
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1)
|
||||
{ [weak self] in
|
||||
self?.pollTitle(
|
||||
expectedPrefix: expectedPrefix,
|
||||
timeoutAt: timeoutAt,
|
||||
completion: completion
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func collectAndFinish(code: Int32, reason: String)
|
||||
{
|
||||
let script = """
|
||||
JSON.stringify({
|
||||
title: document.title,
|
||||
summary: document.getElementById('summary-text')?.textContent || '',
|
||||
failures: Array.from(document.querySelectorAll('#results li[data-state="fail"]'))
|
||||
.map((item) => item.textContent),
|
||||
results: Array.from(document.querySelectorAll('#results li'))
|
||||
.map((item) => item.textContent)
|
||||
})
|
||||
"""
|
||||
evaluate(script)
|
||||
{ [weak self] value in
|
||||
print("WKWEBVIEW_RESULT=\(value)")
|
||||
self?.finish(code: code, reason: reason)
|
||||
}
|
||||
}
|
||||
|
||||
private func evaluate(_ script: String, completion: @escaping (String) -> Void)
|
||||
{
|
||||
webView.evaluateJavaScript(script)
|
||||
{ value, error in
|
||||
if let error
|
||||
{
|
||||
completion("EVALUATION_ERROR: \(error)")
|
||||
return
|
||||
}
|
||||
completion(String(describing: value ?? "nil"))
|
||||
}
|
||||
}
|
||||
|
||||
private func finish(code: Int32, reason: String)
|
||||
{
|
||||
guard !finished else { return }
|
||||
finished = true
|
||||
print("WKWEBVIEW_EXIT_REASON=\(reason)")
|
||||
fflush(stdout)
|
||||
window.close()
|
||||
app.stop(nil)
|
||||
exit(code)
|
||||
}
|
||||
}
|
||||
|
||||
@main
|
||||
struct Main
|
||||
{
|
||||
@MainActor
|
||||
private static var harness: WkWebViewHarness?
|
||||
|
||||
@MainActor
|
||||
static func main()
|
||||
{
|
||||
guard CommandLine.arguments.count == 3 else
|
||||
{
|
||||
fputs("usage: harness <e2e.html> <read-root>\n", stderr)
|
||||
exit(64)
|
||||
}
|
||||
let app = NSApplication.shared
|
||||
app.setActivationPolicy(.accessory)
|
||||
harness = WkWebViewHarness(
|
||||
app: app,
|
||||
pagePath: CommandLine.arguments[1],
|
||||
readRoot: CommandLine.arguments[2]
|
||||
)
|
||||
app.run()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user