fix(ios): stabilize release screenshot launches

This commit is contained in:
joshavant
2026-07-23 00:24:47 -05:00
parent 89cfea034a
commit ebbbfa4c30
6 changed files with 67 additions and 25 deletions

View File

@@ -721,7 +721,9 @@ struct OpenClawApp: App {
.environment(self.appModel.voiceWake)
.environment(self.gatewayController)
.task {
self.voiceLiveActivityCoordinator.start(appModel: self.appModel)
if !Self.screenshotModeEnabled {
self.voiceLiveActivityCoordinator.start(appModel: self.appModel)
}
self.appModel.setScenePhase(self.scenePhase)
self.appDelegate.appModel = self.appModel
self.appDelegate.scenePhaseChanged(self.scenePhase)

View File

@@ -135,6 +135,24 @@ struct RootTabs: View {
self.rootOverlays(
self.sidebarSplitContent
.tint(OpenClawBrand.accent))))
.overlay(alignment: .topLeading) {
self.uiTestReadinessMarker
}
}
@ViewBuilder
private var uiTestReadinessMarker: some View {
#if DEBUG
if ProcessInfo.processInfo.arguments.contains("--openclaw-ui-test-readiness") {
Color.clear
.frame(width: 1, height: 1)
.allowsHitTesting(false)
.accessibilityElement(children: .ignore)
.accessibilityIdentifier("RootTabs.Ready")
.accessibilityLabel(Text(verbatim: "OpenClaw test readiness"))
.accessibilityValue(self.scenePhase == .active ? "ready" : "inactive")
}
#endif
}
private var sidebarSplitContent: some View {

View File

@@ -204,6 +204,7 @@ struct LiveActivityPresentationArbiterTests {
"LiveActivity/VoiceLiveActivityCoordinator.swift"),
encoding: .utf8)
#expect(appSource.contains("if !Self.screenshotModeEnabled {"))
#expect(appSource.contains("self.voiceLiveActivityCoordinator.start(appModel: self.appModel)"))
#expect(coordinatorSource.contains("LiveActivityManager.shared.showVoice("))
#expect(coordinatorSource.contains("LiveActivityManager.shared.updateVoiceLevel("))

View File

@@ -15,7 +15,7 @@ final class OpenClawSnapshotUITests: XCTestCase {
ScreenshotTarget(initialTab: "agent", initialDestination: "agents", name: "03-agent-connected"),
ScreenshotTarget(initialTab: "settings", initialDestination: "settings", name: "04-settings-connected"),
]
private static let screenshotLaunchRetryThreshold: TimeInterval = 30
private static let appReadinessAccessibilityIdentifier = "RootTabs.Ready"
private var app: XCUIApplication?
@@ -25,8 +25,7 @@ final class OpenClawSnapshotUITests: XCTestCase {
}
override func tearDownWithError() throws {
self.app?.terminate()
self.app = nil
self.terminateCurrentApp()
try super.tearDownWithError()
}
@@ -723,6 +722,16 @@ final class OpenClawSnapshotUITests: XCTestCase {
XCTAssertTrue(self.app?.buttons["Online"].exists == true)
XCTAssertTrue(self.app?.buttons["Ready"].exists == true)
self.attachScreenshot(named: "agent-toolbar-filter")
// Native context menus must finish their dismissal before teardown. Killing
// the app with this menu open can leave the next app scene inactive.
self.app?.buttons["Ready"].tap()
self.waitForValue("Ready", of: menu)
menu.tap()
let all = try XCTUnwrap(self.app?.buttons["All"])
XCTAssertTrue(all.waitForExistence(timeout: 3))
all.tap()
self.waitForValue("All", of: menu)
}
func testLiveGatewayFreshInstallSetupAndRelaunch() throws {
@@ -908,7 +917,7 @@ final class OpenClawSnapshotUITests: XCTestCase {
screenshotMode: Bool = true,
additionalArguments: [String] = [])
{
self.app?.terminate()
self.terminateCurrentApp()
let app = XCUIApplication()
setupSnapshot(app)
@@ -919,6 +928,7 @@ final class OpenClawSnapshotUITests: XCTestCase {
target.initialDestination,
"--openclaw-sidebar-visibility",
"hidden",
"--openclaw-ui-test-readiness",
]
if screenshotMode {
app.launchArguments.append("--openclaw-screenshot-mode")
@@ -927,31 +937,39 @@ final class OpenClawSnapshotUITests: XCTestCase {
if let appearance {
app.launchArguments += ["--openclaw-appearance", appearance]
}
let launchStartedAt = Date()
app.launch()
self.app = app
if screenshotMode,
Date().timeIntervalSince(launchStartedAt) >= Self.screenshotLaunchRetryThreshold
{
// Xcode can return after its idle timeout while the screenshot scene is still
// inactive; one fresh launch avoids querying a stalled accessibility tree.
app.terminate()
let retryStartedAt = Date()
app.launch()
XCTAssertLessThan(
Date().timeIntervalSince(retryStartedAt),
Self.screenshotLaunchRetryThreshold,
"Screenshot app launch stalled again after one retry")
}
XCTAssertTrue(app.wait(for: .runningForeground, timeout: 8))
let readiness = app.descendants(matching: .any)[Self.appReadinessAccessibilityIdentifier]
XCTAssertTrue(
readiness.waitForExistence(timeout: 8),
"OpenClaw root readiness marker did not appear")
self.waitForValue("ready", of: readiness, timeout: 8)
}
private func waitForValue(_ value: String, of element: XCUIElement) {
private func terminateCurrentApp(
file: StaticString = #filePath,
line: UInt = #line)
{
guard let app = self.app else { return }
app.terminate()
XCTAssertTrue(
app.wait(for: .notRunning, timeout: 5),
"OpenClaw did not terminate before the next launch",
file: file,
line: line)
self.app = nil
}
private func waitForValue(
_ value: String,
of element: XCUIElement,
timeout: TimeInterval = 3)
{
let expectation = XCTNSPredicateExpectation(
predicate: NSPredicate(format: "value == %@", value),
object: element)
XCTAssertEqual(XCTWaiter.wait(for: [expectation], timeout: 3), .completed)
XCTAssertEqual(XCTWaiter.wait(for: [expectation], timeout: timeout), .completed)
}
private func waitForEnabled(_ element: XCUIElement) {

View File

@@ -1680,9 +1680,9 @@ platform :ios do
configuration: "Debug",
devices: [device],
languages: ["en-US"],
# The scheme also carries functional UI tests (share extension); the
# screenshot lane must only drive the deterministic snapshot suite.
only_testing: ["OpenClawUITests/OpenClawSnapshotUITests"],
# App Store capture is isolated from functional UI checks; this lane only
# needs the deterministic four-screenshot release flow.
only_testing: ["OpenClawUITests/OpenClawSnapshotUITests/testConnectedGatewayTabs"],
launch_arguments: ["--openclaw-screenshot-mode"],
output_directory: output_directory,
clear_previous_screenshots: index.zero?,

View File

@@ -118,6 +118,9 @@ describe("iOS Fastlane release upload gates", () => {
const verifier = functionBody(fastfile, "verify_snapshot_test_result!");
expect(screenshots).toContain("snapshot_devices.each_with_index");
expect(screenshots).toContain(
'only_testing: ["OpenClawUITests/OpenClawSnapshotUITests/testConnectedGatewayTabs"]',
);
expect(screenshots).toContain("result_bundle: true");
expect(screenshots).toContain("number_of_retries: 0");
expect(screenshots).toContain("stop_after_first_error: true");