From 38fb7bf4d87739a876a787c10612eb6694636363 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Thu, 8 Oct 2026 13:54:24 -0400 Subject: [PATCH 1/2] fix(macos): give snapshot nodes with a non-finite AX rect no rect --- .../SnapshotTraversal.swift | 16 +++++++++++++- .../RectResponseTests.swift | 22 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 apple/macos-helper/Tests/AgentDeviceMacOSHelperTests/RectResponseTests.swift diff --git a/apple/macos-helper/Sources/AgentDeviceMacOSHelper/SnapshotTraversal.swift b/apple/macos-helper/Sources/AgentDeviceMacOSHelper/SnapshotTraversal.swift index 1121eaa7f3..c30c60b2f9 100644 --- a/apple/macos-helper/Sources/AgentDeviceMacOSHelper/SnapshotTraversal.swift +++ b/apple/macos-helper/Sources/AgentDeviceMacOSHelper/SnapshotTraversal.swift @@ -19,6 +19,20 @@ struct RectResponse: Encodable { let y: Double let width: Double let height: Double + + init(x: Double, y: Double, width: Double, height: Double) { + self.x = x + self.y = y + self.width = width + self.height = height + } + + /// JSONEncoder throws on inf/NaN, which fails the whole snapshot. SwiftUI reports lazy + /// containers scrolled out of view as an AXOpaqueProviderGroup at (inf, inf, 0, 0). + init?(finiteX x: Double, y: Double, width: Double, height: Double) { + guard x.isFinite, y.isFinite, width.isFinite, height.isFinite else { return nil } + self.init(x: x, y: y, width: width, height: height) + } } struct SnapshotNodeResponse: Encodable { @@ -796,7 +810,7 @@ func rectAttribute(_ element: AXUIElement) -> RectResponse? { } return RectResponse( - x: Double(position.x), + finiteX: Double(position.x), y: Double(position.y), width: Double(size.width), height: Double(size.height) diff --git a/apple/macos-helper/Tests/AgentDeviceMacOSHelperTests/RectResponseTests.swift b/apple/macos-helper/Tests/AgentDeviceMacOSHelperTests/RectResponseTests.swift new file mode 100644 index 0000000000..01dcc82488 --- /dev/null +++ b/apple/macos-helper/Tests/AgentDeviceMacOSHelperTests/RectResponseTests.swift @@ -0,0 +1,22 @@ +import XCTest + +@testable import AgentDeviceMacOSHelper + +final class RectResponseTests: XCTestCase { + func testFiniteRectKeepsItsValues() throws { + let rect = try XCTUnwrap(RectResponse(finiteX: 1, y: 2, width: 3, height: 4)) + XCTAssertEqual([rect.x, rect.y, rect.width, rect.height], [1, 2, 3, 4]) + } + + func testNonFiniteComponentYieldsNoRect() { + XCTAssertNil(RectResponse(finiteX: .infinity, y: .infinity, width: 0, height: 0)) + XCTAssertNil(RectResponse(finiteX: 0, y: -.infinity, width: 0, height: 0)) + XCTAssertNil(RectResponse(finiteX: 0, y: 0, width: .nan, height: 0)) + XCTAssertNil(RectResponse(finiteX: 0, y: 0, width: 0, height: .infinity)) + } + + func testUnguardedInfiniteRectFailsEncoding() { + XCTAssertThrowsError( + try JSONEncoder().encode(RectResponse(x: .infinity, y: 0, width: 0, height: 0))) + } +} From 6ddff2579ab7729abc343b0c2c1399aad166d1ed Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Thu, 8 Oct 2026 21:01:40 -0400 Subject: [PATCH 2/2] refactor(macos): test the non-finite AX rect guard through a pure function --- .../SnapshotTraversal.swift | 32 +++++++------------ .../RectResponseTests.swift | 19 ++++++----- 2 files changed, 21 insertions(+), 30 deletions(-) diff --git a/apple/macos-helper/Sources/AgentDeviceMacOSHelper/SnapshotTraversal.swift b/apple/macos-helper/Sources/AgentDeviceMacOSHelper/SnapshotTraversal.swift index c30c60b2f9..0149e017db 100644 --- a/apple/macos-helper/Sources/AgentDeviceMacOSHelper/SnapshotTraversal.swift +++ b/apple/macos-helper/Sources/AgentDeviceMacOSHelper/SnapshotTraversal.swift @@ -19,20 +19,6 @@ struct RectResponse: Encodable { let y: Double let width: Double let height: Double - - init(x: Double, y: Double, width: Double, height: Double) { - self.x = x - self.y = y - self.width = width - self.height = height - } - - /// JSONEncoder throws on inf/NaN, which fails the whole snapshot. SwiftUI reports lazy - /// containers scrolled out of view as an AXOpaqueProviderGroup at (inf, inf, 0, 0). - init?(finiteX x: Double, y: Double, width: Double, height: Double) { - guard x.isFinite, y.isFinite, width.isFinite, height.isFinite else { return nil } - self.init(x: x, y: y, width: width, height: height) - } } struct SnapshotNodeResponse: Encodable { @@ -809,12 +795,18 @@ func rectAttribute(_ element: AXUIElement) -> RectResponse? { return nil } - return RectResponse( - finiteX: Double(position.x), - y: Double(position.y), - width: Double(size.width), - height: Double(size.height) - ) + return finiteRectResponse(position: position, size: size) +} + +/// JSONEncoder throws on inf/NaN, which fails the whole snapshot. SwiftUI reports lazy +/// containers scrolled out of view as an AXOpaqueProviderGroup at (inf, inf, 0, 0). +func finiteRectResponse(position: CGPoint, size: CGSize) -> RectResponse? { + let x = Double(position.x) + let y = Double(position.y) + let width = Double(size.width) + let height = Double(size.height) + guard x.isFinite, y.isFinite, width.isFinite, height.isFinite else { return nil } + return RectResponse(x: x, y: y, width: width, height: height) } private func accessibilityAxValue(_ value: CFTypeRef?) -> AXValue? { diff --git a/apple/macos-helper/Tests/AgentDeviceMacOSHelperTests/RectResponseTests.swift b/apple/macos-helper/Tests/AgentDeviceMacOSHelperTests/RectResponseTests.swift index 01dcc82488..d93d9d05fc 100644 --- a/apple/macos-helper/Tests/AgentDeviceMacOSHelperTests/RectResponseTests.swift +++ b/apple/macos-helper/Tests/AgentDeviceMacOSHelperTests/RectResponseTests.swift @@ -4,19 +4,18 @@ import XCTest final class RectResponseTests: XCTestCase { func testFiniteRectKeepsItsValues() throws { - let rect = try XCTUnwrap(RectResponse(finiteX: 1, y: 2, width: 3, height: 4)) + let rect = try XCTUnwrap( + finiteRectResponse(position: CGPoint(x: 1, y: 2), size: CGSize(width: 3, height: 4))) XCTAssertEqual([rect.x, rect.y, rect.width, rect.height], [1, 2, 3, 4]) } func testNonFiniteComponentYieldsNoRect() { - XCTAssertNil(RectResponse(finiteX: .infinity, y: .infinity, width: 0, height: 0)) - XCTAssertNil(RectResponse(finiteX: 0, y: -.infinity, width: 0, height: 0)) - XCTAssertNil(RectResponse(finiteX: 0, y: 0, width: .nan, height: 0)) - XCTAssertNil(RectResponse(finiteX: 0, y: 0, width: 0, height: .infinity)) - } - - func testUnguardedInfiniteRectFailsEncoding() { - XCTAssertThrowsError( - try JSONEncoder().encode(RectResponse(x: .infinity, y: 0, width: 0, height: 0))) + let nonFinite: [CGFloat] = [.infinity, -.infinity, .nan] + for value in nonFinite { + XCTAssertNil(finiteRectResponse(position: CGPoint(x: value, y: 0), size: .zero)) + XCTAssertNil(finiteRectResponse(position: CGPoint(x: 0, y: value), size: .zero)) + XCTAssertNil(finiteRectResponse(position: .zero, size: CGSize(width: value, height: 0))) + XCTAssertNil(finiteRectResponse(position: .zero, size: CGSize(width: 0, height: value))) + } } }