Skip to content

feat: Add WatchOS support - #1209

Merged
mykola-mokhnach merged 2 commits into
masterfrom
watch
Aug 18, 2026
Merged

feat: Add WatchOS support#1209
mykola-mokhnach merged 2 commits into
masterfrom
watch

Conversation

@mykola-mokhnach

@mykola-mokhnach mykola-mokhnach commented Aug 14, 2026

Copy link
Copy Markdown

Summary

Adds a working watchOS port of WebDriverAgent (#4829): WebDriverAgentLib_watchOS and WebDriverAgentRunner_watchOS build, run, and serve the real WebDriver HTTP API against a watchOS Simulator target — verified end to end, not just compiled.

  • find, click, screenshot, source, session lifecycle, app lifecycle, device info/location, and all locator strategies (accessibility id, class name, class chain, xpath, predicate string, link text) work correctly over HTTP against the real server, the same way Appium would drive it.
  • type has a known, documented limitation: the on-screen keyboard opens and the field gets focus, but neither low-level key synthesis nor the public -typeText: API delivers keystrokes into it in this environment. Covered by a test wrapped in XCTExpectFailure so it starts failing loudly (as a signal to revisit) the moment it starts working, instead of silently staying stale.
  • CI builds and statically analyzes the new watchOS targets, mirroring the existing tvOS matrix.

Why watchOS needs its own port, not just new #if branches

  • No BSD sockets. watchOS forbids them entirely, so RoutingHTTPServer/CocoaHTTPServer/CocoaAsyncSocket (all GCDAsyncSocket-based) can't be built there. FBTCPSocket gained a full TARGET_OS_WATCH implementation on Network.framework's C API (nw_listener_t/nw_connection_t) instead.
  • No RoutingHTTPServer. A new minimal HTTP/1.1 server (FBWatchHTTPServer, plus RouteRequest/RouteResponse stand-ins) sits on top of the new socket layer, reproducing just enough of RoutingHTTPServer's surface that FBWebServer.m only needs a single #if TARGET_OS_WATCH swap of which server class it drives — no changes to route registration, dispatch, or exception handling.
  • A much smaller UI-automation surface. Gesture synthesis (swipe/pinch/rotate/tap-coordinates/etc.), orientation/rotation, pasteboard, battery info, and MJPEG streaming are either not meaningfully applicable on watchOS or not verified to work there, so those routes are excluded from the watchOS build rather than left to fail unpredictably at runtime.

Test coverage

IntegrationTests_watchOS was rewritten from a single leftover spike file into 10 split test files (32 test methods), organized by feature area like IntegrationTests_1/2/3/IntegrationTests_tvOS — but unlike those, which call WDA's fb_* categories in-process, this suite runs as its own XCTest process and drives the real WebDriverAgentRunner_watchOS server purely over HTTP, matching how a real client actually exercises WDA.

Deliberately out of scope for this suite (with rationale in code comments): gesture synthesis, orientation, pasteboard, battery, keyboard input, picker wheel (none registered on watchOS at all); video recording, VoiceOver toggling, Touch ID, Siri, homescreen/lock/pressButton (registered, but either unverified on the Simulator or too disruptive for a repeatable, hermetic suite).

CI

Added Generic_watchOS_Build, watchOS_Build, watchOS_Lib_Analyze, and watchOS_Runner_Analyze to the build/analyze matrix in wda-tests.yml, at both MIN and MAX Xcode — the same shape as the existing tvOS entries. Scripts/build.sh gained matching watch_lib/watch_runner/watch/watch_generic/watch_sim/watch_device cases. No unit/integration test job was added yet — that's future work, see below.

Real device status

Per feedback from @Dan-Maor testing on real hardware (see comment):

  • A framework-embed bug (WebDriverAgentLib_watchOS.framework was copied into Resources/ instead of Frameworks/ inside the .xctest bundle) has been fixed — this was causing the runner to crash on launch on a real device.
  • With that fixed, the HTTP server still doesn't work on a real Apple Watch: connections never complete (nw_connection_get_flow_id_on_nw_queue failed repeating in the log). Per Apple's own guidance (TN3135), low-level networking via Network.framework is permitted on the Simulator but restricted to a narrow set of approved use cases on real watchOS hardware — general-purpose TCP servers aren't one of them. This is a platform restriction, not a WDA bug, and there is currently no known workaround.
  • watchOS Simulator is the only supported target for now. Real-device support is blocked on Apple's networking restrictions and is tracked as a follow-up, not solved in this PR.

Known limitations / follow-ups (not in scope for this PR)

  • Typing doesn't work (see above) — needs further investigation, possibly lower-level HID key injection.
  • Real hardware doesn't work for the reasons above — Simulator only for now.
  • No CI test job yet for IntegrationTests_watchOS (build + static analysis only for now).
  • No appium-xcuitest-driver changes — this PR is WDA-side only; the driver still needs scheme selection, platform/device detection, and new capabilities to actually launch this from Appium (see the tvOS driver PR, tvos support appium-xcuitest-driver#911, as a template).

@mykola-mokhnach

Copy link
Copy Markdown
Author

@Dan-Maor @KazuCocoa Could you please try it with a real device? I haven't wired watch into the node package fully yet, this is just a POC to gather the feedback and to test if it works with real devices (I did my testing on Simulator)

@eglitise

Copy link
Copy Markdown

Wow, incredible work! 🎉

  • Is there a hard minimum supported watchOS version? I see that CI runs on 10.5 as the oldest version, but I'm sure someone will want to try this on an even older version, so it's worth specifying.
  • It seems gestures aren't supported, which is a major limitation. Apple does recommend implementing app navigation primarily using the Digital Crown since watchOS 10, so support for that would be quite important to add in a follow-up PR.

Comment thread WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m Outdated
Comment thread WebDriverAgentLib/Commands/FBCustomCommands.m Outdated
@Dan-Maor

Copy link
Copy Markdown
Collaborator

I have an unused Apple watch lying around somewhere, will try to test it on Sunday.

@KazuCocoa

Copy link
Copy Markdown
Member

I don't have Apple Watch, although will review the code tomorrow or the next

Comment thread WebDriverAgentLib/Routing/WatchOS/FBWatchHTTPServer.m Outdated
@Dan-Maor

Copy link
Copy Markdown
Collaborator

I've experimented on my Apple Watch and managed to get it to run, but there are two blocking issues:

  1. The Xcode project is configured to copy the WebDriverAgentLib framework to the Resources directory instead of the Frameworks directory; without changing it to Frameworks the test runner crashes on startup stating not being able to locate the framework.
  2. After launching the test runner I've attempted to send a /status command via curl, but it hanged with the following message repeating in the debugger log in Xcode:
nw_ip_channel_inbox_handle_new_flow nw_connection_get_flow_id_on_nw_queue failed

Restarting the watch didn't help either, seems to me like the network connection handler doesn't fire.

Like mentioned in the description - Apple doesn't really like the idea of using low level networking on Apple Watch for anything other than some specific scenarios listed here: https://developer.apple.com/documentation/technotes/tn3135-low-level-networking-on-watchos and emphasize the difference between a simulator and a real Apple Watch:

When writing watchOS networking code, test it on a real device; the simulator always allows low-level networking.

Given that the code is functional on simulators it may be still worth merging it while emphasizing that only simulators are supported for Apple Watch testing.

mykola-mokhnach added a commit that referenced this pull request Aug 16, 2026
…ks, not Resources

WebDriverAgentRunner_watchOS's copy-files build phase used
dstSubfolderSpec = 7 (Resources) instead of 10 (Frameworks), so the
framework ended up in the wrong location inside the .xctest bundle.
On a real device this made the runner crash on startup unable to
locate the framework (works on Simulator, which is more lenient about
bundle layout). Found by @Dan-Maor testing on real hardware, see
#1209 (comment)

Verified locally: framework now lands under
PlugIns/WebDriverAgentRunner_watchOS.xctest/Frameworks/ instead of
Resources/, matching the existing iOS/tvOS "Copy frameworks" phases.
@mykola-mokhnach

Copy link
Copy Markdown
Author

I've experimented on my Apple Watch and managed to get it to run, but there are two blocking issues:

  1. The Xcode project is configured to copy the WebDriverAgentLib framework to the Resources directory instead of the Frameworks directory; without changing it to Frameworks the test runner crashes on startup stating not being able to locate the framework.
  2. After launching the test runner I've attempted to send a /status command via curl, but it hanged with the following message repeating in the debugger log in Xcode:
nw_ip_channel_inbox_handle_new_flow nw_connection_get_flow_id_on_nw_queue failed

Restarting the watch didn't help either, seems to me like the network connection handler doesn't fire.

Like mentioned in the description - Apple doesn't really like the idea of using low level networking on Apple Watch for anything other than some specific scenarios listed here: https://developer.apple.com/documentation/technotes/tn3135-low-level-networking-on-watchos and emphasize the difference between a simulator and a real Apple Watch:

When writing watchOS networking code, test it on a real device; the simulator always allows low-level networking.

Given that the code is functional on simulators it may be still worth merging it while emphasizing that only simulators are supported for Apple Watch testing.

Thanks for checking it @Dan-Maor
I've fixed the problem with copying frameworks and documented Simulator-only destination.

mykola-mokhnach added a commit that referenced this pull request Aug 16, 2026
## Summary

Adds a working watchOS port of WebDriverAgent (#4829): `WebDriverAgentLib_watchOS` and `WebDriverAgentRunner_watchOS` build, run, and serve the real WebDriver HTTP API against a watchOS Simulator target — verified end to end, not just compiled.

- `find`, `click`, `screenshot`, `source`, session lifecycle, app lifecycle, device info/location, and all locator strategies (accessibility id, class name, class chain, xpath, predicate string, link text) work correctly over HTTP against the real server, the same way Appium would drive it.
- `type` has a known, documented limitation: the on-screen keyboard opens and the field gets focus, but neither low-level key synthesis nor the public `-typeText:` API delivers keystrokes into it in this environment. Covered by a test wrapped in `XCTExpectFailure` so it starts failing loudly (as a signal to revisit) the moment it starts working, instead of silently staying stale.
- CI builds and statically analyzes the new watchOS targets, mirroring the existing tvOS matrix.

## Why watchOS needs its own port, not just new `#if` branches

- **No BSD sockets.** watchOS forbids them entirely, so `RoutingHTTPServer`/`CocoaHTTPServer`/`CocoaAsyncSocket` (all GCDAsyncSocket-based) can't be built there. `FBTCPSocket` gained a full `TARGET_OS_WATCH` implementation on `Network.framework`'s C API (`nw_listener_t`/`nw_connection_t`) instead.
- **No RoutingHTTPServer.** A new minimal HTTP/1.1 server (`FBWatchHTTPServer`, plus `RouteRequest`/`RouteResponse` stand-ins) sits on top of the new socket layer, reproducing just enough of RoutingHTTPServer's surface that `FBWebServer.m` only needs a single `#if TARGET_OS_WATCH` swap of which server class it drives — no changes to route registration, dispatch, or exception handling.
- **A much smaller UI-automation surface.** Gesture synthesis (swipe/pinch/rotate/tap-coordinates/etc.), orientation/rotation, pasteboard, battery info, and MJPEG streaming are either not meaningfully applicable on watchOS or not verified to work there, so those routes are excluded from the watchOS build rather than left to fail unpredictably at runtime.

## Test coverage

`IntegrationTests_watchOS` was rewritten from a single leftover spike file into 10 split test files (32 test methods), organized by feature area like `IntegrationTests_1/2/3`/`IntegrationTests_tvOS` — but unlike those, which call WDA's `fb_*` categories in-process, this suite runs as its own XCTest process and drives the real `WebDriverAgentRunner_watchOS` server purely over HTTP, matching how a real client actually exercises WDA.

Deliberately out of scope for this suite (with rationale in code comments): gesture synthesis, orientation, pasteboard, battery, keyboard input, picker wheel (none registered on watchOS at all); video recording, VoiceOver toggling, Touch ID, Siri, homescreen/lock/pressButton (registered, but either unverified on the Simulator or too disruptive for a repeatable, hermetic suite).

## CI

Added `Generic_watchOS_Build`, `watchOS_Build`, `watchOS_Lib_Analyze`, and `watchOS_Runner_Analyze` to the build/analyze matrix in `wda-tests.yml`, at both MIN and MAX Xcode — the same shape as the existing tvOS entries. `Scripts/build.sh` gained matching `watch_lib`/`watch_runner`/`watch`/`watch_generic`/`watch_sim`/`watch_device` cases. No unit/integration test job was added yet — that's future work, see below.

## Real device status

Per feedback from @Dan-Maor testing on real hardware (see [comment](#1209 (comment))):

- A framework-embed bug (`WebDriverAgentLib_watchOS.framework` was copied into `Resources/` instead of `Frameworks/` inside the `.xctest` bundle) has been fixed — this was causing the runner to crash on launch on a real device.
- With that fixed, the HTTP server still doesn't work on a real Apple Watch: connections never complete (`nw_connection_get_flow_id_on_nw_queue failed` repeating in the log). Per Apple's own guidance ([TN3135](https://developer.apple.com/documentation/technotes/tn3135-low-level-networking-on-watchos)), low-level networking via `Network.framework` is permitted on the Simulator but restricted to a narrow set of approved use cases on real watchOS hardware — general-purpose TCP servers aren't one of them. This is a platform restriction, not a WDA bug, and there is currently no known workaround.
- **watchOS Simulator is the only supported target for now.** Real-device support is blocked on Apple's networking restrictions and is tracked as a follow-up, not solved in this PR.

## Known limitations / follow-ups (not in scope for this PR)

- **Typing doesn't work** (see above) — needs further investigation, possibly lower-level HID key injection.
- **Real hardware doesn't work** for the reasons above — Simulator only for now.
- **No CI test job yet** for `IntegrationTests_watchOS` (build + static analysis only for now).
- **No `appium-xcuitest-driver` changes** — this PR is WDA-side only; the driver still needs scheme selection, platform/device detection, and new capabilities to actually launch this from Appium (see the tvOS driver PR, appium/appium-xcuitest-driver#911, as a template).
Comment thread WebDriverAgentTests/IntegrationTests_watchOS/WDAWatchHTTPClient.swift Outdated
Comment thread WebDriverAgentLib/Routing/FBTCPSocket.m Outdated

@KazuCocoa KazuCocoa left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lg. The title can address "simulator"

Adds a working watchOS port of WebDriverAgent (#4829): `WebDriverAgentLib_watchOS` and `WebDriverAgentRunner_watchOS` build, run, and serve the real WebDriver HTTP API against a watchOS Simulator target — verified end to end, not just compiled.

- `find`, `click`, `screenshot`, `source`, session lifecycle, app lifecycle, device info/location, and all locator strategies (accessibility id, class name, class chain, xpath, predicate string, link text) work correctly over HTTP against the real server, the same way Appium would drive it.
- `type` has a known, documented limitation: the on-screen keyboard opens and the field gets focus, but neither low-level key synthesis nor the public `-typeText:` API delivers keystrokes into it in this environment. Covered by a test wrapped in `XCTExpectFailure` so it starts failing loudly (as a signal to revisit) the moment it starts working, instead of silently staying stale.
- CI builds and statically analyzes the new watchOS targets, mirroring the existing tvOS matrix.

- **No BSD sockets.** watchOS forbids them entirely, so `RoutingHTTPServer`/`CocoaHTTPServer`/`CocoaAsyncSocket` (all GCDAsyncSocket-based) can't be built there. `FBTCPSocket` gained a full `TARGET_OS_WATCH` implementation on `Network.framework`'s C API (`nw_listener_t`/`nw_connection_t`) instead.
- **No RoutingHTTPServer.** A new minimal HTTP/1.1 server (`FBWatchHTTPServer`, plus `RouteRequest`/`RouteResponse` stand-ins) sits on top of the new socket layer, reproducing just enough of RoutingHTTPServer's surface that `FBWebServer.m` only needs a single `#if TARGET_OS_WATCH` swap of which server class it drives — no changes to route registration, dispatch, or exception handling.
- **A much smaller UI-automation surface.** Gesture synthesis (swipe/pinch/rotate/tap-coordinates/etc.), orientation/rotation, pasteboard, battery info, and MJPEG streaming are either not meaningfully applicable on watchOS or not verified to work there, so those routes are excluded from the watchOS build rather than left to fail unpredictably at runtime.

`IntegrationTests_watchOS` was rewritten from a single leftover spike file into 10 split test files (32 test methods), organized by feature area like `IntegrationTests_1/2/3`/`IntegrationTests_tvOS` — but unlike those, which call WDA's `fb_*` categories in-process, this suite runs as its own XCTest process and drives the real `WebDriverAgentRunner_watchOS` server purely over HTTP, matching how a real client actually exercises WDA.

Deliberately out of scope for this suite (with rationale in code comments): gesture synthesis, orientation, pasteboard, battery, keyboard input, picker wheel (none registered on watchOS at all); video recording, VoiceOver toggling, Touch ID, Siri, homescreen/lock/pressButton (registered, but either unverified on the Simulator or too disruptive for a repeatable, hermetic suite).

Added `Generic_watchOS_Build`, `watchOS_Build`, `watchOS_Lib_Analyze`, and `watchOS_Runner_Analyze` to the build/analyze matrix in `wda-tests.yml`, at both MIN and MAX Xcode — the same shape as the existing tvOS entries. `Scripts/build.sh` gained matching `watch_lib`/`watch_runner`/`watch`/`watch_generic`/`watch_sim`/`watch_device` cases. No unit/integration test job was added yet — that's future work, see below.

Per feedback from @Dan-Maor testing on real hardware (see [comment](#1209 (comment))):

- A framework-embed bug (`WebDriverAgentLib_watchOS.framework` was copied into `Resources/` instead of `Frameworks/` inside the `.xctest` bundle) has been fixed — this was causing the runner to crash on launch on a real device.
- With that fixed, the HTTP server still doesn't work on a real Apple Watch: connections never complete (`nw_connection_get_flow_id_on_nw_queue failed` repeating in the log). Per Apple's own guidance ([TN3135](https://developer.apple.com/documentation/technotes/tn3135-low-level-networking-on-watchos)), low-level networking via `Network.framework` is permitted on the Simulator but restricted to a narrow set of approved use cases on real watchOS hardware — general-purpose TCP servers aren't one of them. This is a platform restriction, not a WDA bug, and there is currently no known workaround.
- **watchOS Simulator is the only supported target for now.** Real-device support is blocked on Apple's networking restrictions and is tracked as a follow-up, not solved in this PR.

- **Typing doesn't work** (see above) — needs further investigation, possibly lower-level HID key injection.
- **Real hardware doesn't work** for the reasons above — Simulator only for now.
- **No CI test job yet** for `IntegrationTests_watchOS` (build + static analysis only for now).
- **No `appium-xcuitest-driver` changes** — this PR is WDA-side only; the driver still needs scheme selection, platform/device detection, and new capabilities to actually launch this from Appium (see the tvOS driver PR, appium/appium-xcuitest-driver#911, as a template).
- FBTCPSocket.m: nw_listener_set_state_changed_handler's failure path
  stored the underlying CFError/NSError itself under
  NSLocalizedDescriptionKey, which must be string-valued - calling
  startupError.localizedDescription would crash with
  -[NSError length]: unrecognized selector. Use the underlying error's
  .localizedDescription for the string and keep the error itself under
  NSUnderlyingErrorKey. (@KazuCocoa)
- WDAWatchHTTPClient.swift: doc comment referenced the now-deleted
  Scripts/watchos-http-integration-test.sh; point at the actual way to
  start the server instead. (@KazuCocoa)
@mykola-mokhnach
mykola-mokhnach merged commit 90b443c into master Aug 18, 2026
51 of 54 checks passed
@mykola-mokhnach
mykola-mokhnach deleted the watch branch August 18, 2026 19:25
github-actions Bot pushed a commit that referenced this pull request Aug 18, 2026
## [16.3.0](v16.2.2...v16.3.0) (2026-08-18)

### Features

* Add WatchOS support ([#1209](#1209)) ([90b443c](90b443c))
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 16.3.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants