Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
350 lines
14 KiB
Swift
350 lines
14 KiB
Swift
import OpenClawKit
|
|
import SwiftUI
|
|
import Testing
|
|
import UIKit
|
|
@testable import OpenClaw
|
|
|
|
struct SwiftUIRenderSmokeTests {
|
|
@MainActor private static func host(_ view: some View, size: CGSize? = nil) -> UIWindow {
|
|
let frame = CGRect(origin: .zero, size: size ?? UIScreen.main.bounds.size)
|
|
let window = UIWindow(frame: frame)
|
|
window.rootViewController = UIHostingController(rootView: view)
|
|
window.makeKeyAndVisible()
|
|
window.rootViewController?.view.setNeedsLayout()
|
|
window.rootViewController?.view.layoutIfNeeded()
|
|
return window
|
|
}
|
|
|
|
@Test @MainActor func `settings pro tab builds A view hierarchy`() {
|
|
let appModel = NodeAppModel()
|
|
let gatewayController = GatewayConnectionController(appModel: appModel, startDiscovery: false)
|
|
|
|
let root = SettingsProTab()
|
|
.environment(AppAppearanceModel())
|
|
.environment(appModel)
|
|
.environment(appModel.voiceWake)
|
|
.environment(gatewayController)
|
|
|
|
_ = Self.host(root)
|
|
}
|
|
|
|
@Test @MainActor func `settings pro tab builds in light and dark mode`() {
|
|
for scheme in [ColorScheme.light, ColorScheme.dark] {
|
|
let appModel = NodeAppModel()
|
|
let gatewayController = GatewayConnectionController(appModel: appModel, startDiscovery: false)
|
|
|
|
let root = SettingsProTab()
|
|
.environment(AppAppearanceModel())
|
|
.environment(appModel)
|
|
.environment(appModel.voiceWake)
|
|
.environment(gatewayController)
|
|
.preferredColorScheme(scheme)
|
|
|
|
_ = Self.host(root)
|
|
}
|
|
}
|
|
|
|
@Test @MainActor func `settings About destination builds in light and dark mode`() {
|
|
for scheme in [ColorScheme.light, ColorScheme.dark] {
|
|
let appModel = NodeAppModel()
|
|
let gatewayController = GatewayConnectionController(appModel: appModel, startDiscovery: false)
|
|
|
|
let root = SettingsProTab(directRoute: .about)
|
|
.environment(AppAppearanceModel())
|
|
.environment(appModel)
|
|
.environment(appModel.voiceWake)
|
|
.environment(gatewayController)
|
|
.preferredColorScheme(scheme)
|
|
|
|
_ = Self.host(root, size: CGSize(width: 393, height: 852))
|
|
}
|
|
}
|
|
|
|
@Test @MainActor func `settings Licenses destination builds in light and dark mode`() {
|
|
var windows: [UIWindow] = []
|
|
defer { windows.forEach { $0.isHidden = true } }
|
|
|
|
for scheme in [ColorScheme.light, ColorScheme.dark] {
|
|
let appModel = NodeAppModel()
|
|
let gatewayController = GatewayConnectionController(appModel: appModel, startDiscovery: false)
|
|
|
|
let root = SettingsProTab(directRoute: .licenses)
|
|
.environment(AppAppearanceModel())
|
|
.environment(appModel)
|
|
.environment(appModel.voiceWake)
|
|
.environment(gatewayController)
|
|
.preferredColorScheme(scheme)
|
|
|
|
windows.append(Self.host(root, size: CGSize(width: 393, height: 852)))
|
|
}
|
|
}
|
|
|
|
@Test @MainActor func `settings pro tab appearance row builds for all preferences`() throws {
|
|
for preference in AppAppearancePreference.allCases {
|
|
let suiteName = "OpenClawTests.appearance.\(preference.rawValue).\(UUID().uuidString)"
|
|
let defaults = try #require(UserDefaults(suiteName: suiteName))
|
|
defer { defaults.removePersistentDomain(forName: suiteName) }
|
|
defaults.set(preference.rawValue, forKey: AppAppearancePreference.storageKey)
|
|
|
|
let appModel = NodeAppModel()
|
|
let gatewayController = GatewayConnectionController(appModel: appModel, startDiscovery: false)
|
|
|
|
let root = SettingsProTab()
|
|
.defaultAppStorage(defaults)
|
|
.environment(AppAppearanceModel(userDefaults: defaults))
|
|
.environment(appModel)
|
|
.environment(appModel.voiceWake)
|
|
.environment(gatewayController)
|
|
|
|
_ = Self.host(root)
|
|
}
|
|
}
|
|
|
|
@Test @MainActor func `hosted push relay disclosure builds A view hierarchy`() {
|
|
for typeSize in [DynamicTypeSize.large, .accessibility5] {
|
|
let root = HostedPushRelayDisclosureSheet(
|
|
message: "Enabling this sends delivery data through OpenClaw's hosted push relay.",
|
|
onContinue: {})
|
|
.environment(\.dynamicTypeSize, typeSize)
|
|
|
|
_ = Self.host(root, size: CGSize(width: 402, height: 450))
|
|
}
|
|
}
|
|
|
|
@Test @MainActor func `root tabs builds device orientation shell matrix`() {
|
|
for scenario in Self.rootTabsShellScenarios() {
|
|
let appModel = NodeAppModel()
|
|
let gatewayController = GatewayConnectionController(appModel: appModel, startDiscovery: false)
|
|
|
|
let root = RootTabs()
|
|
.environment(AppAppearanceModel())
|
|
.environment(appModel)
|
|
.environment(appModel.voiceWake)
|
|
.environment(gatewayController)
|
|
.environment(\.rootTabsUserInterfaceIdiomOverride, scenario.idiom)
|
|
.environment(\.horizontalSizeClass, scenario.horizontalSizeClass)
|
|
.environment(\.verticalSizeClass, scenario.verticalSizeClass)
|
|
|
|
_ = Self.host(root, size: scenario.size)
|
|
}
|
|
}
|
|
|
|
@Test @MainActor func `root tabs build gateway state view hierarchies`() {
|
|
for appModel in Self.rootTabsGatewayStateModels() {
|
|
let gatewayController = GatewayConnectionController(appModel: appModel, startDiscovery: false)
|
|
|
|
let root = RootTabs()
|
|
.environment(AppAppearanceModel())
|
|
.environment(appModel)
|
|
.environment(appModel.voiceWake)
|
|
.environment(gatewayController)
|
|
|
|
_ = Self.host(root)
|
|
}
|
|
}
|
|
|
|
@Test @MainActor func `gateway trust prompt alert presents when prompt appears after initial render`() async {
|
|
let appModel = NodeAppModel()
|
|
let gatewayController = Self.gatewayControllerWithCapturedTLSFingerprint(appModel: appModel)
|
|
let root = Color.clear
|
|
.gatewayTrustPromptAlert()
|
|
.environment(gatewayController)
|
|
|
|
let window = Self.host(root)
|
|
await Self.triggerGatewayTrustPrompt(controller: gatewayController)
|
|
await Self.waitForPresentedAlert(in: window)
|
|
|
|
#expect(window.rootViewController?.presentedViewController is UIAlertController)
|
|
}
|
|
|
|
@Test @MainActor func `root prompt alert stack presents gateway trust prompt`() async {
|
|
let appModel = NodeAppModel()
|
|
let gatewayController = Self.gatewayControllerWithCapturedTLSFingerprint(appModel: appModel)
|
|
let root = Color.clear
|
|
.gatewayTrustPromptAlert()
|
|
.deepLinkAgentPromptAlert()
|
|
.environment(appModel)
|
|
.environment(gatewayController)
|
|
|
|
let window = Self.host(root)
|
|
await Self.triggerGatewayTrustPrompt(controller: gatewayController)
|
|
await Self.waitForPresentedAlert(in: window)
|
|
|
|
#expect(window.rootViewController?.presentedViewController is UIAlertController)
|
|
}
|
|
|
|
@Test @MainActor func `root prompt alert stack still presents deep link prompt`() async throws {
|
|
let appModel = NodeAppModel()
|
|
appModel._test_setGatewayConnected(true)
|
|
let gatewayController = Self.gatewayControllerWithCapturedTLSFingerprint(appModel: appModel)
|
|
let root = Color.clear
|
|
.gatewayTrustPromptAlert()
|
|
.deepLinkAgentPromptAlert()
|
|
.environment(appModel)
|
|
.environment(gatewayController)
|
|
|
|
let window = Self.host(root)
|
|
let url = try #require(URL(string: "openclaw://agent?message=hello%20from%20deep%20link"))
|
|
await appModel.handleDeepLink(url: url)
|
|
await Self.waitForPresentedAlert(in: window)
|
|
|
|
#expect(window.rootViewController?.presentedViewController is UIAlertController)
|
|
}
|
|
|
|
@MainActor private static func gatewayControllerWithCapturedTLSFingerprint(
|
|
appModel: NodeAppModel)
|
|
-> GatewayConnectionController
|
|
{
|
|
GatewayConnectionController(
|
|
appModel: appModel,
|
|
startDiscovery: false,
|
|
tcpReachabilityProbe: { _, _, _, _ in true },
|
|
tlsFingerprintProbe: { _ in .fingerprint("abc123") })
|
|
}
|
|
|
|
@MainActor private static func triggerGatewayTrustPrompt(controller: GatewayConnectionController) async {
|
|
let host = "gateway-\(UUID().uuidString).example.com"
|
|
let port = 18789
|
|
let stableID = "manual|\(host.lowercased())|\(port)"
|
|
defer { GatewayTLSStore.clearFingerprint(stableID: stableID) }
|
|
GatewayTLSStore.clearFingerprint(stableID: stableID)
|
|
await controller.connectManual(host: host, port: port, useTLS: true)
|
|
}
|
|
|
|
@Test @MainActor func `phone control hub builds gateway state view hierarchies`() {
|
|
for appModel in Self.rootTabsGatewayStateModels() {
|
|
let root = RootTabsPhoneControlHub(
|
|
groups: RootTabs.phoneControlGroups,
|
|
initialDestination: nil,
|
|
navigationRequest: nil,
|
|
openRootDestination: { _ in },
|
|
openChatFromControlDetail: { _ in })
|
|
.environment(appModel)
|
|
|
|
_ = Self.host(root)
|
|
}
|
|
}
|
|
|
|
@Test @MainActor func `phone control hub builds landscape compact state`() {
|
|
let appModel = NodeAppModel()
|
|
let root = RootTabsPhoneControlHub(
|
|
groups: RootTabs.phoneControlGroups,
|
|
initialDestination: nil,
|
|
navigationRequest: nil,
|
|
openRootDestination: { _ in },
|
|
openChatFromControlDetail: { _ in })
|
|
.environment(appModel)
|
|
.environment(\.horizontalSizeClass, .regular)
|
|
.environment(\.verticalSizeClass, .compact)
|
|
|
|
_ = Self.host(root)
|
|
}
|
|
|
|
@Test @MainActor func `routed sidebar screens build offline states`() {
|
|
let appModel = NodeAppModel()
|
|
let screens: [AnyView] = [
|
|
AnyView(CommandCenterTab(openChat: {}, openSettings: {})),
|
|
AnyView(IPadActivityScreen(openChat: {}, openSettings: {})),
|
|
AnyView(OpenClawDocsScreen()),
|
|
AnyView(IPadWorkboardScreen(openChat: {}, openSettings: {})),
|
|
AnyView(IPadSkillWorkshopScreen(openSettings: {})),
|
|
AnyView(AgentProTab(directRoute: .agents)),
|
|
AnyView(AgentProTab(directRoute: .instances)),
|
|
AnyView(CommandSessionsScreen(openChat: {})),
|
|
AnyView(AgentProTab(directRoute: .dreaming)),
|
|
AnyView(AgentProTab(directRoute: .usage)),
|
|
AnyView(AgentProTab(directRoute: .cron)),
|
|
]
|
|
|
|
for screen in screens {
|
|
let root = NavigationStack { screen }
|
|
.environment(appModel)
|
|
_ = Self.host(root)
|
|
}
|
|
}
|
|
|
|
@Test @MainActor func `task screens build phone landscape compact states`() {
|
|
let appModel = NodeAppModel()
|
|
let screens: [AnyView] = [
|
|
AnyView(IPadWorkboardScreen(openChat: {}, openSettings: {})),
|
|
AnyView(IPadSkillWorkshopScreen(openSettings: {})),
|
|
]
|
|
|
|
for screen in screens {
|
|
let root = NavigationStack { screen }
|
|
.environment(appModel)
|
|
.environment(\.horizontalSizeClass, .regular)
|
|
.environment(\.verticalSizeClass, .compact)
|
|
|
|
_ = Self.host(root)
|
|
}
|
|
}
|
|
|
|
@Test @MainActor func `voice wake words view builds A view hierarchy`() {
|
|
let appModel = NodeAppModel()
|
|
let root = NavigationStack { VoiceWakeWordsSettingsView() }
|
|
.environment(appModel)
|
|
_ = Self.host(root)
|
|
}
|
|
|
|
@Test @MainActor func `voice wake toast builds A view hierarchy`() {
|
|
let root = VoiceWakeToast(command: "openclaw: do something")
|
|
_ = Self.host(root)
|
|
}
|
|
|
|
@MainActor private static func waitForPresentedAlert(in window: UIWindow) async {
|
|
for _ in 0..<10 {
|
|
if window.rootViewController?.presentedViewController != nil { return }
|
|
await Task.yield()
|
|
try? await Task.sleep(nanoseconds: 50_000_000)
|
|
}
|
|
}
|
|
|
|
@MainActor private static func rootTabsGatewayStateModels() -> [NodeAppModel] {
|
|
let offlineModel = NodeAppModel()
|
|
|
|
let connectingModel = NodeAppModel()
|
|
connectingModel.gatewayStatusText = "Connecting..."
|
|
|
|
let connectedModel = NodeAppModel()
|
|
connectedModel.enterAppleReviewDemoMode()
|
|
|
|
let errorModel = NodeAppModel()
|
|
errorModel.gatewayStatusText = "Gateway error: connection refused"
|
|
|
|
return [offlineModel, connectingModel, connectedModel, errorModel]
|
|
}
|
|
|
|
private static func rootTabsShellScenarios() -> [RootTabsShellScenario] {
|
|
[
|
|
RootTabsShellScenario(
|
|
idiom: .phone,
|
|
size: CGSize(width: 393, height: 852),
|
|
horizontalSizeClass: .compact,
|
|
verticalSizeClass: .regular),
|
|
RootTabsShellScenario(
|
|
idiom: .phone,
|
|
size: CGSize(width: 852, height: 393),
|
|
horizontalSizeClass: .regular,
|
|
verticalSizeClass: .compact),
|
|
RootTabsShellScenario(
|
|
idiom: .pad,
|
|
size: CGSize(width: 1024, height: 1366),
|
|
horizontalSizeClass: .regular,
|
|
verticalSizeClass: .regular),
|
|
RootTabsShellScenario(
|
|
idiom: .pad,
|
|
size: CGSize(width: 1366, height: 1024),
|
|
horizontalSizeClass: .regular,
|
|
verticalSizeClass: .regular),
|
|
]
|
|
}
|
|
|
|
private struct RootTabsShellScenario {
|
|
let idiom: UIUserInterfaceIdiom
|
|
let size: CGSize
|
|
let horizontalSizeClass: UserInterfaceSizeClass
|
|
let verticalSizeClass: UserInterfaceSizeClass
|
|
}
|
|
}
|