Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a5d4dc1
feat(simulator): Support Xcode 27 Device Hub automation
cameroncooke Jul 14, 2026
d4d464b
test(compatibility): Capture Xcode 26 and 27 CLI contracts
cameroncooke Jul 14, 2026
4a81f70
fix(e2e): Separate build and runtime Xcode toolchains
cameroncooke Jul 14, 2026
e84163c
build(ci): Key release caches by Xcode and pipeline
cameroncooke Jul 14, 2026
8eb6222
build(release): Add local release orchestrator
cameroncooke Jul 14, 2026
571d67b
docs: Document Xcode 27 compatibility
cameroncooke Jul 14, 2026
be68c03
build(idb): Consume pinned fork revision
cameroncooke Jul 15, 2026
5f9322d
fix(release): Harden cached artifact provenance
cameroncooke Jul 15, 2026
0afbb8d
fix(ci): Reject dirty IDB cache checkouts
cameroncooke Jul 15, 2026
4032120
fix(build): Support selected Xcode toolchains
cameroncooke Jul 15, 2026
6805bb7
docs: Clarify Xcode compatibility contract
cameroncooke Jul 15, 2026
a13e4a6
fix(release): Sanitize AppleDouble metadata from release payloads
cameroncooke Jul 15, 2026
725d181
fix(build): Disable git pager during IDB checkout verification
cameroncooke Jul 15, 2026
7b6567c
fix(build): Repair E2E setup and managed IDB caches
cameroncooke Jul 15, 2026
dd23fd1
fix(hid): Prevent ambiguous touch request replay
cameroncooke Jul 15, 2026
0625325
docs(hid): Record Xcode 27 drag delivery limitation
cameroncooke Jul 15, 2026
9479fba
docs(hid): Attribute silent touch delivery to Simulator Resize Mode
cameroncooke Jul 16, 2026
2ca60d2
fix(build): Recover managed custom IDB checkouts
cameroncooke Jul 16, 2026
09d67df
fix(hid): Recover stale brokers before touch dispatch
cameroncooke Jul 16, 2026
e5a4ad1
docs(changelog): Record simulator recovery fixes
cameroncooke Jul 16, 2026
98a0a1d
fix(cli): Render actionable runtime errors
cameroncooke Jul 16, 2026
da03d0f
fix(hid): Harden broker startup and liveness recovery
cameroncooke Jul 16, 2026
2a4af61
fix(accessibility): Make recovery cancellation-safe
cameroncooke Jul 16, 2026
28a22a8
test(ci): Run simulator-free Swift tests
cameroncooke Jul 16, 2026
355b2e2
fix(build): Clean abandoned IDB replacement directories
cameroncooke Jul 16, 2026
6fe80d0
docs(compatibility): Clarify reviewed runtime contracts
cameroncooke Jul 16, 2026
47f5db0
fix(accessibility): Retry transient point fallbacks
cameroncooke Jul 16, 2026
3c51acc
test(e2e): Verify playground fixture readiness
cameroncooke Jul 16, 2026
88da6ff
docs(changelog): Record Xcode 27 recovery fixes
cameroncooke Jul 16, 2026
7fb9949
fix(hid): Curate broker failure messages
cameroncooke Jul 16, 2026
31599ac
docs(hid): Clarify non-replay guarantees
cameroncooke Jul 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Tests/Goldens/** whitespace=-blank-at-eol,-blank-at-eof
59 changes: 44 additions & 15 deletions .github/workflows/release-shared.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,19 @@ jobs:
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '26.2'
xcode-version: '26.5'

- name: Resolve Xcode cache identity
id: xcode_identity
run: |
set -euo pipefail
XCODE_VERSION=$(xcodebuild -version | awk 'NR == 1 { print $2 }')
XCODE_BUILD=$(xcodebuild -version | awk 'NR == 2 { print $3 }')
CACHE_IDENTITY=$(printf 'xcode-%s-%s' "$XCODE_VERSION" "$XCODE_BUILD" | tr -c 'A-Za-z0-9._-' '-')
echo "cache_identity=$CACHE_IDENTITY" >> "$GITHUB_OUTPUT"

- name: Install dependencies
run: brew install jq
run: brew install jq xcodegen

- name: Make release scripts executable
run: |
Expand All @@ -56,27 +65,38 @@ jobs:
id: idb_inputs
run: |
set -euo pipefail
PINNED_IDB_URL=$(grep '^IDB_GIT_URL=' scripts/build.sh | sed -E 's/^IDB_GIT_URL="\$\{IDB_GIT_URL:-([^}]*)\}"$/\1/')
PINNED_IDB_COMMIT=$(grep '^IDB_GIT_REF=' scripts/build.sh | sed -E 's/^IDB_GIT_REF="\$\{IDB_GIT_REF:-([^}]*)\}"$/\1/')
PATCH_HASH=$(find patches/idb -name '*.patch' -type f | sort | xargs shasum -a 256 | shasum -a 256 | awk '{print $1}')
PINNED_IDB_UPSTREAM_BASE=$(grep '^IDB_UPSTREAM_BASE_REF=' scripts/build.sh | sed -E 's/^IDB_UPSTREAM_BASE_REF="\$\{IDB_UPSTREAM_BASE_REF:-([^}]*)\}"$/\1/')
SOURCE_IDENTITY=$(printf '%s\n%s\n%s\n' "$PINNED_IDB_URL" "$PINNED_IDB_COMMIT" "$PINNED_IDB_UPSTREAM_BASE" | shasum -a 256 | awk '{print $1}')
BUILD_PIPELINE_HASH=$(printf '%s\n%s\n' "$(shasum -a 256 scripts/build.sh Package.swift)" "$(xcodegen --version)" | shasum -a 256 | awk '{print $1}')
echo "idb_url=$PINNED_IDB_URL" >> "$GITHUB_OUTPUT"
echo "idb_commit=$PINNED_IDB_COMMIT" >> "$GITHUB_OUTPUT"
echo "patch_hash=$PATCH_HASH" >> "$GITHUB_OUTPUT"
echo "idb_upstream_base=$PINNED_IDB_UPSTREAM_BASE" >> "$GITHUB_OUTPUT"
echo "source_identity=$SOURCE_IDENTITY" >> "$GITHUB_OUTPUT"
echo "build_pipeline_hash=$BUILD_PIPELINE_HASH" >> "$GITHUB_OUTPUT"

- name: Restore IDB repository cache
uses: actions/cache/restore@v4
with:
path: idb_checkout
key: idb-repo-${{ runner.os }}-${{ steps.idb_inputs.outputs.idb_commit }}-${{ steps.idb_inputs.outputs.patch_hash }}
key: idb-repo-${{ runner.os }}-${{ steps.idb_inputs.outputs.source_identity }}

- name: Check IDB repository freshness
id: idb_check
run: |
set -euo pipefail
PINNED_IDB_URL="${{ steps.idb_inputs.outputs.idb_url }}"
PINNED_IDB_COMMIT="${{ steps.idb_inputs.outputs.idb_commit }}"
echo "idb_commit=$PINNED_IDB_COMMIT" >> "$GITHUB_OUTPUT"
PINNED_IDB_UPSTREAM_BASE="${{ steps.idb_inputs.outputs.idb_upstream_base }}"

if [ -d "idb_checkout/.git" ]; then
LOCAL_COMMIT=$(git -C idb_checkout rev-parse HEAD)
if [ "$LOCAL_COMMIT" = "$PINNED_IDB_COMMIT" ]; then
LOCAL_REMOTE=$(git -C idb_checkout remote get-url origin)
if [ "$LOCAL_COMMIT" = "$PINNED_IDB_COMMIT" ] \
&& [ "$LOCAL_REMOTE" = "$PINNED_IDB_URL" ] \
&& git -C idb_checkout merge-base --is-ancestor "$PINNED_IDB_UPSTREAM_BASE" "$LOCAL_COMMIT" \
&& [ -z "$(git -C idb_checkout status --porcelain)" ]; then
echo "needs_setup=false" >> "$GITHUB_OUTPUT"
exit 0
fi
Expand All @@ -88,19 +108,23 @@ jobs:
uses: actions/cache/restore@v4
with:
path: build_products/Frameworks
key: frameworks-${{ runner.os }}-${{ steps.idb_check.outputs.idb_commit }}-${{ steps.idb_inputs.outputs.patch_hash }}
key: frameworks-${{ runner.os }}-${{ steps.xcode_identity.outputs.cache_identity }}-${{ steps.idb_inputs.outputs.source_identity }}-${{ steps.idb_inputs.outputs.build_pipeline_hash }}

- name: Restore XCFramework cache
uses: actions/cache/restore@v4
with:
path: build_products/XCFrameworks
key: xcframeworks-${{ runner.os }}-${{ steps.idb_check.outputs.idb_commit }}-${{ steps.idb_inputs.outputs.patch_hash }}
key: xcframeworks-${{ runner.os }}-${{ steps.xcode_identity.outputs.cache_identity }}-${{ steps.idb_inputs.outputs.source_identity }}-${{ steps.idb_inputs.outputs.build_pipeline_hash }}

- name: Check XCFramework outputs
id: xcframework_check
run: |
set -euo pipefail
REQUIRED=(
"build_products/Frameworks/FBControlCore.framework"
"build_products/Frameworks/XCTestBootstrap.framework"
"build_products/Frameworks/FBSimulatorControl.framework"
"build_products/Frameworks/FBDeviceControl.framework"
"build_products/XCFrameworks/FBControlCore.xcframework"
"build_products/XCFrameworks/XCTestBootstrap.xcframework"
"build_products/XCFrameworks/FBSimulatorControl.xcframework"
Expand All @@ -114,7 +138,12 @@ jobs:
fi
done

echo "needs_build=false" >> "$GITHUB_OUTPUT"
if scripts/build.sh verify-xcframeworks >/dev/null 2>&1; then
echo "needs_build=false" >> "$GITHUB_OUTPUT"
else
echo "Cached XCFramework verification failed; rebuilding framework artifacts"
echo "needs_build=true" >> "$GITHUB_OUTPUT"
fi

- name: Setup - Clone IDB repository
if: steps.idb_check.outputs.needs_setup == 'true'
Expand Down Expand Up @@ -192,37 +221,37 @@ jobs:
echo "$NOTARIZE_APP_STORE_CONNECT_API_KEY" | base64 --decode > keys/AuthKey_${{ secrets.NOTARIZE_APP_STORE_CONNECT_API_KEY_ID }}.p8

- name: Sign - Framework binaries
if: steps.signing.outputs.has_certificate == 'true' && (steps.idb_check.outputs.needs_setup == 'true' || steps.xcframework_check.outputs.needs_build == 'true')
if: steps.signing.outputs.has_certificate == 'true'
run: scripts/build.sh sign-frameworks

- name: Create - XCFrameworks
if: steps.idb_check.outputs.needs_setup == 'true' || steps.xcframework_check.outputs.needs_build == 'true'
run: scripts/build.sh xcframeworks

- name: Sign - XCFramework bundles
if: steps.signing.outputs.has_certificate == 'true' && (steps.idb_check.outputs.needs_setup == 'true' || steps.xcframework_check.outputs.needs_build == 'true')
if: steps.signing.outputs.has_certificate == 'true'
run: scripts/build.sh sign-xcframeworks

- name: Save IDB repository cache
if: steps.idb_check.outputs.needs_setup == 'true'
uses: actions/cache/save@v4
with:
path: idb_checkout
key: idb-repo-${{ runner.os }}-${{ steps.idb_check.outputs.idb_commit }}-${{ steps.idb_inputs.outputs.patch_hash }}
key: idb-repo-${{ runner.os }}-${{ steps.idb_inputs.outputs.source_identity }}

- name: Save runtime Frameworks cache
if: steps.idb_check.outputs.needs_setup == 'true' || steps.xcframework_check.outputs.needs_build == 'true'
uses: actions/cache/save@v4
with:
path: build_products/Frameworks
key: frameworks-${{ runner.os }}-${{ steps.idb_check.outputs.idb_commit }}-${{ steps.idb_inputs.outputs.patch_hash }}
key: frameworks-${{ runner.os }}-${{ steps.xcode_identity.outputs.cache_identity }}-${{ steps.idb_inputs.outputs.source_identity }}-${{ steps.idb_inputs.outputs.build_pipeline_hash }}

- name: Save XCFramework cache
if: steps.idb_check.outputs.needs_setup == 'true' || steps.xcframework_check.outputs.needs_build == 'true'
uses: actions/cache/save@v4
with:
path: build_products/XCFrameworks
key: xcframeworks-${{ runner.os }}-${{ steps.idb_check.outputs.idb_commit }}-${{ steps.idb_inputs.outputs.patch_hash }}
key: xcframeworks-${{ runner.os }}-${{ steps.xcode_identity.outputs.cache_identity }}-${{ steps.idb_inputs.outputs.source_identity }}-${{ steps.idb_inputs.outputs.build_pipeline_hash }}

- name: Prepare - Clean Swift build environment
run: |
Expand Down
33 changes: 33 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Test

on:
pull_request:
push:
branches:
- main
workflow_dispatch:

permissions:
contents: read

concurrency:
group: test-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
swift-tests:
runs-on: macos-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable

- name: Install build dependencies
run: brew install xcodegen

- name: Run Swift tests
run: ./test-runner.sh --unit-tests
17 changes: 16 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- Changed the IDB integration to consume an immutable full-SHA-pinned AXe fork instead of applying local patches during builds.
- Changed build and release validation to record Xcode build identifiers as provenance instead of requiring exact toolchain and runtime builds.
- Changed BGRA video streams to honor the requested `--fps` value consistently with the other stream formats instead of using an uncapped native frame rate.

### Fixed

- Fixed SimulatorKit loading with Xcode 27 beta by checking Xcode's `Contents/SharedFrameworks` layout before falling back to the legacy private-framework path.
- Fixed IDB framework builds failing on incomplete generated checkouts by replacing the cache with a clean clone.
- Fixed `make e2e` under Xcode 27 by rebuilding IDB frameworks with the selected toolchain, selecting the matching iOS 27 simulator, and launching Device Hub before running tests.
- Fixed simulator automation with Xcode 27 Beta 3 (27A5218g) while retaining Xcode 26 support by updating the IDB integration, selecting the compatible HID transport, and bootstrapping accessibility through Xcode's current private-framework path.
- Fixed `test-runner.sh` loading AXe's binary target frameworks with newer SwiftPM layouts while remaining compatible with Xcode 26.
- Fixed E2E test hangs by resolving the `axe` executable path before `swift test` starts instead of invoking SwiftPM from inside test cases.
- Fixed release archives embedding AppleDouble (`._*`) metadata files that broke strict Gatekeeper verification of the bundled frameworks by sanitizing staged payloads and excluding filesystem metadata from zip and tar packaging.
- Fixed `build.sh` blocking on an interactive git pager during IDB checkout verification when run in a terminal.
- Fixed Xcode 26 E2E setup and managed IDB cache recovery, including incomplete custom `IDB_CHECKOUT_DIR` checkouts created by AXe.
- Fixed HID broker crash and simulator-reboot recovery so concurrent cold starts spawn one broker, stale sessions are replaced before sending the first touch request, ambiguous touch requests are never replayed, and broker identity follows the selected Xcode.
- Fixed runtime failures printing raw error implementation details and internal simulator terminology instead of actionable, user-facing messages.
- Fixed transient Xcode 27 accessibility point lookups returning the screen root instead of the targeted element.
- Fixed E2E fixture relaunches racing Device Hub by verifying the requested screen before interacting.

## [v1.7.1] - 2026-06-02

Expand Down
30 changes: 28 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,28 @@
// swift-tools-version:5.10
import Foundation
import PackageDescription

let packageRoot = URL(fileURLWithPath: #filePath).deletingLastPathComponent()
let idbCheckoutDirectory = ProcessInfo.processInfo.environment["IDB_CHECKOUT_DIR"]
.map { URL(fileURLWithPath: $0) }
?? packageRoot.appendingPathComponent("idb_checkout", isDirectory: true)
let idbPrivateHeadersDirectory = idbCheckoutDirectory.appendingPathComponent(
"PrivateHeaders",
isDirectory: true
)
// Compile-only module-map inputs: never copy these headers into release artifacts or runtime rpaths.
let idbPrivateHeaderSearchFlags = [
idbPrivateHeadersDirectory,
idbPrivateHeadersDirectory.appendingPathComponent("AccessibilityPlatformTranslation", isDirectory: true),
idbPrivateHeadersDirectory.appendingPathComponent("AXRuntime", isDirectory: true),
idbPrivateHeadersDirectory.appendingPathComponent("CoreSimDeviceIO", isDirectory: true),
idbPrivateHeadersDirectory.appendingPathComponent("CoreSimulator", isDirectory: true),
idbPrivateHeadersDirectory.appendingPathComponent("CoreSimulatorUtilities", isDirectory: true),
idbPrivateHeadersDirectory.appendingPathComponent("SimulatorKit", isDirectory: true),
// Keep each search path in one token. SwiftPM can otherwise drop the path while propagating
// unsafe flags to its generated test runner, leaving a bare `-I` that consumes the next option.
].map { "-I\($0.path)" }

let package = Package(
name: "AXe",
platforms: [
Expand Down Expand Up @@ -39,7 +61,7 @@ let package = Package(
.copy("Resources/skills")
],
swiftSettings: [
.unsafeFlags(["-parse-as-library"])
.unsafeFlags(["-parse-as-library"] + idbPrivateHeaderSearchFlags)
],
linkerSettings: [
// For XCFrameworks, rpath can often be just @executable_path
Expand All @@ -56,7 +78,11 @@ let package = Package(
.testTarget(
name: "AXeTests",
dependencies: ["AXe", "AXeCore"],
path: "Tests"
path: "Tests",
exclude: ["Goldens"],
swiftSettings: [
.unsafeFlags(idbPrivateHeaderSearchFlags)
]
),
.plugin(
name: "VersionPlugin",
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,20 @@ axe --help
axe list-simulators
```

## Xcode compatibility

AXe supports Xcode 26 and Xcode 27. Xcode 27 simulator automation uses Device Hub; Simulator.app is not required.

Release artifacts are built once with the Xcode version selected by the release environment and run unchanged with either supported Xcode selected. Compatibility was validated with Xcode 26.5 (build `17F42`) and iOS 26.5 (`23F77`), and with Xcode 27 Beta 3 (build `27A5218g`) and iOS 27 (`24A5380g`).

AXe builds IDB from the immutable fork revision `cameroncooke/idb@1395103ca786ee990c70514e1f8bb75fa98cdd82`, based on upstream IDB `e682506725e9efefb9c43b8b917c0b12eb2a5939`. The build does not apply a local patch queue.

## E2E development

`make e2e` rebuilds the local IDB XCFrameworks, builds AXe, and runs the simulator tests with the Xcode selected by `DEVELOPER_DIR` or `xcode-select`. XcodeGen is required to generate the IDB projects. When Xcode 27 is selected, the runner starts Device Hub, chooses an available iOS 27 iPhone 17 Pro, and boots it with `simctl`.

To validate a release payload unchanged under another supported Xcode, first build the test bundle with that Xcode, then run `AXE_BIN_PATH=/path/to/release/axe ./test-runner.sh --tests-only`. The supplied executable must retain its packaged frameworks beside it.

## Basic usage

```bash
Expand Down
8 changes: 4 additions & 4 deletions Sources/AXe/Commands/Button.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,11 @@ struct Button: AsyncParsableCommand {

if let duration = duration {
// For duration-based presses, we need to create separate down/up events with delay
let buttonDownEvent = FBSimulatorHIDEvent.buttonDown(buttonType.hidButton)
let buttonDownEvent = FBSimulatorHIDEvent.button(direction: .down, button: buttonType.hidButton)
let delayEvent = FBSimulatorHIDEvent.delay(duration)
let buttonUpEvent = FBSimulatorHIDEvent.buttonUp(buttonType.hidButton)
buttonEvent = FBSimulatorHIDEvent(events: [
let buttonUpEvent = FBSimulatorHIDEvent.button(direction: .up, button: buttonType.hidButton)

buttonEvent = FBSimulatorHIDEvent.composite([
buttonDownEvent,
delayEvent,
buttonUpEvent
Expand Down
2 changes: 1 addition & 1 deletion Sources/AXe/Commands/Gesture.swift
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ struct Gesture: AsyncParsableCommand {
}

// Execute the gesture sequence
let finalEvent = events.count == 1 ? events[0] : FBSimulatorHIDEvent(events: events)
let finalEvent = events.count == 1 ? events[0] : FBSimulatorHIDEvent.composite(events)

try await HIDInteractor
.performHIDEvent(
Expand Down
28 changes: 28 additions & 0 deletions Sources/AXe/Commands/HIDBrokerCommand.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import ArgumentParser
import Darwin
import FBControlCore

struct HIDBrokerCommand: AsyncParsableCommand {
// This hidden command is the process entry point used by AXe's HID client. It is an internal
// implementation detail rather than a supported public CLI command.
static let configuration = CommandConfiguration(
commandName: "hid-broker",
shouldDisplay: false
)

@Option(name: .customLong("udid"))
var simulatorUDID: String

func run() async throws {
let endpoint = try HIDBroker.endpointPath(simulatorUDID: simulatorUDID)
let lifetimeLock = try HIDBroker.acquireLifetimeLock(endpoint: endpoint)
defer {
_ = flock(lifetimeLock, LOCK_UN)
Darwin.close(lifetimeLock)
}
let logger = AxeLogger()
try await setup(logger: logger)
try await performGlobalSetup(logger: logger)
try await HIDBroker.serve(simulatorUDID: simulatorUDID, logger: logger)
}
}
8 changes: 4 additions & 4 deletions Sources/AXe/Commands/Key.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,11 @@ struct Key: AsyncParsableCommand {

if let duration = duration {
// For duration-based presses, we need to create separate down/up events with delay
let keyDownEvent = FBSimulatorHIDEvent.keyDown(UInt32(keycode))
let keyDownEvent = FBSimulatorHIDEvent.keyboard(direction: .down, keyCode: UInt32(keycode))
let delayEvent = FBSimulatorHIDEvent.delay(duration)
let keyUpEvent = FBSimulatorHIDEvent.keyUp(UInt32(keycode))
keyEvent = FBSimulatorHIDEvent(events: [
let keyUpEvent = FBSimulatorHIDEvent.keyboard(direction: .up, keyCode: UInt32(keycode))

keyEvent = FBSimulatorHIDEvent.composite([
keyDownEvent,
delayEvent,
keyUpEvent
Expand Down
6 changes: 3 additions & 3 deletions Sources/AXe/Commands/KeyCombo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,18 +76,18 @@ struct KeyCombo: AsyncParsableCommand {

// Press modifiers down in order
for modifier in parsedModifiers {
events.append(FBSimulatorHIDEvent.keyDown(UInt32(modifier)))
events.append(FBSimulatorHIDEvent.keyboard(direction: .down, keyCode: UInt32(modifier)))
}

// Press and release the target key
events.append(FBSimulatorHIDEvent.shortKeyPress(UInt32(key)))

// Release modifiers in reverse order
for modifier in parsedModifiers.reversed() {
events.append(FBSimulatorHIDEvent.keyUp(UInt32(modifier)))
events.append(FBSimulatorHIDEvent.keyboard(direction: .up, keyCode: UInt32(modifier)))
}

let comboEvent = FBSimulatorHIDEvent(events: events)
let comboEvent = FBSimulatorHIDEvent.composite(events)

try await HIDInteractor
.performHIDEvent(
Expand Down
2 changes: 1 addition & 1 deletion Sources/AXe/Commands/KeySequence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ struct KeySequence: AsyncParsableCommand {
}

// Create composite event
let sequenceEvent = FBSimulatorHIDEvent(events: events)
let sequenceEvent = FBSimulatorHIDEvent.composite(events)

// Perform the key sequence event
try await HIDInteractor
Expand Down
Loading
Loading