feat(KAN-70): integrate native workspace and update settings
CI/CD / preconditions (pull_request) Successful in 1s
Native JavaScript checks / Native launcher and release tests (pull_request) Successful in 1m2s
macOS checks / Native Rust tests and unsigned build (pull_request) Successful in 1m38s
CI/CD / test_and_check_app (pull_request) Successful in 6m47s
CI/CD / Publish immutable image (pull_request) Skipped

This commit is contained in:
2026-09-29 17:11:10 +02:00
parent 752ba2cd38
commit 912cfc28c2
32 changed files with 1651 additions and 38 deletions
@@ -135,6 +135,83 @@ final class WorkspaceTests: XCTestCase {
"Changing workspaces must retain native gesture navigation")
}
func testSettingsProtocolRejectsUnsupportedActionsAndInvalidPayloads() throws {
for body in [
#"{"version":2,"id":1,"action":"getState","payload":{}}"#,
#"{"version":true,"id":1,"action":"getState","payload":{}}"#,
#"{"version":1,"id":1,"action":"installUpdate","payload":{"version":"1"}}"#,
#"{"version":1,"id":1,"action":"setWorkspace","payload":{"server":12}}"#,
#"{"version":1,"id":1,"action":"getState","payload":{"extra":1}}"#,
#"{"version":1,"id":1,"action":"shell","payload":{}}"#,
] { XCTAssertThrowsError(try NativeSettingsRequest.parse(body), body) }
XCTAssertThrowsError(try NativeSettingsRequest.parse(String(repeating: "x", count: 8193)))
XCTAssertEqual(try NativeSettingsRequest.parse(#"{"version":1,"id":2,"action":"getState","payload":{}}"#).id, 2)
}
@MainActor
func testSettingsBridgeHidesToolbarOnlyAfterHandshakeAndRejectsFramesAndOldWorkspaces() async throws {
let name = "KAN70-settings-\(UUID())"
let defaults = try XCTUnwrap(UserDefaults(suiteName: name))
defer { defaults.removePersistentDomain(forName: name) }
let controller = WorkspaceViewController(defaults: defaults)
controller.loadViewIfNeeded()
controller.view.frame = CGRect(x: 0, y: 0, width: 390, height: 844)
let url = URL(string: "https://example.invalid/")!
controller.open(url)
let originalProfile = controller.server!.profile
let webview = try XCTUnwrap(controller.workspace)
webview.stopLoading()
webview.loadSimulatedRequest(URLRequest(url: url.appendingPathComponent("login")), responseHTML: "<html><body>Login<iframe srcdoc='<p>frame</p>'></iframe></body></html>")
try await waitFor(webview, "document.readyState === 'complete' && typeof window.kanbanNative === 'object'")
let state = try await callScript(webview, "return await window.kanbanNative.request('getState')") as? [String: Any]
XCTAssertEqual(state?["platform"] as? String, "ios")
XCTAssertEqual(state?["workspace"] as? String, url.absoluteString)
XCTAssertEqual(state?["capabilities"] as? [String], ["workspace"])
XCTAssertNil(state?["updates"])
let toolbar = try XCTUnwrap(controller.view.subviews.compactMap { $0 as? UIToolbar }.first)
XCTAssertFalse(toolbar.isHidden, "Older pages must retain native recovery")
let rejected = try await callScript(webview, """
try {
await window.frames[0].webkit.messageHandlers.kanbanSettings.postMessage(JSON.stringify({version:1,id:2,action:'settingsReady',payload:{}}));
return false;
} catch { return true; }
""")
XCTAssertEqual(rejected as? Bool, true)
XCTAssertFalse(toolbar.isHidden)
_ = try await callScript(webview, "return await window.kanbanNative.request('settingsReady')")
controller.view.layoutIfNeeded()
XCTAssertTrue(toolbar.isHidden)
XCTAssertEqual(toolbar.frame.height, 0)
XCTAssertEqual(webview.frame.minY, controller.view.safeAreaLayoutGuide.layoutFrame.minY, accuracy: 1)
let invalid = try await callScript(webview, "try { await window.kanbanNative.request('setWorkspace', {server:'http://bad.test'}); return false; } catch { return true; }")
XCTAssertEqual(invalid as? Bool, true)
XCTAssertEqual(controller.server?.url, url)
_ = try await callScript(webview, "return await window.kanbanNative.request('openSettings')")
XCTAssertFalse(controller.view.subviews.first { $0.accessibilityIdentifier == "serverSettings" }!.isHidden)
_ = try await callScript(webview, "return await window.kanbanNative.request('setWorkspace', {server:'https://other.invalid/app/'})")
for _ in 0..<50 {
if controller.workspace !== webview { break }
try await Task.sleep(for: .milliseconds(20))
}
XCTAssertFalse(controller.workspace === webview)
XCTAssertNotEqual(controller.server?.profile, originalProfile)
let restored = WorkspaceViewController(defaults: defaults)
restored.loadViewIfNeeded()
XCTAssertEqual(restored.server?.url.absoluteString, "https://other.invalid/app/")
let stale = try await callScript(webview, "try { await window.kanbanNative.request('setWorkspace', {server:'https://stale.invalid/'}); return false; } catch { return true; }")
XCTAssertEqual(stale as? Bool, true)
XCTAssertEqual(controller.server?.url.absoluteString, "https://other.invalid/app/")
}
@MainActor
private func callScript(_ webview: WKWebView, _ script: String) async throws -> Any {
try await withCheckedThrowingContinuation { continuation in
webview.callAsyncJavaScript(script, arguments: [:], in: nil, in: .page) { result in
continuation.resume(with: result)
}
}
}
@MainActor
private func waitFor(_ webview: WKWebView, _ expression: String) async throws {
for _ in 0..<100 {
@@ -201,7 +278,7 @@ final class WorkspaceTests: XCTestCase {
controller.open(url)
let server = try XCTUnwrap(controller.server)
XCTAssertEqual(controller.workspace?.configuration.websiteDataStore.identifier, server.profile)
XCTAssertEqual(controller.workspace?.configuration.userContentController.userScripts.count, 1)
XCTAssertEqual(controller.workspace?.configuration.userContentController.userScripts.count, 2)
let restored = WorkspaceViewController(defaults: defaults)
restored.loadViewIfNeeded()
XCTAssertEqual(restored.server, server)