Files
openclaw/apps/ios/Tests/SwiftUIRenderSmokeTests.swift
2026-08-09 16:53:04 -07:00

806 lines
32 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import OpenClawKit
import SwiftUI
import Testing
import UIKit
@testable import OpenClaw
@testable import OpenClawChatUI
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 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] {
for typeSize in [DynamicTypeSize.large, .accessibility2] {
let appModel = NodeAppModel()
let gatewayController = GatewayConnectionController(appModel: appModel, startDiscovery: false)
let root = SettingsProTab(directRoute: .about)
.environment(AppAppearanceModel())
.environment(appModel)
.environment(appModel.voiceWake)
.environment(gatewayController)
.environment(\.dynamicTypeSize, typeSize)
.preferredColorScheme(scheme)
_ = Self.host(root, size: CGSize(width: 320, height: 852))
}
}
}
@Test @MainActor func `settings Privacy destination builds across appearance and type size`() {
for scheme in [ColorScheme.light, ColorScheme.dark] {
for typeSize in [DynamicTypeSize.large, .accessibility2] {
let appModel = NodeAppModel()
let gatewayController = GatewayConnectionController(appModel: appModel, startDiscovery: false)
let root = SettingsProTab(directRoute: .privacy)
.environment(AppAppearanceModel())
.environment(appModel)
.environment(appModel.voiceWake)
.environment(gatewayController)
.preferredColorScheme(scheme)
.environment(\.dynamicTypeSize, typeSize)
_ = 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 OpenClaw destination builds access gate across appearance and type size`() {
var windows: [UIWindow] = []
defer { windows.forEach { $0.isHidden = true } }
for scheme in [ColorScheme.light, ColorScheme.dark] {
for typeSize in [DynamicTypeSize.large, .accessibility2] {
let appModel = NodeAppModel()
let gatewayController = GatewayConnectionController(appModel: appModel, startDiscovery: false)
let root = SettingsProTab(directRoute: .systemAgent)
.environment(AppAppearanceModel())
.environment(appModel)
.environment(appModel.voiceWake)
.environment(gatewayController)
.environment(\.dynamicTypeSize, typeSize)
.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 `display math builds valid and fallback view hierarchies`() {
for typeSize in [DynamicTypeSize.large, .accessibility2] {
let root = VStack {
ChatMarkdownRenderer(
text: #"Inline math \(E = mc^2\) stays inside prose."#,
context: .assistant,
variant: .standard,
textColor: OpenClawChatTheme.assistantText)
ChatMathBlockView(block: ChatMathBlock(
latex: #"\frac{-b \pm \sqrt{b^2 - 4ac}}{2a}"#,
isComplete: true), textColor: OpenClawChatTheme.assistantText)
ChatMathBlockView(block: ChatMathBlock(
latex: #"\notARealCommand{"#,
isComplete: true), textColor: OpenClawChatTheme.assistantText)
ChatMathBlockView(block: ChatMathBlock(
latex: "α + β = γ",
isComplete: true), textColor: OpenClawChatTheme.assistantText)
ChatMathBlockView(block: ChatMathBlock(
latex: String(repeating: "{", count: 65) + "x",
isComplete: true), textColor: OpenClawChatTheme.assistantText)
ChatMathBlockView(block: ChatMathBlock(
latex: String(repeating: #"\bar"#, count: 129) + "x",
isComplete: true), textColor: OpenClawChatTheme.assistantText)
ChatMathBlockView(block: ChatMathBlock(
latex: #"x\textcolor{#fff}{}"#,
isComplete: true), textColor: OpenClawChatTheme.assistantText)
}
.environment(\.dynamicTypeSize, typeSize)
_ = Self.host(root, size: CGSize(width: 393, height: 240))
}
}
@Test @MainActor func `markdown heading hierarchy builds with inline formatting and table`() {
let markdown = """
# First **strong** heading
## Second [linked](https://example.com) heading
### Third `code` heading
#### Fourth heading
##### Fifth heading
###### Sixth heading
| Surface | State |
| --- | --- |
| iOS | Native |
"""
for typeSize in [DynamicTypeSize.large, .accessibility2] {
let root = ChatMarkdownRenderer(
text: markdown,
context: .assistant,
variant: .standard,
textColor: OpenClawChatTheme.assistantText)
.environment(\.dynamicTypeSize, typeSize)
_ = Self.host(root, size: CGSize(width: 393, height: 700))
}
}
@Test @MainActor func `markdown lists and thematic breaks build across appearance and type size`() {
let markdown = """
Here are the options:
9. **Option one heading** a sentence describing it.
10. **Option two heading** another sentence.
- Nested detail
- [x] Completed detail
---
Final paragraph.
"""
for scheme in [ColorScheme.light, .dark] {
for typeSize in [DynamicTypeSize.large, .accessibility2] {
let root = ChatMarkdownRenderer(
text: markdown,
context: .assistant,
variant: .standard,
textColor: OpenClawChatTheme.assistantText)
.environment(\.dynamicTypeSize, typeSize)
.preferredColorScheme(scheme)
_ = Self.host(root, size: CGSize(width: 320, height: 700))
}
}
}
@Test @MainActor func `long user prompt disclosure builds across dynamic type sizes`() {
let text = Array(repeating: "A long user-authored prompt line.", count: 13).joined(separator: "\n")
let message = OpenClawChatMessage(
role: "user",
content: [OpenClawChatMessageContent(
type: "text",
text: text,
mimeType: nil,
fileName: nil,
content: nil)],
timestamp: nil)
for typeSize in [DynamicTypeSize.large, .accessibility2] {
let root = ChatMessageBubble(
message: message,
style: .standard,
markdownVariant: .standard,
userAccent: nil,
displayOptions: [],
assistantName: "OpenClaw",
assistantAvatarText: "OC",
assistantAvatarTint: nil,
showsAssistantAvatar: true,
isClean: false,
contextWindowTokens: nil,
userMessageExpanded: false,
onToggleUserMessageExpanded: {},
inlineWidgetResolverReady: true,
inlineWidgetResourceResolver: { _, _ in nil },
mediaArtifactResolverReady: false,
mediaPlaybackAllowed: { true },
loadMediaArtifact: { _, _, _ in nil })
.environment(\.dynamicTypeSize, typeSize)
_ = Self.host(root, size: CGSize(width: 320, height: 420))
}
}
@Test @MainActor func `managed assistant image starts its artifact load`() async throws {
let artifactId = "artifact_managed_image_11111111-1111-4111-8111-111111111111"
let message = OpenClawChatMessage(
role: "assistant",
content: [OpenClawChatMessageContent(
type: "image",
text: nil,
mimeType: "image/png",
fileName: nil,
artifactId: artifactId,
url: "/api/chat/media/outgoing/main/11111111-1111-4111-8111-111111111111/full",
alt: "Managed preview",
content: nil)],
timestamp: 1)
var requestedArtifactId: String?
let root = ChatMessageBubble(
message: message,
style: .standard,
markdownVariant: .standard,
userAccent: nil,
displayOptions: [],
assistantName: "OpenClaw",
assistantAvatarText: "OC",
assistantAvatarTint: nil,
showsAssistantAvatar: true,
isClean: false,
contextWindowTokens: nil,
userMessageExpanded: false,
onToggleUserMessageExpanded: {},
inlineWidgetResolverReady: true,
inlineWidgetResourceResolver: { _, _ in nil },
mediaArtifactResolverReady: true,
mediaPlaybackAllowed: { true },
loadMediaArtifact: { requested, kind, _ in
requestedArtifactId = requested
#expect(kind == .image)
return OpenClawChatLoadedMedia.data(OpenClawChatMediaData(
data: Data(base64Encoded:
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8A" +
"AusB9Y9Zl1sAAAAASUVORK5CYII=")!,
mimeType: "image/png"))
})
let window = Self.host(root, size: CGSize(width: 393, height: 420))
defer { window.isHidden = true }
let deadline = ContinuousClock().now.advanced(by: .seconds(2))
while requestedArtifactId == nil, ContinuousClock().now < deadline {
try await Task.sleep(for: .milliseconds(10))
}
#expect(requestedArtifactId == artifactId)
}
@Test @MainActor func `streaming assistant bubble builds mixed prose and code`() {
let text = """
Earlier prose stays visible.
```swift
let answer = 42
```
Trailing streamed words fade in.
"""
let root = ChatStreamingAssistantBubble(
text: text,
markdownVariant: .standard,
showsReasoning: false,
assistantName: "OpenClaw",
assistantAvatarText: "OC",
assistantAvatarTint: nil,
showsAssistantAvatar: true,
isClean: false)
_ = Self.host(root, size: CGSize(width: 393, height: 400))
}
@Test @MainActor func `completed and streaming assistant trace headings build across type sizes`() {
let text = """
<think>
# Internal plan
</think>
<final>
# Final answer
</final>
"""
let message = OpenClawChatMessage(
role: "assistant",
content: [OpenClawChatMessageContent(
type: "text",
text: text,
mimeType: nil,
fileName: nil,
content: nil)],
timestamp: 1)
for typeSize in [DynamicTypeSize.large, .accessibility2] {
let root = VStack {
ChatMessageBubble(
message: message,
style: .standard,
markdownVariant: .standard,
userAccent: nil,
displayOptions: [.reasoning],
assistantName: "OpenClaw",
assistantAvatarText: "OC",
assistantAvatarTint: nil,
showsAssistantAvatar: true,
isClean: false,
contextWindowTokens: nil,
userMessageExpanded: false,
onToggleUserMessageExpanded: {},
inlineWidgetResolverReady: true,
inlineWidgetResourceResolver: { _, _ in nil },
mediaArtifactResolverReady: false,
mediaPlaybackAllowed: { true },
loadMediaArtifact: { _, _, _ in nil })
ChatStreamingAssistantBubble(
text: text,
markdownVariant: .standard,
showsReasoning: true,
assistantName: "OpenClaw",
assistantAvatarText: "OC",
assistantAvatarTint: nil,
showsAssistantAvatar: true,
isClean: false)
}
.environment(\.dynamicTypeSize, typeSize)
_ = Self.host(root, size: CGSize(width: 393, height: 700))
}
}
@Test @MainActor func `assistant usage footer builds across dynamic type sizes`() throws {
let usage = try JSONDecoder().decode(
OpenClawChatUsage.self,
from: Data(#"{"input":12000,"output":300,"cacheRead":438400,"cacheWrite":307000,"cost":{"total":0.0123}}"#
.utf8))
let message = OpenClawChatMessage(
role: "assistant",
content: [OpenClawChatMessageContent(
type: "text",
text: "A completed assistant response with per-run usage.",
thinking: nil,
thinkingSignature: nil,
mimeType: nil,
fileName: nil,
content: nil,
id: nil,
name: nil,
arguments: nil)],
timestamp: nil,
usage: usage)
for typeSize in [DynamicTypeSize.large, .accessibility2] {
let root = ChatMessageBubble(
message: message,
style: .standard,
markdownVariant: .standard,
userAccent: nil,
displayOptions: [],
assistantName: "OpenClaw",
assistantAvatarText: "OC",
assistantAvatarTint: nil,
showsAssistantAvatar: true,
isClean: false,
contextWindowTokens: 1_000_000,
userMessageExpanded: false,
onToggleUserMessageExpanded: {},
inlineWidgetResolverReady: true,
inlineWidgetResourceResolver: { _, _ in nil },
mediaArtifactResolverReady: false,
mediaPlaybackAllowed: { true },
loadMediaArtifact: { _, _, _ in nil })
.environment(\.dynamicTypeSize, typeSize)
_ = Self.host(root, size: CGSize(width: 320, height: 280))
}
}
@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(initialSidebarVisibility: scenario.sidebarVisible)
.environment(AppAppearanceModel())
.environment(appModel)
.environment(appModel.voiceWake)
.environment(gatewayController)
.environment(\.horizontalSizeClass, scenario.horizontalSizeClass)
.environment(\.verticalSizeClass, scenario.verticalSizeClass)
_ = Self.host(root, size: scenario.size)
}
}
@Test @MainActor func `gateway quick setup builds candidate and empty states`() {
let gateways: [GatewayDiscoveryModel.DiscoveredGateway?] = [
.previewGateway,
nil,
]
for gateway in gateways {
let appModel = NodeAppModel()
let gatewayController = GatewayConnectionController(appModel: appModel, startDiscovery: false)
if let gateway {
gatewayController._test_setGateways([gateway])
appModel.gatewayStatusText = "Ready to pair"
}
let root = GatewayQuickSetupSheet()
.environment(appModel)
.environment(gatewayController)
.openClawSheetChrome()
_ = Self.host(root, size: CGSize(width: 393, height: 520))
}
}
@Test @MainActor func `onboarding activation screens build across appearance and type size`() {
let screens: [AnyView] = [
AnyView(OnboardingIntroStep(onContinue: {})),
AnyView(OnboardingWelcomeStep(
statusLine: "",
isConnecting: false,
onScanQRCode: {},
onManualSetup: {})),
AnyView(OnboardingSuccessStep(
gatewayName: "OpenClaw Gateway",
gatewayAddress: "openclaw.local",
onGetStarted: {})),
AnyView(NavigationStack {
Form {
Section("Connection Mode") {
OnboardingModeRow(
title: "Home Network",
subtitle: "LAN or Tailscale host",
symbol: "house.and.flag",
selected: true,
action: {})
OnboardingModeRow(
title: "Remote Domain",
subtitle: "VPS with domain",
symbol: "globe",
selected: false,
action: {})
}
}
.scrollContentBackground(.hidden)
.background(OpenClawBrand.activationCanvas)
}),
]
for scheme in [ColorScheme.light, ColorScheme.dark] {
for screen in screens {
let root = screen
.preferredColorScheme(scheme)
.environment(\.dynamicTypeSize, .accessibility2)
_ = Self.host(root, size: CGSize(width: 393, height: 852))
}
}
}
@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 `exec approval dialog builds on compact screens with accessibility text`() throws {
var windows: [UIWindow] = []
defer { windows.forEach { $0.isHidden = true } }
let layouts: [(CGSize, DynamicTypeSize)] = [
(CGSize(width: 320, height: 568), .accessibility5),
(CGSize(width: 568, height: 320), .accessibility3),
]
for (size, typeSize) in layouts {
let appModel = NodeAppModel()
let prompt = try #require(NodeAppModel._test_makeExecApprovalPrompt(
id: "approval-layout",
commandText: String(repeating: "/usr/bin/find /private/var/mobile/Documents ", count: 12),
warningText: String(
repeating: "This command can modify files outside the current workspace. ",
count: 12),
allowedDecisions: ["allow-once", "allow-always", "deny"],
host: "gateway.example.com",
nodeId: "node-mobile",
agentId: "main",
expiresAtMs: Int64.max))
appModel._test_presentExecApprovalPrompt(prompt)
let root = Color.clear
.execApprovalPromptDialog()
.environment(appModel)
.environment(\.dynamicTypeSize, typeSize)
windows.append(Self.host(root, size: size))
}
}
@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.gatewayConnected = 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 `root sidebar builds gateway state view hierarchies`() {
for appModel in Self.rootTabsGatewayStateModels() {
let root = RootSidebar(
model: RootSidebarModel(),
selectedDestination: .overview,
isDrawerLayout: true,
isDismissButtonEnabled: true,
selectDestination: { _ in },
hideSidebar: {})
.environment(appModel)
_ = Self.host(root, size: CGSize(width: 340, height: 852))
}
}
@Test @MainActor func `root sidebar builds landscape compact state`() {
let appModel = NodeAppModel()
let root = RootSidebar(
model: RootSidebarModel(),
selectedDestination: .chat,
isDrawerLayout: true,
isDismissButtonEnabled: true,
selectDestination: { _ in },
hideSidebar: {})
.environment(appModel)
.environment(\.horizontalSizeClass, .regular)
.environment(\.verticalSizeClass, .compact)
_ = Self.host(root, size: CGSize(width: 340, height: 393))
}
@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(
size: CGSize(width: 393, height: 852),
horizontalSizeClass: .compact,
verticalSizeClass: .regular,
sidebarVisible: false),
RootTabsShellScenario(
size: CGSize(width: 393, height: 852),
horizontalSizeClass: .compact,
verticalSizeClass: .regular,
sidebarVisible: true),
RootTabsShellScenario(
size: CGSize(width: 852, height: 393),
horizontalSizeClass: .regular,
verticalSizeClass: .compact,
sidebarVisible: false),
RootTabsShellScenario(
size: CGSize(width: 1024, height: 1366),
horizontalSizeClass: .regular,
verticalSizeClass: .regular,
sidebarVisible: true),
RootTabsShellScenario(
size: CGSize(width: 1366, height: 1024),
horizontalSizeClass: .regular,
verticalSizeClass: .regular,
sidebarVisible: true),
]
}
private struct RootTabsShellScenario {
let size: CGSize
let horizontalSizeClass: UserInterfaceSizeClass
let verticalSizeClass: UserInterfaceSizeClass
let sidebarVisible: Bool
}
}
extension GatewayDiscoveryModel.DiscoveredGateway {
fileprivate static let previewGateway = GatewayDiscoveryModel.DiscoveredGateway(
name: "Studio Gateway",
endpoint: .hostPort(
host: .name("openclaw.local", nil),
port: 18789),
stableID: "preview-gateway",
debugID: "openclaw.local",
lanHost: "openclaw.local",
tailnetDns: nil,
gatewayPort: 18789,
canvasPort: 18789,
tlsEnabled: true,
tlsFingerprintSha256: "preview",
cliPath: "/opt/homebrew/bin/openclaw")
}