197 lines
5.7 KiB
Swift
197 lines
5.7 KiB
Swift
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()
|
|
}
|
|
}
|