Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions AxePlaygroundApp/AxePlayground/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ struct ContentView: View {
SwitchTestView()
case "tab-view-test":
TabViewTestView()
case "slider-value-test":
SliderValueTestView()
case "searchable-test":
SearchableTestView()
case "toolbar-picker-test":
ToolbarPickerTestView()

// Input & Text
case "text-input":
Expand Down Expand Up @@ -106,6 +112,11 @@ struct MainMenuView: View {
("Batch", [
("batch-test", "Batch Test", "State changes + delayed element appearance"),
("batch-login-flow", "Batch Login Flow", "Multi-step login + loading + post-login action")
]),
("Accessibility", [
("slider-value-test", "Slider Value Test", "Numeric AXValue with selector tap"),
("searchable-test", "Searchable Test", "Navigation search field targeting"),
("toolbar-picker-test", "Toolbar Picker Test", "Toolbar segmented picker targeting")
])
]

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import SwiftUI

struct SliderValueTestView: View {
@State private var sliderValue = 0.25
@State private var state = "Initial"

var body: some View {
VStack(spacing: 24) {
Text("Slider Value State: \(state)")
.accessibilityIdentifier("slider-value-state")
.accessibilityValue(state)

Button("Slider Value Button") {
state = "Tapped"
}
.buttonStyle(.borderedProminent)
.accessibilityIdentifier("slider-value-button")

Text("Slider Position: \(sliderValue.formatted(.number.precision(.fractionLength(2))))")
.accessibilityIdentifier("slider-position-value")
.accessibilityValue(sliderValue.formatted(.number.precision(.fractionLength(2))))

Slider(value: $sliderValue, in: 0...1)
.accessibilityIdentifier("slider-value-slider")
.accessibilityLabel("Slider Value Slider")
}
.padding()
.navigationTitle("Slider Value")
.navigationBarTitleDisplayMode(.inline)
}
}

struct SearchableTestView: View {
@State private var query = ""

private var rows: [String] {
let allRows = ["Alpha Row", "Beta Row"]
guard !query.isEmpty else { return allRows }
return allRows.filter { $0.localizedCaseInsensitiveContains(query) }
}

var body: some View {
List {
Text("Search Query: \(query.isEmpty ? "empty" : query)")
.accessibilityIdentifier("searchable-test-query")
.accessibilityValue(query.isEmpty ? "empty" : query)

ForEach(rows, id: \.self) { row in
Text(row)
.accessibilityIdentifier("searchable-test-\(row.replacingOccurrences(of: " ", with: "-").lowercased())")
}
}
.navigationTitle("Searchable Test")
.navigationBarTitleDisplayMode(.large)
.searchable(
text: $query,
placement: .navigationBarDrawer(displayMode: .always),
prompt: "Search Books"
)
}
}

struct ToolbarPickerTestView: View {
private enum Filter: String, CaseIterable, Identifiable {
case all = "All"
case unread = "Unread"
case read = "Read"

var id: String { rawValue }
}

@State private var filter: Filter = .all

var body: some View {
List {
Text("Toolbar Picker State: \(filter.rawValue)")
.accessibilityIdentifier("toolbar-picker-test-state")
.accessibilityValue(filter.rawValue)

Text("Toolbar Picker Detail Body")
.accessibilityIdentifier("toolbar-picker-test-body")
}
.navigationTitle("Toolbar Picker")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Picker("Filter", selection: $filter) {
ForEach(Filter.allCases) { filter in
Text(filter.rawValue).tag(filter)
}
}
.pickerStyle(.segmented)
.accessibilityIdentifier("toolbar-picker-test-filter")
}
}
}
}
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- Fixed `describe-ui` and selector-based `tap --label` exposing and activating real SwiftUI `TabView` tab items from the CoreSimulator accessibility bridge.
- Fixed `describe-ui` and selector-based `tap --label` exposing and activating real SwiftUI `TabView` tab items, navigation search fields, toolbar segmented picker items, and generated navigation back buttons from the CoreSimulator accessibility bridge. Also fixed selector decoding when the accessibility tree contains numeric `AXValue` fields such as sliders.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add issue links to match internal changelog entry format.

This new internal fix entry should include issue references in the required format.

Suggested update
-- Fixed `describe-ui` and selector-based `tap --label` exposing and activating real SwiftUI `TabView` tab items, navigation search fields, toolbar segmented picker items, and generated navigation back buttons from the CoreSimulator accessibility bridge. Also fixed selector decoding when the accessibility tree contains numeric `AXValue` fields such as sliders.
+- Fixed `describe-ui` and selector-based `tap --label` exposing and activating real SwiftUI `TabView` tab items, navigation search fields, toolbar segmented picker items, and generated navigation back buttons from the CoreSimulator accessibility bridge. Also fixed selector decoding when the accessibility tree contains numeric `AXValue` fields such as sliders ([`#8`](https://github.com/cameroncooke/AXe/issues/8), [`#43`](https://github.com/cameroncooke/AXe/issues/43), [`#45`](https://github.com/cameroncooke/AXe/issues/45)).

As per coding guidelines, For internal changes from issues, use format Fixed foo bar ([#123](https://github.com/cameroncooke/AXe/issues/123)).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- Fixed `describe-ui` and selector-based `tap --label` exposing and activating real SwiftUI `TabView` tab items, navigation search fields, toolbar segmented picker items, and generated navigation back buttons from the CoreSimulator accessibility bridge. Also fixed selector decoding when the accessibility tree contains numeric `AXValue` fields such as sliders.
- Fixed `describe-ui` and selector-based `tap --label` exposing and activating real SwiftUI `TabView` tab items, navigation search fields, toolbar segmented picker items, and generated navigation back buttons from the CoreSimulator accessibility bridge. Also fixed selector decoding when the accessibility tree contains numeric `AXValue` fields such as sliders ([`#8`](https://github.com/cameroncooke/AXe/issues/8), [`#43`](https://github.com/cameroncooke/AXe/issues/43), [`#45`](https://github.com/cameroncooke/AXe/issues/45)).
🧰 Tools
🪛 LanguageTool

[uncategorized] ~12-~12: Possible missing comma found.
Context: ...the CoreSimulator accessibility bridge. Also fixed selector decoding when the access...

(AI_HYDRA_LEO_MISSING_COMMA)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` at line 12, Update the changelog entry to include issue
references in the internal format: append the issue link(s) after the
description using "([`#123`](https://github.com/cameroncooke/AXe/issues/123))"
style; for example modify the line mentioning describe-ui, selector-based tap
--label, CoreSimulator accessibility bridge, SwiftUI TabView, and AXValue
numeric fields so it ends with the appropriate issue reference(s) in that exact
format (referencing the specific issue numbers for this fix).

- Fixed `tap`, `touch`, `swipe`, and matching batch steps dispatching logical UI coordinates directly to FBSimulatorHIDEvent without landscape rotation or letterbox correction, causing interactions to land in the wrong location in rotated landscape simulators and portrait-hardware landscape-only apps. AXe now detects the simulator UI orientation automatically instead of requiring callers to pass landscape flags ([#5](https://github.com/cameroncooke/AXe/issues/5) by [@Nitewriter](https://github.com/Nitewriter))
- Fixed selector-based `tap` and batch tap steps so UIKit `UISwitch` and SwiftUI `Toggle` controls can be activated reliably, including when a matched row or label contains a single switch/toggle control. Added `--tap-style` so switch/toggle taps can use physical touch automatically while normal taps keep the simulator `tapAt` path by default ([#46](https://github.com/cameroncooke/AXe/pull/46)).
- Fixed element comparison in `AccessibilityTargetResolver` to prevent distinct elements with the same type and frame but lacking labels/values from being incorrectly identified as identical during ancestor tree traversal.
Expand Down
28 changes: 28 additions & 0 deletions Tests/DescribeUITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,34 @@ struct DescribeUITests {
#expect(settingsTab?.frame != nil)
}

@Test("Describe-ui exposes navigation searchable field")
func describeUIExposesNavigationSearchableField() async throws {
try await TestHelpers.launchPlaygroundApp(to: "searchable-test")

let uiState = try await TestHelpers.getUIState()
let searchField = UIStateParser.findElement(in: uiState) { element in
element.type == "TextField" && element.value == "Search Books"
}

#expect(searchField?.frame != nil)
}

@Test("Describe-ui exposes toolbar segmented picker and navigation back button")
func describeUIExposesToolbarPickerAndBackButton() async throws {
try await TestHelpers.launchPlaygroundApp(to: "toolbar-picker-test")

let uiState = try await TestHelpers.getUIState()
let backButton = UIStateParser.findElement(in: uiState, withIdentifier: "BackButton")
let allOption = UIStateParser.findElementByLabel(in: uiState, label: "All")
let unreadOption = UIStateParser.findElementByLabel(in: uiState, label: "Unread")
let readOption = UIStateParser.findElementByLabel(in: uiState, label: "Read")

#expect(backButton?.type == "Button")
#expect(allOption?.type == "RadioButton")
#expect(unreadOption?.type == "RadioButton")
#expect(readOption?.type == "RadioButton")
}

@Test("Describe-ui --point returns the targeted element")
func describeUIAtPoint() async throws {
let simulatorUDID = try TestHelpers.requireSimulatorUDID()
Expand Down
58 changes: 58 additions & 0 deletions Tests/TapTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,64 @@ struct TapTests {
#expect(selectedState == "Current Tab: Settings")
}

@Test("Selector tap works when the accessibility tree contains numeric AXValue")
func selectorTapWorksWhenAccessibilityTreeContainsNumericAXValue() async throws {
try await TestHelpers.launchPlaygroundApp(to: "slider-value-test")

let initialState = try await TestHelpers.waitForLabel(containing: "Slider Value State:", timeout: 3) {
$0 == "Slider Value State: Initial"
}
#expect(initialState == "Slider Value State: Initial")

let uiState = try await TestHelpers.getUIState()
let positionText = UIStateParser.findElementByLabel(in: uiState, label: "Slider Position: 0.25")
let slider = UIStateParser.findElement(in: uiState) { element in
element.type == "Slider" && element.value == "0.25"
}
#expect(positionText?.value == "0.25")
#expect(slider != nil)

try await TestHelpers.runAxeCommand("tap --id slider-value-button", simulatorUDID: defaultSimulatorUDID)

let tappedState = try await TestHelpers.waitForLabel(containing: "Slider Value State:", timeout: 3) {
$0 == "Slider Value State: Tapped"
}
#expect(tappedState == "Slider Value State: Tapped")
}

@Test("Selector tap switches toolbar segmented picker")
func selectorTapSwitchesToolbarSegmentedPicker() async throws {
try await TestHelpers.launchPlaygroundApp(to: "toolbar-picker-test")

let initialState = try await TestHelpers.waitForLabel(containing: "Toolbar Picker State:", timeout: 3) {
$0 == "Toolbar Picker State: All"
}
#expect(initialState == "Toolbar Picker State: All")

try await TestHelpers.runAxeCommand("tap --label Unread --element-type RadioButton", simulatorUDID: defaultSimulatorUDID)

let selectedState = try await TestHelpers.waitForLabel(containing: "Toolbar Picker State:", timeout: 3) {
$0 == "Toolbar Picker State: Unread"
}
#expect(selectedState == "Toolbar Picker State: Unread")
}

@Test("Selector tap activates generated navigation back button")
func selectorTapActivatesGeneratedNavigationBackButton() async throws {
try await TestHelpers.launchPlaygroundApp(to: "toolbar-picker-test")

let uiState = try await TestHelpers.getUIState()
let backButton = UIStateParser.findElement(in: uiState, withIdentifier: "BackButton")
#expect(backButton?.type == "Button")

try await TestHelpers.runAxeCommand("tap --id BackButton", simulatorUDID: defaultSimulatorUDID)

let menuState = try await TestHelpers.waitForLabel(containing: "Touch & Gestures", timeout: 3) {
$0 == "Touch & Gestures"
}
#expect(menuState == "Touch & Gestures")
}

@Test("Selector tap toggles SwiftUI Toggle")
func selectorTapTogglesSwiftUIToggle() async throws {
try await TestHelpers.launchPlaygroundApp(to: "switch-test")
Expand Down
31 changes: 31 additions & 0 deletions Tests/TypeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,37 @@ struct TypeTests {
#expect(textFieldElement?.value == textToType, "Text should still be typed with manual delays")
}

@Test("Typing into navigation searchable field filters visible state")
func typingIntoNavigationSearchableFieldFiltersVisibleState() async throws {
try await TestHelpers.launchPlaygroundApp(to: "searchable-test")

let initialState = try await TestHelpers.waitForLabel(containing: "Search Query:", timeout: 3) {
$0 == "Search Query: empty"
}
#expect(initialState == "Search Query: empty")

let uiState = try await TestHelpers.getUIState()
let searchField = UIStateParser.findElement(in: uiState) { element in
element.type == "TextField" && element.value == "Search Books"
}
#expect(searchField?.frame != nil)

try await TestHelpers.runAxeCommand("tap --value 'Search Books' --element-type TextField", simulatorUDID: defaultSimulatorUDID)
try await Task.sleep(nanoseconds: 400_000_000)
try await TestHelpers.runAxeCommand("type Alpha", simulatorUDID: defaultSimulatorUDID)

let filteredState = try await TestHelpers.waitForLabel(containing: "Search Query:", timeout: 3) {
$0 == "Search Query: Alpha"
}
#expect(filteredState == "Search Query: Alpha")

let filteredUIState = try await TestHelpers.getUIState()
let alphaRow = UIStateParser.findElementByLabel(in: filteredUIState, label: "Alpha Row")
let betaRow = UIStateParser.findElementByLabel(in: filteredUIState, label: "Beta Row")
#expect(alphaRow != nil)
#expect(betaRow == nil)
}

@Test("Unsupported characters throw error")
func unsupportedCharactersError() async throws {
// Arrange
Expand Down
Loading