diff --git a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerRequestPanel.swift b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerRequestPanel.swift index b712353a15..6c43319cd2 100644 --- a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerRequestPanel.swift +++ b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerRequestPanel.swift @@ -118,15 +118,23 @@ extension RunnerTests { return windows } - func elementQueries(app: XCUIApplication, matching type: XCUIElement.ElementType) throws -> [XCUIElementQuery] { - try elementQueries(root: app, matching: type, litDisplayID: requestLitPanel?.displayID) + /// One walk of the roots for every type, so a multi-type read walks the lit windows once. + func elementQueries(app: XCUIApplication, matching types: XCUIElement.ElementType...) throws -> [XCUIElementQuery] { + queries(matching: types, in: try elementSearchRoots(root: app, litDisplayID: requestLitPanel?.displayID)) } func elementQueries( root: XCUIElement, - matching type: XCUIElement.ElementType, + matching types: XCUIElement.ElementType..., litDisplayID: UInt? ) throws -> [XCUIElementQuery] { - try elementSearchRoots(root: root, litDisplayID: litDisplayID).map { $0.descendants(matching: type) } + queries(matching: types, in: try elementSearchRoots(root: root, litDisplayID: litDisplayID)) + } + + private func queries( + matching types: [XCUIElement.ElementType], + in roots: [XCUIElement] + ) -> [XCUIElementQuery] { + types.flatMap { type in roots.map { $0.descendants(matching: type) } } } } diff --git a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextInputProbe.swift b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextInputProbe.swift index 2106ac54c3..adba1bd27c 100644 --- a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextInputProbe.swift +++ b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextInputProbe.swift @@ -179,8 +179,7 @@ extension RunnerTests { /// Resolved before the probe's issue containment, so a lit-panel refusal is not read as no input. private func textInputQueries(app: XCUIApplication) throws -> [XCUIElementQuery] { - let inputTypes: [XCUIElement.ElementType] = [.textField, .secureTextField, .searchField, .textView] - return try inputTypes.flatMap { try elementQueries(app: app, matching: $0) } + try elementQueries(app: app, matching: .textField, .secureTextField, .searchField, .textView) } private func queryTextInputs( diff --git a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerRequestPanelTests.swift b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerRequestPanelTests.swift index 045b2a2c42..081dcb01ff 100644 --- a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerRequestPanelTests.swift +++ b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerRequestPanelTests.swift @@ -575,6 +575,16 @@ extension RunnerTests { try assertions(response) } + /// Runs `body` with the app's window probed as the only window on lit display 3 at `frame`, + /// handing it the count of window-list reads. + @MainActor + func withLitPanelWindow(frame: CGRect, _ body: (_ windowListReads: () -> Int) throws -> Void) throws { + try withLitPanelBookingProbe { _ in + LitPanelBookingProbe.windowFrame = frame + try body { LitPanelBookingProbe.windowListReads } + } + } + @MainActor private func withLitPanelBookingProbe(composed: Bool = false, _ body: (XCUIElement) throws -> Void) throws { app.launch() diff --git a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+KeyboardTests.swift b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+KeyboardTests.swift index e22758ea7a..06c3aacf0a 100644 --- a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+KeyboardTests.swift +++ b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+KeyboardTests.swift @@ -4,6 +4,20 @@ import AgentDeviceSnapshotPresentation #if AGENT_DEVICE_RUNNER_UNIT_TESTS extension RunnerTests { #if os(iOS) + /// Keyboard reads stay application-wide on a lit-panel request, so a failed lit-window walk + /// cannot turn `keyboard dismiss` into a refusal. + @MainActor + func testKeyboardDismissIgnoresAFailedLitPanelWindowWalk() throws { + try withLitPanelWindow(frame: CGRect(x: 0, y: 0, width: 669, height: 951)) { _ in + litPanelWindowWalkIssueForTesting = true + defer { litPanelWindowWalkIssueForTesting = false } + let command = try runnerCommandFixture(#"{"command":"keyboardDismiss","litPanel":{"displayId":3,"widthPt":669,"heightPt":951}}"#) + setRequestLitPanel(from: command) + let response = try executeOnMainPrepared(command: command, activeApp: app) + XCTAssertTrue(response.ok, String(describing: response.error)) + } + } + @MainActor func testCapturedKeyboardBandIncludesTheInputAssistantOutsideTheKeyboard() { let keyboard = ElementAttributesFixture(.keyboard, CGRect(x: 0, y: 600, width: 390, height: 244)) diff --git a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+LifecycleTests.swift b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+LifecycleTests.swift index ede54c72ac..5d076bb49a 100644 --- a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+LifecycleTests.swift +++ b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+LifecycleTests.swift @@ -12,6 +12,18 @@ private struct AlertCommandTraitsFixture: Decodable { } extension RunnerTests { +#if os(iOS) + /// A frame sample resolves the lit panel without booking it, so it never replaces the request's booking. + @MainActor + func testFrameSamplingResolvesTheLitPanelWithoutBookingIt() throws { + try withLitPanelWindow(frame: CGRect(x: 0, y: 0, width: 669, height: 951)) { _ in + setRequestLitPanel(from: try runnerCommandFixture(#"{"command":"snapshot","litPanel":{"displayId":3,"widthPt":669,"heightPt":951}}"#)) + _ = captureRunnerFrame(app: app) + XCTAssertNil(litPanelState.surface) + } + } +#endif + func testAlertRetryFactMatchesTheSharedGoldenTable() throws { let fixtureURL = URL(fileURLWithPath: #filePath) .deletingLastPathComponent() diff --git a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+TextInputProbeTests.swift b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+TextInputProbeTests.swift index 42f2502ea0..3b88b50588 100644 --- a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+TextInputProbeTests.swift +++ b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/UnitTests/RunnerTests+TextInputProbeTests.swift @@ -2,6 +2,30 @@ import XCTest extension RunnerTests { #if AGENT_DEVICE_RUNNER_UNIT_TESTS && os(iOS) + /// One text-input lookup walks the lit windows once, however many input types it queries. + @MainActor + func testTextInputProbeWalksTheLitWindowsOnce() throws { + try withLitPanelWindow(frame: CGRect(x: 0, y: 0, width: 669, height: 951)) { windowListReads in + setRequestLitPanel(from: try runnerCommandFixture( + #"{"command":"type","litPanel":{"displayId":3,"widthPt":669,"heightPt":951}}"# + )) + _ = try probeTextInputs(app: app, point: CGPoint(x: 10, y: 10), contentTranslation: .none) + XCTAssertEqual(windowListReads(), 1) + } + } + + /// A content-vector read on a lit-panel request that nothing booked yet books the request's + /// window and answers its origin, instead of refusing for want of an earlier booking. + @MainActor + func testUnbookedLitPanelContentTranslationBooksTheRequestWindow() throws { + try withLitPanelWindow(frame: CGRect(x: 0, y: 80, width: 669, height: 871)) { _ in + setRequestLitPanel(from: try runnerCommandFixture(#"{"command":"type","litPanel":{"displayId":3,"widthPt":669,"heightPt":951}}"#)) + guard case .known(let vector) = liveContentTranslation(app: app) else { return XCTFail("no vector") } + XCTAssertEqual(vector, CGSize(width: 0, height: 80)) + XCTAssertNotNil(litPanelState.surface) + } + } + func testTextInputProbePreservesEnclosingRunnerWait() { app.launchArguments = ["--agent-device-text-entry-regression"] app.launch()