mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 12:31:32 +00:00
fix(ios): stabilize release screenshot launches
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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("))
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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?,
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user