Vendor OpenClaw source as Adolf fork baseline
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
This commit is contained in:
2026-07-05 09:36:54 +00:00
parent 3216769225
commit bedb527145
21108 changed files with 6010766 additions and 0 deletions

View File

@@ -0,0 +1,896 @@
import UIKit
import XCTest
@MainActor
final class OpenClawSnapshotUITests: XCTestCase {
private struct ScreenshotTarget {
let initialTab: String
let initialDestination: String
let name: String
}
private static let screenshotTargets = [
ScreenshotTarget(initialTab: "control", initialDestination: "overview", name: "01-control-connected"),
ScreenshotTarget(initialTab: "chat", initialDestination: "chat", name: "02-chat-connected"),
ScreenshotTarget(initialTab: "talk", initialDestination: "talk", name: "03-talk-connected"),
ScreenshotTarget(initialTab: "agent", initialDestination: "agents", name: "04-agent-connected"),
ScreenshotTarget(initialTab: "settings", initialDestination: "settings", name: "05-settings-connected"),
]
private var app: XCUIApplication?
override func setUpWithError() throws {
try super.setUpWithError()
continueAfterFailure = false
}
override func tearDownWithError() throws {
self.app?.terminate()
self.app = nil
try super.tearDownWithError()
}
func testConnectedGatewayTabs() {
for target in Self.screenshotTargets {
self.launchApp(for: target)
snapshot(target.name, timeWaitingForIdle: 5)
self.attachScreenshot(named: target.name)
}
}
func testOnboardingExplainsCapabilitiesAndTrust() {
let app = XCUIApplication()
app.launchArguments += ["--openclaw-reset-onboarding"]
app.launch()
self.app = app
XCTAssertTrue(app.buttons["Continue"].waitForExistence(timeout: 8))
XCTAssertTrue(app.staticTexts["Security"].exists)
let disclosure = app.staticTexts.matching(NSPredicate(
format: "label CONTAINS[c] 'camera' AND label CONTAINS[c] 'trust the gateway and agent'")).firstMatch
XCTAssertTrue(disclosure.exists)
self.attachScreenshot(named: "onboarding-capabilities-and-trust")
}
func testControlOverviewNavigation() throws {
try XCTSkipIf(UIDevice.current.userInterfaceIdiom != .phone, "Phone control hub only")
self.launchApp(for: ScreenshotTarget(
initialTab: "control",
initialDestination: "control",
name: "control-overview-navigation"))
let overview = self.app?.buttons.containing(.staticText, identifier: "Overview").firstMatch
XCTAssertTrue(overview?.waitForExistence(timeout: 5) == true)
overview?.tap()
XCTAssertTrue(self.app?.navigationBars.buttons["Control"].waitForExistence(timeout: 5) == true)
XCTAssertTrue(self.app?.buttons["Gateway settings"].waitForExistence(timeout: 5) == true)
XCTAssertEqual(self.app?.state, .runningForeground)
}
func testLocationAlwaysWaitsForSlowSystemPermissionResponse() throws {
XCUIApplication().resetAuthorizationStatus(for: .location)
self.launchApp(for: ScreenshotTarget(
initialTab: "settings",
initialDestination: "settings",
name: "location-always-slow-prompt"))
let permissions = try XCTUnwrap(
self.app?.buttons.containing(.staticText, identifier: "Permissions").firstMatch)
XCTAssertTrue(permissions.waitForExistence(timeout: 8))
permissions.tap()
let offMode = try XCTUnwrap(self.app?.buttons["Off"])
if !offMode.isSelected {
offMode.tap()
XCTAssertTrue(offMode.isSelected)
}
let alwaysMode = try XCTUnwrap(self.app?.buttons["Always"])
XCTAssertTrue(alwaysMode.waitForExistence(timeout: 5))
alwaysMode.tap()
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
let prompt = springboard.alerts.firstMatch
XCTAssertTrue(prompt.waitForExistence(timeout: 5))
Thread.sleep(forTimeInterval: 3)
XCTAssertTrue(prompt.exists)
XCTAssertTrue(alwaysMode.isSelected)
XCTAssertTrue(self.app?.staticTexts["Requesting iOS location permission…"].exists == true)
self.attachFullScreenScreenshot(named: "location-always-first-prompt-after-3s")
let firstAllow = prompt.buttons.matching(
NSPredicate(format: "label CONTAINS[c] 'While Using'")).firstMatch
XCTAssertTrue(firstAllow.exists)
firstAllow.tap()
if prompt.waitForExistence(timeout: 5) {
Thread.sleep(forTimeInterval: 3)
XCTAssertTrue(prompt.exists)
XCTAssertTrue(alwaysMode.isSelected)
XCTAssertTrue(self.app?.staticTexts["Requesting iOS location permission…"].exists == true)
self.attachFullScreenScreenshot(named: "location-always-upgrade-prompt-after-3s")
let changeToAlways = prompt.buttons.matching(
NSPredicate(format: "label CONTAINS[c] 'Change to Always'")).firstMatch
XCTAssertTrue(changeToAlways.exists)
changeToAlways.tap()
}
self.app?.activate()
XCTAssertTrue(alwaysMode.waitForExistence(timeout: 5))
XCTAssertTrue(alwaysMode.isSelected)
XCTAssertFalse(self.app?.staticTexts["Requesting iOS location permission…"].exists == true)
let backgroundAllowed = try XCTUnwrap(self.app?.staticTexts.matching(
NSPredicate(format: "label BEGINSWITH %@", "Background location requests")).firstMatch)
XCTAssertTrue(backgroundAllowed.waitForExistence(timeout: 5))
Thread.sleep(forTimeInterval: 1)
self.attachScreenshot(named: "location-always-granted-after-slow-prompt")
}
func testSettingsBackReturnsToOriginatingPhoneTab() throws {
try XCTSkipIf(UIDevice.current.userInterfaceIdiom != .phone, "Phone settings navigation only")
self.launchApp(for: ScreenshotTarget(
initialTab: "chat",
initialDestination: "chat",
name: "chat-settings-back"))
let gatewaySettings = try XCTUnwrap(self.app?.buttons["chat-gateway-status"])
XCTAssertTrue(gatewaySettings.waitForExistence(timeout: 8))
gatewaySettings.tap()
let gatewayNavigationBar = try XCTUnwrap(self.app?.navigationBars["Gateway"])
XCTAssertTrue(gatewayNavigationBar.waitForExistence(timeout: 5))
XCTAssertTrue(self.app?.tabBars.buttons["Chat"].isSelected == true)
self.attachScreenshot(named: "chat-gateway-origin-stack")
gatewayNavigationBar.buttons["BackButton"].tap()
XCTAssertTrue(gatewaySettings.waitForExistence(timeout: 5))
XCTAssertTrue(self.app?.tabBars.buttons["Chat"].isSelected == true)
self.attachScreenshot(named: "chat-after-settings-back")
self.launchApp(for: ScreenshotTarget(
initialTab: "talk",
initialDestination: "talk",
name: "talk-settings-back"))
let voiceSettings = try XCTUnwrap(self.app?.buttons["talk-voice-settings-control"])
for _ in 0..<3 where !voiceSettings.exists {
self.app?.swipeUp()
}
XCTAssertTrue(voiceSettings.waitForExistence(timeout: 8))
voiceSettings.tap()
let voiceNavigationBar = try XCTUnwrap(self.app?.navigationBars["Voice & Talk"])
XCTAssertTrue(voiceNavigationBar.waitForExistence(timeout: 5))
XCTAssertTrue(self.app?.tabBars.buttons["Talk"].isSelected == true)
voiceNavigationBar.buttons["BackButton"].tap()
XCTAssertTrue(voiceSettings.waitForExistence(timeout: 5))
XCTAssertTrue(self.app?.tabBars.buttons["Talk"].isSelected == true)
}
func testVoiceWakeResumesAfterTalkModeToggle() throws {
try XCTSkipIf(UIDevice.current.userInterfaceIdiom != .phone, "Phone Settings proof only")
self.addUIInterruptionMonitor(withDescription: "Microphone and speech permissions") { alert in
guard alert.buttons["Allow"].exists else { return false }
alert.buttons["Allow"].tap()
return true
}
self.launchApp(for: ScreenshotTarget(
initialTab: "settings",
initialDestination: "settings",
name: "voice-wake-talk-lifecycle"))
let voiceSettings = try XCTUnwrap(
self.app?.buttons.containing(.staticText, identifier: "Voice & Talk").firstMatch)
XCTAssertTrue(voiceSettings.waitForExistence(timeout: 8))
voiceSettings.tap()
let voiceWake = try XCTUnwrap(self.app?.switches["Voice Wake"])
let talkMode = try XCTUnwrap(self.app?.switches["Talk Mode"])
XCTAssertTrue(voiceWake.waitForExistence(timeout: 5))
XCTAssertTrue(talkMode.exists)
if talkMode.value as? String == "1" {
talkMode.tap()
}
if voiceWake.value as? String == "1" {
voiceWake.tap()
}
voiceWake.tap()
XCTAssertEqual(voiceWake.value as? String, "1")
talkMode.tap()
XCTAssertEqual(talkMode.value as? String, "1")
talkMode.tap()
XCTAssertEqual(talkMode.value as? String, "0")
XCTAssertEqual(voiceWake.value as? String, "1")
XCTAssertEqual(self.app?.state, .runningForeground)
self.attachScreenshot(named: "voice-wake-after-talk-resume")
let voiceNavigationBar = try XCTUnwrap(self.app?.navigationBars["Voice & Talk"])
voiceNavigationBar.buttons["BackButton"].tap()
let diagnostics = try XCTUnwrap(
self.app?.buttons.containing(.staticText, identifier: "Diagnostics").firstMatch)
XCTAssertTrue(diagnostics.waitForExistence(timeout: 5))
diagnostics.tap()
let voiceWakeStatus = try XCTUnwrap(
self.app?.descendants(matching: .any)["diagnostics-voice-wake-status"])
XCTAssertTrue(voiceWakeStatus.waitForExistence(timeout: 5))
let resumed = expectation(
for: NSPredicate(
format: "value == %@",
"Voice Wake isnt supported on Simulator"),
evaluatedWith: voiceWakeStatus)
wait(for: [resumed], timeout: 5)
let diagnosticsNavigationBar = try XCTUnwrap(self.app?.navigationBars["Diagnostics"])
diagnosticsNavigationBar.buttons["BackButton"].tap()
voiceSettings.tap()
XCTAssertTrue(voiceWake.waitForExistence(timeout: 5))
voiceWake.tap()
XCTAssertEqual(voiceWake.value as? String, "0")
}
func testChatComposerStartsCompactAndGrowsWithDraft() throws {
try XCTSkipIf(UIDevice.current.userInterfaceIdiom != .phone, "Phone composer proof only")
self.launchApp(for: ScreenshotTarget(
initialTab: "chat",
initialDestination: "chat",
name: "chat-composer-growth"))
let textField = try XCTUnwrap(app?.textFields["chat-message-input"])
XCTAssertTrue(textField.waitForExistence(timeout: 8))
let talkButton = try XCTUnwrap(app?.buttons["chat-realtime-control"])
XCTAssertTrue(talkButton.waitForExistence(timeout: 5))
let attachmentButton = try XCTUnwrap(app?.buttons["chat-attachment-picker"])
XCTAssertTrue(attachmentButton.waitForExistence(timeout: 5))
let composerSurface = try XCTUnwrap(app?.otherElements["chat-composer-surface"])
XCTAssertTrue(composerSurface.waitForExistence(timeout: 5))
let gatewayStatus = try XCTUnwrap(app?.buttons["chat-gateway-status"])
XCTAssertTrue(gatewayStatus.waitForExistence(timeout: 5))
let sendButton = try XCTUnwrap(app?.buttons["chat-send-message"])
XCTAssertFalse(sendButton.exists)
XCTAssertLessThanOrEqual(attachmentButton.frame.maxX, composerSurface.frame.minX)
XCTAssertLessThanOrEqual(composerSurface.frame.maxX, talkButton.frame.minX)
XCTAssertGreaterThanOrEqual(attachmentButton.frame.width, 44)
XCTAssertGreaterThanOrEqual(attachmentButton.frame.height, 44)
XCTAssertGreaterThanOrEqual(talkButton.frame.width, 44)
XCTAssertGreaterThanOrEqual(talkButton.frame.height, 44)
let compactHeight = textField.frame.height
XCTAssertLessThanOrEqual(compactHeight, 44)
XCTAssertLessThanOrEqual(abs(talkButton.frame.midY - textField.frame.midY), 1)
self.attachScreenshot(named: "chat-composer-compact")
textField.tap()
textField.typeText(
"Draft a polished launch note that covers the new design, validation, rollout plan, " +
"and follow-up details for the team.")
let composerGrew = expectation(
for: NSPredicate { _, _ in textField.frame.height >= compactHeight + 12 },
evaluatedWith: textField)
wait(for: [composerGrew], timeout: 4)
XCTAssertTrue(sendButton.waitForExistence(timeout: 3))
XCTAssertTrue(talkButton.waitForNonExistence(timeout: 3))
XCTAssertGreaterThanOrEqual(sendButton.frame.width, 44)
XCTAssertGreaterThanOrEqual(sendButton.frame.height, 44)
self.attachScreenshot(named: "chat-composer-expanded")
self.app?.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.2)).tap()
XCTAssertTrue(self.app?.keyboards.firstMatch.waitForNonExistence(timeout: 3) == true)
}
func testChatPresentationInLightAppearance() throws {
try XCTSkipIf(UIDevice.current.userInterfaceIdiom != .phone, "Phone chat proof only")
self.launchApp(
for: ScreenshotTarget(
initialTab: "chat",
initialDestination: "chat",
name: "chat-light"),
appearance: "light")
XCTAssertTrue(self.app?.buttons["chat-gateway-status"].waitForExistence(timeout: 8) == true)
XCTAssertTrue(self.app?.otherElements["chat-composer-surface"].exists == true)
self.attachScreenshot(named: "chat-light")
}
func testEmptyChatStarterPromptSendsMessage() throws {
self.launchApp(
for: ScreenshotTarget(
initialTab: "chat",
initialDestination: "chat",
name: "chat-empty-starters"),
additionalArguments: ["--openclaw-empty-chat-fixture"])
let starter = try XCTUnwrap(self.app?.buttons["chat-starter-summarize-status"])
XCTAssertTrue(starter.waitForExistence(timeout: 8))
XCTAssertTrue(self.app?.staticTexts["What would you like to work on?"].exists == true)
self.attachScreenshot(named: "chat-empty-starters")
starter.tap()
let sentText = "Summarize the current OpenClaw status and tell me what needs attention."
let sentRows = self.app?.staticTexts.matching(NSPredicate(format: "label == %@", sentText))
XCTAssertTrue(sentRows?.firstMatch.waitForExistence(timeout: 5) == true)
XCTAssertEqual(sentRows?.count, 1)
XCTAssertTrue(
self.app?.staticTexts.matching(NSPredicate(format: "label CONTAINS %@", "I can help with"))
.firstMatch.waitForExistence(timeout: 5) == true)
self.attachScreenshot(named: "chat-starter-response")
}
func testEmptyChatStarterPromptsLocalizeInGerman() throws {
self.launchApp(
for: ScreenshotTarget(
initialTab: "chat",
initialDestination: "chat",
name: "chat-empty-starters-german"),
additionalArguments: [
"--openclaw-empty-chat-fixture",
"-AppleLanguages",
"(de)",
"-AppleLocale",
"de_DE",
])
XCTAssertTrue(self.app?.staticTexts["Woran möchtest du arbeiten?"].waitForExistence(timeout: 8) == true)
let starter = try XCTUnwrap(self.app?.buttons["OpenClaw-Status prüfen"])
XCTAssertTrue(starter.exists)
starter.tap()
XCTAssertTrue(
self.app?.staticTexts[
"Fasse den aktuellen OpenClaw-Status zusammen und sage mir, was Aufmerksamkeit erfordert."
].waitForExistence(timeout: 5) == true)
self.attachScreenshot(named: "chat-empty-starters-german")
}
func testOnboardingPairCommandAndCompletionOpenChat() throws {
try XCTSkipIf(UIDevice.current.userInterfaceIdiom != .phone, "Phone onboarding proof only")
self.addUIInterruptionMonitor(withDescription: "Local network access") { alert in
guard alert.buttons["Allow"].exists else { return false }
alert.buttons["Allow"].tap()
return true
}
let app = XCUIApplication()
setupSnapshot(app)
app.launchArguments += [
"--openclaw-reset-onboarding",
"--openclaw-initial-tab",
"settings",
"--openclaw-initial-destination",
"settings",
]
app.launch()
self.app = app
XCTAssertTrue(app.buttons["Continue"].waitForExistence(timeout: 8))
app.buttons["Continue"].tap()
app.tap()
let copyPairCommand = app.buttons["onboarding-copy-pair-command"]
XCTAssertTrue(copyPairCommand.waitForExistence(timeout: 8))
copyPairCommand.tap()
XCTAssertEqual(copyPairCommand.label, "Pair command copied")
self.attachScreenshot(named: "onboarding-copy-pair-command")
app.buttons["Set Up Manually"].tap()
let setupCode = app.textFields["Paste setup code"]
XCTAssertTrue(setupCode.waitForExistence(timeout: 5))
setupCode.tap()
setupCode.typeText("APPLE-REVIEW-DEMO")
app.buttons["Done"].tap()
app.buttons["Apply Setup Code"].tap()
XCTAssertTrue(app.staticTexts["Connected"].waitForExistence(timeout: 8))
XCTAssertTrue(app.staticTexts["Apple Review Demo Gateway"].exists)
XCTAssertFalse(app.staticTexts["Local demo mode"].exists)
self.attachScreenshot(named: "onboarding-connected-go-to-chat")
app.buttons["Go to Chat"].tap()
XCTAssertTrue(app.tabBars.buttons["Chat"].waitForExistence(timeout: 5))
XCTAssertTrue(app.tabBars.buttons["Chat"].isSelected)
}
func testTalkUsesNativeControls() throws {
try XCTSkipIf(UIDevice.current.userInterfaceIdiom != .phone, "Phone Talk controls only")
self.launchApp(for: ScreenshotTarget(
initialTab: "talk",
initialDestination: "talk",
name: "talk-native-controls"))
let speakerphone = try XCTUnwrap(app?.switches["talk-speakerphone-control"])
let backgroundListening = try XCTUnwrap(app?.switches["talk-background-listening-control"])
let voiceSettings = try XCTUnwrap(app?.buttons["talk-voice-settings-control"])
for _ in 0..<3 where !speakerphone.exists {
self.app?.swipeUp()
}
XCTAssertTrue(speakerphone.waitForExistence(timeout: 8))
XCTAssertTrue(backgroundListening.exists)
XCTAssertTrue(voiceSettings.exists)
let originalValue = speakerphone.value as? String
defer {
if speakerphone.value as? String != originalValue {
speakerphone.coordinate(withNormalizedOffset: CGVector(dx: 0.9, dy: 0.5)).tap()
}
}
if originalValue == "0" {
speakerphone.coordinate(withNormalizedOffset: CGVector(dx: 0.9, dy: 0.5)).tap()
self.waitForValue("1", of: speakerphone)
}
XCTAssertEqual(speakerphone.value as? String, "1")
self.attachScreenshot(named: "talk-native-controls")
speakerphone.coordinate(withNormalizedOffset: CGVector(dx: 0.9, dy: 0.5)).tap()
self.waitForValue("0", of: speakerphone)
}
func testAppearanceUsesSettingsRow() throws {
self.launchApp(for: ScreenshotTarget(
initialTab: "settings",
initialDestination: "settings",
name: "appearance-compact"), appearance: nil, screenshotMode: false)
if self.app?.buttons["Close"].waitForExistence(timeout: 2) == true {
self.app?.buttons["Close"].tap()
}
let row = try XCTUnwrap(self.app?.buttons["settings-appearance-row"])
XCTAssertTrue(row.waitForExistence(timeout: 8))
XCTAssertFalse(self.app?.buttons["settings-appearance-menu"].exists == true)
XCTAssertFalse(self.app?.segmentedControls["settings-appearance-picker"].exists == true)
row.tap()
let navigationBar = try XCTUnwrap(self.app?.navigationBars["Appearance"])
XCTAssertTrue(navigationBar.waitForExistence(timeout: 3))
let system = try XCTUnwrap(self.app?.buttons["settings-appearance-system"])
let light = try XCTUnwrap(self.app?.buttons["settings-appearance-light"])
let dark = try XCTUnwrap(self.app?.buttons["settings-appearance-dark"])
XCTAssertTrue(system.exists)
XCTAssertTrue(light.exists)
XCTAssertTrue(dark.exists)
if system.value as? String != "Selected" {
system.tap()
XCTAssertTrue(row.waitForExistence(timeout: 3))
self.waitForValue("System", of: row)
row.tap()
XCTAssertTrue(navigationBar.waitForExistence(timeout: 3))
self.waitForValue("Selected", of: system)
}
Thread.sleep(forTimeInterval: 1)
self.attachScreenshot(named: "appearance-system")
dark.tap()
XCTAssertTrue(row.waitForExistence(timeout: 3))
self.waitForValue("Dark", of: row)
self.assertDarkAppearanceTextVisible()
self.attachScreenshot(named: "settings-dark")
row.tap()
XCTAssertTrue(navigationBar.waitForExistence(timeout: 3))
system.tap()
XCTAssertTrue(row.waitForExistence(timeout: 3))
self.waitForValue("System", of: row)
Thread.sleep(forTimeInterval: 1)
self.attachScreenshot(named: "appearance-system-restored")
}
func testChatReturnsToOriginatingControlDetail() throws {
try XCTSkipIf(UIDevice.current.userInterfaceIdiom != .phone, "Phone Control proof only")
self.launchApp(for: ScreenshotTarget(
initialTab: "control",
initialDestination: "activity",
name: "control-chat-return"))
let chatTab = try XCTUnwrap(self.app?.tabBars.buttons["Chat"])
let controlTab = try XCTUnwrap(self.app?.tabBars.buttons["Control"])
// Retain an embedded Chat Settings route, then prove contextual routing pops it.
chatTab.tap()
let gatewaySettings = try XCTUnwrap(self.app?.buttons["chat-gateway-status"])
XCTAssertTrue(gatewaySettings.waitForExistence(timeout: 5))
gatewaySettings.tap()
XCTAssertTrue(self.app?.navigationBars["Gateway"].waitForExistence(timeout: 5) == true)
controlTab.tap()
XCTAssertTrue(self.app?.navigationBars["Control"].waitForExistence(timeout: 8) == true)
let activity = try XCTUnwrap(self.app?.buttons["Activity"])
XCTAssertTrue(activity.waitForExistence(timeout: 5))
activity.tap()
let recentActivity = try XCTUnwrap(self.app?.staticTexts["Recent activity"])
XCTAssertTrue(recentActivity.waitForExistence(timeout: 8))
self.attachScreenshot(named: "control-activity-before-chat")
let activityChat = try self.controlDetailChatButton(above: chatTab)
activityChat.tap()
XCTAssertTrue(chatTab.isSelected)
let returnButton = try XCTUnwrap(self.app?.buttons["OpenClawChatBackToControlDetailButton"])
XCTAssertTrue(returnButton.waitForExistence(timeout: 5))
XCTAssertEqual(returnButton.label, "Back to Activity")
self.attachScreenshot(named: "chat-return-to-activity")
returnButton.tap()
XCTAssertTrue(recentActivity.waitForExistence(timeout: 8))
XCTAssertTrue(controlTab.isSelected)
self.attachScreenshot(named: "control-activity-after-chat")
try self.controlDetailChatButton(above: chatTab).tap()
XCTAssertTrue(chatTab.isSelected)
controlTab.tap()
XCTAssertTrue(self.app?.navigationBars["Control"].waitForExistence(timeout: 8) == true)
let overview = try XCTUnwrap(self.app?.buttons["Overview"])
XCTAssertTrue(overview.exists)
self.attachScreenshot(named: "control-tab-returns-to-root")
overview.tap()
XCTAssertTrue(self.app?.staticTexts["Agent session"].waitForExistence(timeout: 8) == true)
let agentSession = try XCTUnwrap(
self.app?.buttons.containing(.staticText, identifier: "Molty").firstMatch)
XCTAssertTrue(agentSession.waitForExistence(timeout: 8))
agentSession.tap()
XCTAssertTrue(returnButton.waitForExistence(timeout: 8))
XCTAssertEqual(returnButton.label, "Back to Overview")
self.attachScreenshot(named: "chat-session-return-to-overview")
returnButton.tap()
XCTAssertTrue(self.app?.navigationBars["Overview"].waitForExistence(timeout: 8) == true)
}
func testAgentUsesToolbarFilter() throws {
try XCTSkipIf(UIDevice.current.userInterfaceIdiom != .phone, "Phone Agent proof only")
self.launchApp(for: ScreenshotTarget(
initialTab: "agent",
initialDestination: "agents",
name: "agent-toolbar-filter"))
let menu = try XCTUnwrap(app?.buttons["agent-status-filter-menu"])
XCTAssertTrue(menu.waitForExistence(timeout: 8))
XCTAssertFalse(self.app?.segmentedControls["Agent status"].exists == true)
menu.tap()
XCTAssertTrue(self.app?.buttons["All"].waitForExistence(timeout: 3) == true)
XCTAssertTrue(self.app?.buttons["Online"].exists == true)
XCTAssertTrue(self.app?.buttons["Ready"].exists == true)
self.attachScreenshot(named: "agent-toolbar-filter")
}
func testLiveGatewayChatRoundTripAndControlOverview() throws {
try XCTSkipIf(UIDevice.current.userInterfaceIdiom != .phone, "Phone chat proof only")
let app = try launchPairedLiveGatewayApp(initialTab: "chat", initialDestination: "chat")
// Build scrollable history through the paired app before checking reader behavior.
for index in 0..<3 {
let seedMarker = "OPENCLAW_E2E_SEED_\(index)_\(Int(Date().timeIntervalSince1970 * 1000))"
let seedContext = String(repeating: "Reader context \(index). ", count: 6)
self.sendLiveGatewayMessage(
"\(seedContext)Reply exactly with \(seedMarker) and no other text.",
expecting: seedMarker,
in: app)
}
let replyMarker = "OPENCLAW_E2E_OK_\(Int(Date().timeIntervalSince1970 * 1000))"
self.sendLiveGatewayMessage(
"Reply exactly with \(replyMarker) and no other text.",
expecting: replyMarker,
in: app)
let jumpToLatest = app.buttons["Jump to latest reply"]
XCTAssertTrue(jumpToLatest.waitForExistence(timeout: 3))
self.attachScreenshot(named: "live-gateway-chat-reply-anchored")
jumpToLatest.tap()
XCTAssertTrue(jumpToLatest.waitForNonExistence(timeout: 3))
XCTAssertTrue(app.staticTexts[replyMarker].exists)
Thread.sleep(forTimeInterval: 0.5)
self.attachScreenshot(named: "live-gateway-chat-jumped-to-latest")
let transcript = app.scrollViews.firstMatch
XCTAssertTrue(transcript.exists)
transcript.swipeDown()
XCTAssertTrue(jumpToLatest.waitForExistence(timeout: 3))
self.attachScreenshot(named: "live-gateway-chat-manual-departure")
jumpToLatest.tap()
XCTAssertTrue(jumpToLatest.waitForNonExistence(timeout: 3))
let controlApp = self.relaunchConnectedLiveGatewayApp(
initialTab: "control",
initialDestination: "control")
let overview = controlApp.buttons.containing(.staticText, identifier: "Overview").firstMatch
XCTAssertTrue(overview.waitForExistence(timeout: 8))
self.attachScreenshot(named: "live-gateway-control")
overview.tap()
XCTAssertTrue(controlApp.navigationBars.buttons["Control"].waitForExistence(timeout: 8))
XCTAssertTrue(controlApp.buttons["Gateway settings"].waitForExistence(timeout: 5))
self.attachScreenshot(named: "live-gateway-overview")
XCTAssertEqual(controlApp.state, .runningForeground)
}
func testManualAuthRetryUsesEditedToken() throws {
try XCTSkipUnless(
ProcessInfo.processInfo.environment["OPENCLAW_IOS_RETRY_E2E"] == "1",
"Set OPENCLAW_IOS_RETRY_E2E=1 with a local token-auth Gateway on port 18920")
let token = try XCTUnwrap(ProcessInfo.processInfo.environment["OPENCLAW_IOS_RETRY_TOKEN"])
let app = XCUIApplication()
addUIInterruptionMonitor(withDescription: "Local network access") { alert in
guard alert.buttons["Allow"].exists else { return false }
alert.buttons["Allow"].tap()
return true
}
app.launchArguments += ["--openclaw-reset-onboarding"]
app.launch()
self.app = app
XCTAssertTrue(app.buttons["Continue"].waitForExistence(timeout: 8))
app.buttons["Continue"].tap()
app.tap()
XCTAssertTrue(app.buttons["Set Up Manually"].waitForExistence(timeout: 8))
app.buttons["Set Up Manually"].tap()
let developerMode = app.buttons["Developer mode"]
if developerMode.value as? String != "On" {
developerMode.tap()
}
app.buttons.matching(NSPredicate(format: "label BEGINSWITH %@", "Same Machine (Dev)")).firstMatch.tap()
app.buttons["Continue"].tap()
let port = app.textFields["Port"]
XCTAssertTrue(port.waitForExistence(timeout: 5))
port.tap()
port.typeText(String(repeating: XCUIKeyboardKey.delete.rawValue, count: 5) + "18920")
app.buttons["Connect"].tap()
let tokenField = app.secureTextFields["Gateway Auth Token"]
XCTAssertTrue(tokenField.waitForExistence(timeout: 20))
tokenField.tap()
tokenField.typeText(token)
app.buttons["Done"].tap()
app.buttons["Retry Connection"].tap()
XCTAssertTrue(app.staticTexts["Connected"].waitForExistence(timeout: 30))
self.attachScreenshot(named: "manual-auth-retry-connected")
}
func testPhotosLimitedAccess() throws {
try XCTSkipUnless(
ProcessInfo.processInfo.environment["OPENCLAW_IOS_PHOTOS_E2E"] == "1",
"Set OPENCLAW_IOS_PHOTOS_E2E=1 to exercise the system Photos prompt")
addUIInterruptionMonitor(withDescription: "Photos access") { alert in
for title in ["Limit Access…", "Select Photos…"] where alert.buttons[title].exists {
alert.buttons[title].tap()
return true
}
return false
}
self.launchApp(for: ScreenshotTarget(
initialTab: "settings",
initialDestination: "settings",
name: "photos-limited-access"))
let permissions = try XCTUnwrap(
self.app?.buttons.containing(.staticText, identifier: "Permissions").firstMatch)
XCTAssertTrue(permissions.waitForExistence(timeout: 8))
permissions.tap()
let privacy = try XCTUnwrap(
self.app?.buttons.containing(.staticText, identifier: "Privacy & Access").firstMatch)
XCTAssertTrue(privacy.waitForExistence(timeout: 8))
privacy.tap()
let request = try XCTUnwrap(self.app?.buttons["privacy-access-Photos-action"])
XCTAssertTrue(request.waitForExistence(timeout: 5))
request.tap()
self.app?.tap()
// The limited picker is an out-of-process system surface without stable accessibility identifiers.
// Normalized taps are confined to this opt-in simulator test; app-owned state proves completion below.
let screen = XCUIApplication(bundleIdentifier: "com.apple.springboard")
screen.coordinate(withNormalizedOffset: CGVector(dx: 0.17, dy: 0.43)).tap()
screen.coordinate(withNormalizedOffset: CGVector(dx: 0.90, dy: 0.16)).tap()
self.app?.activate()
let limitedStatus = try XCTUnwrap(self.app?.staticTexts.matching(
NSPredicate(
format: "identifier == %@ AND label == %@",
"privacy-access-Photos-status",
"Limited")).firstMatch)
XCTAssertTrue(limitedStatus.waitForExistence(timeout: 8))
XCTAssertEqual(self.app?.buttons["privacy-access-Photos-action"].label, "Manage Access")
self.attachScreenshot(named: "photos-limited-access")
}
private func launchApp(
for target: ScreenshotTarget,
appearance: String? = "dark",
screenshotMode: Bool = true,
additionalArguments: [String] = [])
{
self.app?.terminate()
let app = XCUIApplication()
setupSnapshot(app)
app.launchArguments += [
"--openclaw-initial-tab",
target.initialTab,
"--openclaw-initial-destination",
target.initialDestination,
"--openclaw-sidebar-visibility",
"hidden",
]
if screenshotMode {
app.launchArguments.append("--openclaw-screenshot-mode")
}
app.launchArguments += additionalArguments
if let appearance {
app.launchArguments += ["--openclaw-appearance", appearance]
}
app.launch()
self.app = app
XCTAssertTrue(app.wait(for: .runningForeground, timeout: 8))
}
private func waitForValue(_ value: String, of element: XCUIElement) {
let expectation = XCTNSPredicateExpectation(
predicate: NSPredicate(format: "value == %@", value),
object: element)
XCTAssertEqual(XCTWaiter.wait(for: [expectation], timeout: 3), .completed)
}
private func controlDetailChatButton(above chatTab: XCUIElement) throws -> XCUIElement {
let buttons = try XCTUnwrap(self.app?.buttons.matching(NSPredicate(format: "label == 'Chat'")))
return try XCTUnwrap(buttons.allElementsBoundByIndex.first { $0.frame.maxY < chatTab.frame.minY })
}
private func launchPairedLiveGatewayApp(
initialTab: String,
initialDestination: String) throws -> XCUIApplication
{
try XCTSkipUnless(
ProcessInfo.processInfo.environment["OPENCLAW_IOS_LIVE_GATEWAY"] == "1",
"Set OPENCLAW_IOS_LIVE_GATEWAY=1 and provide a fresh setup code")
if let setupCode = ProcessInfo.processInfo.environment["OPENCLAW_IOS_LIVE_SETUP_CODE"] {
UIPasteboard.general.string = setupCode
}
let app = XCUIApplication()
addUIInterruptionMonitor(withDescription: "Local network access") { alert in
guard alert.buttons["Allow"].exists else { return false }
alert.buttons["Allow"].tap()
return true
}
app.launchArguments += [
"--openclaw-reset-onboarding",
"--openclaw-initial-tab",
initialTab,
"--openclaw-initial-destination",
initialDestination,
]
app.launch()
self.app = app
XCTAssertTrue(app.buttons["Continue"].waitForExistence(timeout: 8))
app.buttons["Continue"].tap()
app.tap()
XCTAssertTrue(app.buttons["Set Up Manually"].waitForExistence(timeout: 8))
app.buttons["Set Up Manually"].tap()
let setupCodeField = app.textFields["Paste setup code"]
XCTAssertTrue(setupCodeField.waitForExistence(timeout: 5))
setupCodeField.tap()
setupCodeField.press(forDuration: 1)
XCTAssertTrue(app.menuItems["Paste"].waitForExistence(timeout: 3))
app.menuItems["Paste"].tap()
app.buttons["Done"].tap()
app.buttons["Apply Setup Code"].tap()
XCTAssertTrue(app.staticTexts["Connected"].waitForExistence(timeout: 45))
app.buttons["Go to Chat"].tap()
return app
}
private func relaunchConnectedLiveGatewayApp(
initialTab: String,
initialDestination: String) -> XCUIApplication
{
self.app?.terminate()
let app = XCUIApplication()
app.launchArguments += [
"--openclaw-initial-tab",
initialTab,
"--openclaw-initial-destination",
initialDestination,
]
app.launch()
self.app = app
XCTAssertTrue(app.wait(for: .runningForeground, timeout: 8))
return app
}
private func sendLiveGatewayMessage(
_ text: String,
expecting replyMarker: String,
in app: XCUIApplication)
{
let input = app.textFields["chat-message-input"]
XCTAssertTrue(input.waitForExistence(timeout: 8))
input.tap()
input.typeText(text)
let send = app.buttons["chat-send-message"]
XCTAssertTrue(send.waitForExistence(timeout: 3))
XCTAssertTrue(send.isEnabled)
app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.2)).tap()
XCTAssertTrue(app.keyboards.firstMatch.waitForNonExistence(timeout: 3))
send.tap()
XCTAssertTrue(app.staticTexts[replyMarker].waitForExistence(timeout: 60))
XCTAssertTrue(app.staticTexts["Writing"].waitForNonExistence(timeout: 5))
}
private func assertDarkAppearanceTextVisible(
file: StaticString = #filePath,
line: UInt = #line)
{
guard let app, let image = app.screenshot().image.cgImage else {
XCTFail("App screenshot has no CGImage", file: file, line: line)
return
}
let width = image.width
let height = image.height
var pixels = [UInt8](repeating: 0, count: width * height * 4)
let rendered = pixels.withUnsafeMutableBytes { buffer in
guard let context = CGContext(
data: buffer.baseAddress,
width: width,
height: height,
bitsPerComponent: 8,
bytesPerRow: width * 4,
space: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)
else {
return false
}
context.draw(image, in: CGRect(x: 0, y: 0, width: width, height: height))
return true
}
guard rendered else {
XCTFail("Could not render the appearance screenshot", file: file, line: line)
return
}
// Sample the full List content, excluding navigation/tab chrome. The regression left
// entire labels transparent while isolated row crops could still look healthy.
let sampleX = (width / 12)..<(width * 11 / 12)
let sampleY = (height / 8)..<(height * 4 / 5)
var brightPixels = 0
for y in sampleY {
for x in sampleX {
let offset = (y * width + x) * 4
if pixels[offset] > 190, pixels[offset + 1] > 190, pixels[offset + 2] > 190 {
brightPixels += 1
}
}
}
let sampledPixels = max(1, sampleX.count * sampleY.count)
XCTAssertGreaterThan(
Double(brightPixels) / Double(sampledPixels),
0.002,
"Dark appearance must keep the settings labels visibly light",
file: file,
line: line)
}
private func attachScreenshot(named name: String) {
guard let app else { return }
let attachment = XCTAttachment(screenshot: app.screenshot())
attachment.name = name
attachment.lifetime = .keepAlways
add(attachment)
}
private func attachFullScreenScreenshot(named name: String) {
let attachment = XCTAttachment(screenshot: XCUIScreen.main.screenshot())
attachment.name = name
attachment.lifetime = .keepAlways
add(attachment)
}
}