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
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:
896
apps/ios/UITests/OpenClawSnapshotUITests.swift
Normal file
896
apps/ios/UITests/OpenClawSnapshotUITests.swift
Normal 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 isn’t 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user