diff --git a/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 0000000..8ed5cfc --- /dev/null +++ b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "bkg.png", + "idiom" : "tv" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/bkg.png b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/bkg.png new file mode 100644 index 0000000..d7884c5 Binary files /dev/null and b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Content.imageset/bkg.png differ diff --git a/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Back.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json new file mode 100644 index 0000000..de59d88 --- /dev/null +++ b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Contents.json @@ -0,0 +1,17 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "layers" : [ + { + "filename" : "Front.imagestacklayer" + }, + { + "filename" : "Middle.imagestacklayer" + }, + { + "filename" : "Back.imagestacklayer" + } + ] +} diff --git a/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 0000000..3be21fb --- /dev/null +++ b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "logo.png", + "idiom" : "tv" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/logo.png b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/logo.png new file mode 100644 index 0000000..b6f70c9 Binary files /dev/null and b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Content.imageset/logo.png differ diff --git a/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Front.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 0000000..ffed132 --- /dev/null +++ b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "mask_clean.png", + "idiom" : "tv" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/mask_clean.png b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/mask_clean.png new file mode 100644 index 0000000..64494dd Binary files /dev/null and b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Content.imageset/mask_clean.png differ diff --git a/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon - App Store.imagestack/Middle.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 0000000..61f789c --- /dev/null +++ b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,18 @@ +{ + "images" : [ + { + "filename" : "bkg@1x.png", + "idiom" : "tv", + "scale" : "1x" + }, + { + "filename" : "bkg@2x.png", + "idiom" : "tv", + "scale" : "2x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/bkg@1x.png b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/bkg@1x.png new file mode 100644 index 0000000..efaa550 Binary files /dev/null and b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/bkg@1x.png differ diff --git a/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/bkg@2x.png b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/bkg@2x.png new file mode 100644 index 0000000..0b5733e Binary files /dev/null and b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Content.imageset/bkg@2x.png differ diff --git a/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Back.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json new file mode 100644 index 0000000..de59d88 --- /dev/null +++ b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Contents.json @@ -0,0 +1,17 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "layers" : [ + { + "filename" : "Front.imagestacklayer" + }, + { + "filename" : "Middle.imagestacklayer" + }, + { + "filename" : "Back.imagestacklayer" + } + ] +} diff --git a/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 0000000..766a2a9 --- /dev/null +++ b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,18 @@ +{ + "images" : [ + { + "filename" : "logo@1x.png", + "idiom" : "tv", + "scale" : "1x" + }, + { + "filename" : "logo@2x.png", + "idiom" : "tv", + "scale" : "2x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/logo@1x.png b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/logo@1x.png new file mode 100644 index 0000000..b05c190 Binary files /dev/null and b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/logo@1x.png differ diff --git a/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/logo@2x.png b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/logo@2x.png new file mode 100644 index 0000000..593fb74 Binary files /dev/null and b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Content.imageset/logo@2x.png differ diff --git a/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Front.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json new file mode 100644 index 0000000..612ca1e --- /dev/null +++ b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,18 @@ +{ + "images" : [ + { + "filename" : "mask_clean@1x.png", + "idiom" : "tv", + "scale" : "1x" + }, + { + "filename" : "mask_clean@2x.png", + "idiom" : "tv", + "scale" : "2x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/mask_clean@1x.png b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/mask_clean@1x.png new file mode 100644 index 0000000..69a1e2d Binary files /dev/null and b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/mask_clean@1x.png differ diff --git a/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/mask_clean@2x.png b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/mask_clean@2x.png new file mode 100644 index 0000000..1e8f5ec Binary files /dev/null and b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Content.imageset/mask_clean@2x.png differ diff --git a/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/App Icon.imagestack/Middle.imagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json new file mode 100644 index 0000000..f47ba43 --- /dev/null +++ b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Contents.json @@ -0,0 +1,32 @@ +{ + "assets" : [ + { + "filename" : "App Icon - App Store.imagestack", + "idiom" : "tv", + "role" : "primary-app-icon", + "size" : "1280x768" + }, + { + "filename" : "App Icon.imagestack", + "idiom" : "tv", + "role" : "primary-app-icon", + "size" : "400x240" + }, + { + "filename" : "Top Shelf Image Wide.imageset", + "idiom" : "tv", + "role" : "top-shelf-image-wide", + "size" : "2320x720" + }, + { + "filename" : "Top Shelf Image.imageset", + "idiom" : "tv", + "role" : "top-shelf-image", + "size" : "1920x720" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json new file mode 100644 index 0000000..795cce1 --- /dev/null +++ b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + }, + { + "idiom" : "tv", + "scale" : "2x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json new file mode 100644 index 0000000..795cce1 --- /dev/null +++ b/DeviceKitTests/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "idiom" : "tv", + "scale" : "1x" + }, + { + "idiom" : "tv", + "scale" : "2x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DeviceKitTests/JSONRPC/Handlers/DeviceInfo.swift b/DeviceKitTests/JSONRPC/Handlers/DeviceInfo.swift index bcb9771..232a06b 100644 --- a/DeviceKitTests/JSONRPC/Handlers/DeviceInfo.swift +++ b/DeviceKitTests/JSONRPC/Handlers/DeviceInfo.swift @@ -13,11 +13,19 @@ struct DeviceInfoMethodHandler: RPCMethodHandler { let start = Date() + #if os(tvOS) + // tvOS has no SpringBoard; the main screen bounds describe the full UI. + let bounds = UIScreen.main.bounds + let scale = Int(UIScreen.main.scale) + let width = Int(bounds.width) + let height = Int(bounds.height) + #else let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") let frame = springboard.frame let scale = Int(UIScreen.main.scale) let width = Int(frame.width) let height = Int(frame.height) + #endif let duration = Date().timeIntervalSince(start) logger.info("Device info took \(duration), screen: \(width)x\(height)@\(scale)x") diff --git a/DeviceKitTests/JSONRPC/Handlers/DumpUI.swift b/DeviceKitTests/JSONRPC/Handlers/DumpUI.swift index bf9d3f0..10c346c 100644 --- a/DeviceKitTests/JSONRPC/Handlers/DumpUI.swift +++ b/DeviceKitTests/JSONRPC/Handlers/DumpUI.swift @@ -54,9 +54,15 @@ struct DumpUIMethodHandler: RPCMethodHandler { do { let foregroundApp = RunningApp.getForegroundApp() guard let foregroundApp = foregroundApp else { + #if os(iOS) logger.warning("No foreground app found, returning springboard app hierarchy") let springboardHierarchy = try elementHierarchy(xcuiElement: springboardApplication) return try formatResponse(axElement: springboardHierarchy, format: format) + #else + // tvOS has no SpringBoard; without a foreground app there is no + // hierarchy to serialise. + throw RPCMethodError.internalError("No foreground app found") + #endif } logger.info("[Start] View hierarchy snapshot for \(foregroundApp)") @@ -106,6 +112,7 @@ struct DumpUIMethodHandler: RPCMethodHandler { SystemPermissionManager.handleSystemPermissionAlertIfNeeded(foregroundApp: foregroundApp) let appHierarchy = try getHierarchyWithFallback(foregroundApp) + #if os(iOS) let statusBars = logger.measure(message: "Fetch status bar hierarchy") { fullStatusBars(springboardApplication) } ?? [] @@ -134,6 +141,11 @@ struct DumpUIMethodHandler: RPCMethodHandler { } return buildRootElement(app: appHierarchy, statusBars: statusBars, safari: safariWebViewHierarchy) + #else + // tvOS has no SpringBoard / status bars / Safari view service. Use the + // foreground app's own root hierarchy directly. + return buildRootElement(app: appHierarchy, statusBars: [], safari: nil) + #endif } private func buildRootElement(app: AXElement, statusBars: [AXElement], safari: AXElement?) -> AXElement { diff --git a/DeviceKitTests/JSONRPC/Handlers/IOButton.swift b/DeviceKitTests/JSONRPC/Handlers/IOButton.swift index a9dcf63..f1e1e0c 100644 --- a/DeviceKitTests/JSONRPC/Handlers/IOButton.swift +++ b/DeviceKitTests/JSONRPC/Handlers/IOButton.swift @@ -1,6 +1,9 @@ +import XCTest import os -struct IOButtonRequest : Codable { +#if os(iOS) + +struct IOButtonRequest: Codable { enum Button: String, Codable { case home case lock @@ -52,3 +55,66 @@ struct IOButtonMethodHandler: RPCMethodHandler { } } +#elseif os(tvOS) + +struct IOButtonRequest: Codable { + /// tvOS is driven by the Siri Remote. Every button maps to an + /// `XCUIRemote.Button` press against the currently focused element. + enum Button: String, Codable { + case up + case down + case left + case right + case select + case menu + case home + case playPause + } + + let button: Button +} + +@MainActor +struct IOButtonMethodHandler: RPCMethodHandler { + static let methodName = "device.io.button" + + private let logger = Logger( + subsystem: Bundle.main.bundleIdentifier!, + category: String(describing: Self.self) + ) + + func execute(params: JSONValue?) async throws -> JSONValue { + let request = try decodeParams(IOButtonRequest.self, from: params) + + let start = Date() + + logger.info("[Start] Pressing remote button: \(request.button.rawValue)") + let remoteButton: XCUIRemote.Button + switch request.button { + case .up: + remoteButton = .up + case .down: + remoteButton = .down + case .left: + remoteButton = .left + case .right: + remoteButton = .right + case .select: + remoteButton = .select + case .menu: + remoteButton = .menu + case .home: + remoteButton = .home + case .playPause: + remoteButton = .playPause + } + XCUIRemote.shared.press(remoteButton) + logger.info("[Done] Pressing remote button: \(request.button.rawValue)") + + let duration = Date().timeIntervalSince(start) + logger.info("Remote button press duration took \(duration)") + return .object(["success": .bool(true)]) + } +} + +#endif diff --git a/DeviceKitTests/JSONRPC/Handlers/IOFocus.swift b/DeviceKitTests/JSONRPC/Handlers/IOFocus.swift new file mode 100644 index 0000000..a8a2813 --- /dev/null +++ b/DeviceKitTests/JSONRPC/Handlers/IOFocus.swift @@ -0,0 +1,194 @@ +import XCTest +import os + +#if os(tvOS) + +/// Parameters for `device.io.focus`. +/// +/// The target element is selected by accessibility `identifier` and/or `label`. +/// At least one of `identifier`/`label` must be supplied. `elementType` is an +/// optional hint carried for forward compatibility. +struct IOFocusRequest: Codable { + let identifier: String? + let label: String? + let elementType: String? +} + +@MainActor +struct IOFocusMethodHandler: RPCMethodHandler { + static let methodName = "device.io.focus" + + /// Upper bound on Siri Remote moves before giving up (D3). + private static let maxMoves = 50 + + private let logger = Logger( + subsystem: Bundle.main.bundleIdentifier!, + category: String(describing: Self.self) + ) + + func execute(params: JSONValue?) async throws -> JSONValue { + let request = try decodeParams(IOFocusRequest.self, from: params) + + let identifier = request.identifier.flatMap { $0.isEmpty ? nil : $0 } + let label = request.label.flatMap { $0.isEmpty ? nil : $0 } + + guard identifier != nil || label != nil else { + throw RPCMethodError.invalidParams( + "device.io.focus requires at least one of 'identifier' or 'label'" + ) + } + + let selectorDescription = Self.selectorDescription(identifier: identifier, label: label) + + guard let app = RunningApp.getForegroundApp() else { + throw RPCMethodError.internalError( + "device.io.focus: no foreground app to search for [\(selectorDescription)]" + ) + } + + let target = app.descendants(matching: .any) + .matching(Self.matchPredicate(identifier: identifier, label: label)) + .firstMatch + + guard target.exists else { + throw focusFailure( + selector: selectorDescription, + moves: 0, + lastFocused: focusedElement(in: app), + reason: "target element not found" + ) + } + + var lastFocused = focusedElement(in: app) + // Signature (identifier + frame) of the focused element captured just + // before the last press, used to detect a no-progress dead end. + var previousSignature: (identifier: String, frame: CGRect)? + var movesTaken = 0 + var noProgress = false + + for move in 0...Self.maxMoves { + movesTaken = move + guard let focused = focusedElement(in: app) else { + // No focusable element currently; nudge with a right press and retry. + if move < Self.maxMoves { + XCUIRemote.shared.press(.right) + continue + } + break + } + lastFocused = focused + + if matches(focused, identifier: identifier, label: label) { + logger.info("[Done] Focused [\(selectorDescription)] after \(move) move(s)") + return try serialize(focused) + } + + // No-progress early exit: if the last press did not change the + // focused element (same identifier + frame), we are at a dead end + // or oscillating — fail early instead of burning all 50 moves. + let currentSignature = (identifier: focused.identifier, frame: focused.frame) + if let previousSignature, + previousSignature.identifier == currentSignature.identifier, + previousSignature.frame == currentSignature.frame { + noProgress = true + break + } + + if move == Self.maxMoves { break } + + // Re-read the target's frame each iteration: on tvOS, moving focus + // scrolls the container and shifts the target's on-screen frame, so + // a centre computed once before the loop would be stale. + let targetCenter = center(of: target.frame) + let focusedCenter = center(of: currentSignature.frame) + let dx = targetCenter.x - focusedCenter.x + let dy = targetCenter.y - focusedCenter.y + let direction: XCUIRemote.Button + if abs(dx) > abs(dy) { + direction = dx > 0 ? .right : .left + } else { + direction = dy > 0 ? .down : .up + } + logger.info("Focus move \(move): pressing \(String(describing: direction)) toward [\(selectorDescription)]") + XCUIRemote.shared.press(direction) + previousSignature = currentSignature + } + + let reason = noProgress + ? "focus did not change after \(movesTaken) move(s) (dead end or oscillation)" + : "element not focused after \(Self.maxMoves) move(s)" + throw focusFailure( + selector: selectorDescription, + moves: noProgress ? movesTaken : Self.maxMoves, + lastFocused: lastFocused, + reason: reason + ) + } + + // MARK: - Helpers + + private func focusedElement(in app: XCUIApplication) -> XCUIElement? { + let focused = app.descendants(matching: .any) + .matching(NSPredicate(format: "hasFocus == true")) + .firstMatch + return focused.exists ? focused : nil + } + + private func matches(_ element: XCUIElement, identifier: String?, label: String?) -> Bool { + if let identifier, element.identifier == identifier { return true } + if let label, element.label == label { return true } + return false + } + + private func center(of frame: CGRect) -> CGPoint { + CGPoint(x: frame.midX, y: frame.midY) + } + + private func serialize(_ element: XCUIElement) throws -> JSONValue { + do { + let snapshotDictionary = try element.snapshot().dictionaryRepresentation + let sourceTree = SourceTreeElement(axElement: AXElement(snapshotDictionary)) + return try JSONValue.from(sourceTree) + } catch { + throw RPCMethodError.internalError( + "device.io.focus: failed to serialize element snapshot: \(error.localizedDescription)" + ) + } + } + + private func focusFailure( + selector: String, + moves: Int, + lastFocused: XCUIElement?, + reason: String + ) -> RPCMethodError { + var data: JSONValue? + if let lastFocused, lastFocused.exists { + // Wrapped in serialize(), which converts any snapshot/serialisation + // failure into an internalError; swallowed here so the failure path + // still returns the -32002 focusNotFound error with best-effort data. + data = try? serialize(lastFocused) + } + let message = "device.io.focus could not focus element matching [\(selector)] (\(reason))" + return RPCMethodError.focusNotFound(message: message, data: data) + } + + private static func matchPredicate(identifier: String?, label: String?) -> NSPredicate { + if let identifier, let label { + return NSPredicate(format: "identifier == %@ OR label == %@", identifier, label) + } else if let identifier { + return NSPredicate(format: "identifier == %@", identifier) + } else { + return NSPredicate(format: "label == %@", label ?? "") + } + } + + private static func selectorDescription(identifier: String?, label: String?) -> String { + var parts: [String] = [] + if let identifier { parts.append("identifier=\(identifier)") } + if let label { parts.append("label=\(label)") } + return parts.joined(separator: ", ") + } +} + +#endif diff --git a/DeviceKitTests/JSONRPC/Handlers/IOGesture.swift b/DeviceKitTests/JSONRPC/Handlers/IOGesture.swift index 1d41b41..24e0c05 100644 --- a/DeviceKitTests/JSONRPC/Handlers/IOGesture.swift +++ b/DeviceKitTests/JSONRPC/Handlers/IOGesture.swift @@ -1,3 +1,4 @@ +#if os(iOS) import Foundation import os @@ -226,3 +227,4 @@ struct IOGestureMethodHandler: RPCMethodHandler { logger.info("Finger \(fingerIndex): path completed, total duration \(currentOffset)s") } } +#endif diff --git a/DeviceKitTests/JSONRPC/Handlers/IOKeys.swift b/DeviceKitTests/JSONRPC/Handlers/IOKeys.swift index abf2480..225d863 100644 --- a/DeviceKitTests/JSONRPC/Handlers/IOKeys.swift +++ b/DeviceKitTests/JSONRPC/Handlers/IOKeys.swift @@ -1,3 +1,4 @@ +#if os(iOS) import XCTest import os @@ -126,3 +127,4 @@ struct IOKeysMethodHandler: RPCMethodHandler { return flags } } +#endif diff --git a/DeviceKitTests/JSONRPC/Handlers/IOLongpress.swift b/DeviceKitTests/JSONRPC/Handlers/IOLongpress.swift index 90d7195..7286d8d 100644 --- a/DeviceKitTests/JSONRPC/Handlers/IOLongpress.swift +++ b/DeviceKitTests/JSONRPC/Handlers/IOLongpress.swift @@ -1,3 +1,4 @@ +#if os(iOS) import os struct IOLongpressRequest: Codable { @@ -44,3 +45,4 @@ struct IOLongpressMethodHandler: RPCMethodHandler { } } } +#endif diff --git a/DeviceKitTests/JSONRPC/Handlers/IOOrientationGet.swift b/DeviceKitTests/JSONRPC/Handlers/IOOrientationGet.swift index 43aeb74..a4182b2 100644 --- a/DeviceKitTests/JSONRPC/Handlers/IOOrientationGet.swift +++ b/DeviceKitTests/JSONRPC/Handlers/IOOrientationGet.swift @@ -1,3 +1,4 @@ +#if os(iOS) import os @MainActor @@ -32,3 +33,4 @@ struct IOOrientationGetMethodHandler: RPCMethodHandler { return .object(["orientation": .string(value)]) } } +#endif diff --git a/DeviceKitTests/JSONRPC/Handlers/IOOrientationSet.swift b/DeviceKitTests/JSONRPC/Handlers/IOOrientationSet.swift index 8697e7d..47cc014 100644 --- a/DeviceKitTests/JSONRPC/Handlers/IOOrientationSet.swift +++ b/DeviceKitTests/JSONRPC/Handlers/IOOrientationSet.swift @@ -1,3 +1,4 @@ +#if os(iOS) import os struct IOOrientationSetRequest: Codable { @@ -35,3 +36,4 @@ struct IOOrientationSetMethodHandler: RPCMethodHandler { return .object(["success": .bool(true)]) } } +#endif diff --git a/DeviceKitTests/JSONRPC/Handlers/IOSwipe.swift b/DeviceKitTests/JSONRPC/Handlers/IOSwipe.swift index 9b2d63b..ed625bd 100644 --- a/DeviceKitTests/JSONRPC/Handlers/IOSwipe.swift +++ b/DeviceKitTests/JSONRPC/Handlers/IOSwipe.swift @@ -1,3 +1,4 @@ +#if os(iOS) import os private enum Constants { @@ -46,3 +47,4 @@ struct IOSwipeMethodHandler: RPCMethodHandler { try await RunnerDaemonProxy().synthesize(eventRecord: eventRecord) } } +#endif diff --git a/DeviceKitTests/JSONRPC/Handlers/IOTap.swift b/DeviceKitTests/JSONRPC/Handlers/IOTap.swift index 973928f..f1fb225 100644 --- a/DeviceKitTests/JSONRPC/Handlers/IOTap.swift +++ b/DeviceKitTests/JSONRPC/Handlers/IOTap.swift @@ -1,3 +1,4 @@ +#if os(iOS) import os struct IOTapRequest: Codable { @@ -42,3 +43,4 @@ struct IOTapMethodHandler: RPCMethodHandler { } } +#endif diff --git a/DeviceKitTests/JSONRPC/Handlers/IOText.swift b/DeviceKitTests/JSONRPC/Handlers/IOText.swift index fdf681d..c02d82d 100644 --- a/DeviceKitTests/JSONRPC/Handlers/IOText.swift +++ b/DeviceKitTests/JSONRPC/Handlers/IOText.swift @@ -1,3 +1,4 @@ +#if os(iOS) import XCTest import os @@ -88,3 +89,4 @@ struct IOTextMethodHandler: RPCMethodHandler { } } } +#endif diff --git a/DeviceKitTests/JSONRPC/JSONRPCDispatcher.swift b/DeviceKitTests/JSONRPC/JSONRPCDispatcher.swift index d70ce7d..e71683d 100644 --- a/DeviceKitTests/JSONRPC/JSONRPCDispatcher.swift +++ b/DeviceKitTests/JSONRPC/JSONRPCDispatcher.swift @@ -12,22 +12,33 @@ final class JSONRPCDispatcher { private var handlers: [String: any RPCMethodHandler] = [:] init() { - registerHandler(IOTapMethodHandler()) + // Shared handlers (available on both iOS and tvOS). registerHandler(DumpUIMethodHandler()) - registerHandler(IOTextMethodHandler()) - registerHandler(IOKeysMethodHandler()) registerHandler(AppsLaunchMethodHandler()) registerHandler(AppsTerminateMethodHandler()) - registerHandler(IOSwipeMethodHandler()) - registerHandler(IOLongpressMethodHandler()) registerHandler(ScreenshotMethodHandler()) registerHandler(URLMethodHandler()) - registerHandler(IOGestureMethodHandler()) registerHandler(IOButtonMethodHandler()) - registerHandler(IOOrientationGetMethodHandler()) - registerHandler(IOOrientationSetMethodHandler()) registerHandler(DeviceInfoMethodHandler()) registerHandler(AppsForegroundMethodHandler()) + + // Touch- and keyboard-driven input is iOS-only. tvOS uses focus-based + // remote navigation via the (shared) device.io.button handler. + #if os(iOS) + registerHandler(IOTapMethodHandler()) + registerHandler(IOTextMethodHandler()) + registerHandler(IOKeysMethodHandler()) + registerHandler(IOSwipeMethodHandler()) + registerHandler(IOLongpressMethodHandler()) + registerHandler(IOGestureMethodHandler()) + registerHandler(IOOrientationGetMethodHandler()) + registerHandler(IOOrientationSetMethodHandler()) + #endif + + // Focus-based navigation is tvOS-only (Siri Remote driven). + #if os(tvOS) + registerHandler(IOFocusMethodHandler()) + #endif } func registerHandler(_ handler: T) { diff --git a/DeviceKitTests/JSONRPC/JSONRPCModels.swift b/DeviceKitTests/JSONRPC/JSONRPCModels.swift index 581fe72..5c0563d 100644 --- a/DeviceKitTests/JSONRPC/JSONRPCModels.swift +++ b/DeviceKitTests/JSONRPC/JSONRPCModels.swift @@ -90,6 +90,10 @@ struct JSONRPCError: Codable { static func preconditionFailed(message: String) -> JSONRPCError { JSONRPCError(code: -32001, message: message) } + + static func focusNotFound(message: String, data: JSONValue? = nil) -> JSONRPCError { + JSONRPCError(code: -32002, message: message, data: data) + } } enum JSONValue: Codable, Equatable { diff --git a/DeviceKitTests/JSONRPC/RPCMethodHandler.swift b/DeviceKitTests/JSONRPC/RPCMethodHandler.swift index 0cf5d34..3bd732b 100644 --- a/DeviceKitTests/JSONRPC/RPCMethodHandler.swift +++ b/DeviceKitTests/JSONRPC/RPCMethodHandler.swift @@ -30,6 +30,7 @@ enum RPCMethodError: Error { case invalidParams(String) case internalError(String) case timeout(String) + case focusNotFound(message: String, data: JSONValue?) var jsonRPCError: JSONRPCError { switch self { @@ -39,6 +40,8 @@ enum RPCMethodError: Error { return .internalError(message: message) case .timeout(let message): return .timeout(message: message) + case .focusNotFound(let message, let data): + return .focusNotFound(message: message, data: data) } } } diff --git a/DeviceKitTests/XCTest/EventRecord.swift b/DeviceKitTests/XCTest/EventRecord.swift index 7bae489..b645c6f 100644 --- a/DeviceKitTests/XCTest/EventRecord.swift +++ b/DeviceKitTests/XCTest/EventRecord.swift @@ -1,3 +1,4 @@ +#if os(iOS) import Foundation import UIKit @@ -50,3 +51,4 @@ final class EventRecord: NSObject { return self } } +#endif diff --git a/DeviceKitTests/XCTest/OrientationGeometry.swift b/DeviceKitTests/XCTest/OrientationGeometry.swift index c6e6957..943e959 100644 --- a/DeviceKitTests/XCTest/OrientationGeometry.swift +++ b/DeviceKitTests/XCTest/OrientationGeometry.swift @@ -1,3 +1,4 @@ +#if os(iOS) import XCTest struct OrientationGeometry { @@ -100,3 +101,4 @@ struct OrientationGeometry { } } } +#endif diff --git a/DeviceKitTests/XCTest/PointerEventPath.swift b/DeviceKitTests/XCTest/PointerEventPath.swift index bd8b446..439532b 100644 --- a/DeviceKitTests/XCTest/PointerEventPath.swift +++ b/DeviceKitTests/XCTest/PointerEventPath.swift @@ -1,3 +1,4 @@ +#if os(iOS) import Foundation struct PointerEventPath { @@ -60,3 +61,4 @@ struct PointerEventPath { method(path, selector, text as NSString, offset, UInt64(typingSpeed), shouldRedact) } } +#endif diff --git a/DeviceKitTests/XCTest/RunnerDaemonProxy.swift b/DeviceKitTests/XCTest/RunnerDaemonProxy.swift index 6916632..cd9cc9e 100644 --- a/DeviceKitTests/XCTest/RunnerDaemonProxy.swift +++ b/DeviceKitTests/XCTest/RunnerDaemonProxy.swift @@ -1,3 +1,4 @@ +#if os(iOS) import Foundation @MainActor @@ -70,3 +71,4 @@ class RunnerDaemonProxy { } } } +#endif diff --git a/DeviceKitTests/XCTest/SystemPermissionManager.swift b/DeviceKitTests/XCTest/SystemPermissionManager.swift index be2c588..058ecb7 100644 --- a/DeviceKitTests/XCTest/SystemPermissionManager.swift +++ b/DeviceKitTests/XCTest/SystemPermissionManager.swift @@ -21,6 +21,7 @@ final class SystemPermissionManager { static func handleSystemPermissionAlertIfNeeded( foregroundApp: XCUIApplication ) { + #if os(iOS) let predicate = NSPredicate( format: "label CONTAINS[c] %@", notificationsPermissionLabel @@ -70,5 +71,6 @@ final class SystemPermissionManager { NSLog( "[Done] Foreground app is springboard attempting to tap on permissions dialog" ) + #endif } } diff --git a/DeviceKitTests/XCTestServer.swift b/DeviceKitTests/XCTestServer.swift index b896a85..f3df604 100644 --- a/DeviceKitTests/XCTestServer.swift +++ b/DeviceKitTests/XCTestServer.swift @@ -1,4 +1,5 @@ import FlyingFox +import FlyingSocks import Foundation import os @@ -100,7 +101,7 @@ final class XCTestServer { func start() async throws { let port = ProcessInfo.processInfo.environment["DEVICEKIT_LISTEN_PORT"]?.toUInt16() ?? defaultPort let server = HTTPServer( - address: try .inet(ip4: localhost, port: port), + address: try listenAddress(host: localhost, port: port), timeout: defaultTimeout ) @@ -134,5 +135,11 @@ final class XCTestServer { logger.info("Server is ready (WebSocket: ws://\(self.localhost):\(port)/ws, HTTP: POST http://\(self.localhost):\(port)/rpc, MJPEG: http://\(self.localhost):\(port)/mjpeg)") try await server.run() } -} + private func listenAddress(host: String, port: UInt16) throws -> any SocketAddress { + if host.contains(":") { + return try sockaddr_in6.inet6(ip6: host, port: port) + } + return try sockaddr_in.inet(ip4: host, port: port) + } +} diff --git a/DeviceKitTests/devicekit_iosUITests.swift b/DeviceKitTests/devicekit_iosUITests.swift index d035e19..9ebbc5f 100644 --- a/DeviceKitTests/devicekit_iosUITests.swift +++ b/DeviceKitTests/devicekit_iosUITests.swift @@ -1,4 +1,3 @@ -import FlyingFox import XCTest import os diff --git a/Makefile b/Makefile index 3bd73c1..406bdd0 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ CODE_SIGN_IDENTITY ?= Apple Development # Export method for IPA (development, ad-hoc, app-store, enterprise) EXPORT_METHOD ?= development -.PHONY: help clean build archive ipa-unsigned sim-zip-arm64 sim-zip-x86_64 sim-zip sim-install test-coverage coverage-html lint +.PHONY: help clean build archive ipa-unsigned tvos-ipa-unsigned sim-zip-arm64 sim-zip-x86_64 sim-zip sim-install tvos-sim-zip-arm64 tvos-sim-install test-coverage coverage-html lint .DEFAULT_GOAL := help @@ -25,7 +25,10 @@ help: @echo " sim-zip-arm64 Build XCUITest runner zip for iOS Simulator (arm64 / Apple Silicon)" @echo " sim-zip-x86_64 Build XCUITest runner zip for iOS Simulator (x86_64 / Intel)" @echo " sim-install Build and install on the currently booted simulator" + @echo " tvos-sim-zip-arm64 Build XCUITest runner zip for tvOS Simulator (arm64 / Apple Silicon)" + @echo " tvos-sim-install Build and install the tvOS runner on the booted tvOS simulator" @echo " ipa-unsigned Build unsigned IPA with XCUITest runner for real iOS devices" + @echo " tvos-ipa-unsigned Build unsigned IPA with XCUITest runner for real Apple TV devices" @echo " debug Build with Debug configuration" @echo " release Build with Release configuration" @echo " clean Remove build artifacts" @@ -109,6 +112,65 @@ sim-zip-x86_64: # Build both simulator zips sim-zip: sim-zip-arm64 sim-zip-x86_64 +# Build XCUITest runner for tvOS Simulator (arm64 — Apple Silicon) +tvos-sim-zip-arm64: + @echo "Building devicekit-tvos XCUITest runner for tvOS Simulator (arm64)..." + xcodebuild build-for-testing \ + -project $(PROJECT) \ + -scheme devicekit-tvos \ + -configuration $(CONFIGURATION) \ + -destination 'generic/platform=tvOS Simulator' \ + -derivedDataPath $(BUILD_DIR) \ + CODE_SIGN_IDENTITY="" \ + CODE_SIGNING_REQUIRED=NO \ + CODE_SIGNING_ALLOWED=NO \ + ARCHS=arm64 | xcbeautify + @mkdir -p $(EXPORT_PATH) + @cp -r "$(BUILD_DIR)/Build/Products/$(CONFIGURATION)-appletvsimulator/devicekit-tvosUITests-Runner.app" $(EXPORT_PATH)/ + @scripts/patch-tvos-runner.sh "$(EXPORT_PATH)/devicekit-tvosUITests-Runner.app" + @codesign --force --sign - "$(EXPORT_PATH)/devicekit-tvosUITests-Runner.app" + @cd $(EXPORT_PATH) && zip -r devicekit-tvos-Sim-arm64.zip devicekit-tvosUITests-Runner.app + @rm -rf "$(EXPORT_PATH)/devicekit-tvosUITests-Runner.app" + @echo "Simulator zip created at: $(EXPORT_PATH)/devicekit-tvos-Sim-arm64.zip" + +# Create unsigned IPA with XCUITest runner for real Apple TV devices +tvos-ipa-unsigned: + @echo "Building unsigned tvOS test runner for arm64 Apple TV devices..." + xcodebuild build-for-testing \ + -project $(PROJECT) \ + -scheme devicekit-tvos \ + -configuration $(CONFIGURATION) \ + -destination 'generic/platform=tvOS' \ + -derivedDataPath $(BUILD_DIR) \ + CODE_SIGN_IDENTITY="" \ + CODE_SIGNING_REQUIRED=NO \ + CODE_SIGNING_ALLOWED=NO | xcbeautify + @scripts/patch-tvos-runner.sh "$(BUILD_DIR)/Build/Products/$(CONFIGURATION)-appletvos/devicekit-tvosUITests-Runner.app" device + @echo "Packaging tvOS runner IPA..." + @rm -rf $(EXPORT_PATH)/Payload + @rm -f $(EXPORT_PATH)/devicekit-tvos-runner.ipa + @mkdir -p $(EXPORT_PATH)/Payload + @cp -r "$(BUILD_DIR)/Build/Products/$(CONFIGURATION)-appletvos/devicekit-tvosUITests-Runner.app" $(EXPORT_PATH)/Payload/ + @cd $(EXPORT_PATH) && zip -r devicekit-tvos-runner.ipa Payload + @rm -rf $(EXPORT_PATH)/Payload + @echo "Runner IPA created at: $(EXPORT_PATH)/devicekit-tvos-runner.ipa" + +# Build and install tvOS runner on the booted tvOS simulator +tvos-sim-install: + @BOOTED=$$(xcrun simctl list devices booted -j | jq -r '[.devices[][] | select(.state=="Booted")] | first | .udid'); \ + echo "Building tvOS runner for simulator $$BOOTED..."; \ + xcodebuild build-for-testing \ + -project $(PROJECT) \ + -scheme devicekit-tvos \ + -configuration $(CONFIGURATION) \ + -destination "id=$$BOOTED" \ + -derivedDataPath $(BUILD_DIR)/local-tvos | xcbeautify; \ + PRODUCTS="$(BUILD_DIR)/local-tvos/Build/Products/$(CONFIGURATION)-appletvsimulator"; \ + scripts/patch-tvos-runner.sh "$$PRODUCTS/devicekit-tvosUITests-Runner.app"; \ + codesign --force --sign - "$$PRODUCTS/devicekit-tvosUITests-Runner.app"; \ + xcrun simctl install "$$BOOTED" "$$PRODUCTS/devicekit-tvosUITests-Runner.app"; \ + echo "Installed tvOS runner on simulator $$BOOTED" + # Build and install on booted simulator sim-install: @BOOTED=$$(xcrun simctl list devices booted -j | jq -r '[.devices[][] | select(.state=="Booted")] | first | .udid'); \ diff --git a/README.md b/README.md index 7e9c61a..0909f73 100644 --- a/README.md +++ b/README.md @@ -64,9 +64,11 @@ make sim-zip | Target | Output | Description | |--------|--------|-------------| | `make ipa-unsigned` | `build/export/devicekit-ios-unsigned.ipa` | Unsigned IPA for arm64 devices | +| `make tvos-ipa-unsigned` | `build/export/devicekit-tvos-runner.ipa` | Unsigned IPA for arm64 Apple TV devices | | `make sim-zip-arm64` | `build/export/devicekit-ios-Sim-arm64.zip` | Simulator runner (Apple Silicon) | | `make sim-zip-x86_64` | `build/export/devicekit-ios-Sim-x86_64.zip` | Simulator runner (Intel) | | `make sim-zip` | Both simulator zips | arm64 + x86_64 | +| `make tvos-sim-zip-arm64` | `build/export/devicekit-tvos-Sim-arm64.zip` | tvOS Simulator runner (Apple Silicon) | | `make lint` | — | Run SwiftLint | | `make clean` | — | Remove build artifacts | diff --git a/devicekit-ios.xcodeproj/project.pbxproj b/devicekit-ios.xcodeproj/project.pbxproj index 7cde7c2..ecff0a3 100644 --- a/devicekit-ios.xcodeproj/project.pbxproj +++ b/devicekit-ios.xcodeproj/project.pbxproj @@ -7,6 +7,29 @@ objects = { /* Begin PBXBuildFile section */ + 00C48C967AE3D00B221ADC02 /* FBScreenshot.h in Headers */ = {isa = PBXBuildFile; fileRef = 785281472F2BEC40002A7531 /* FBScreenshot.h */; }; + 0161FB34A40138543E558E15 /* XCAXClient_iOS.h in Headers */ = {isa = PBXBuildFile; fileRef = 52D8FA402D6DCC600015FAB3 /* XCAXClient_iOS.h */; }; + 034CAE017DBB0653A564F523 /* XCUIApplication+Helper.m in Sources */ = {isa = PBXBuildFile; fileRef = 521C1F0A2D801A350024EE42 /* XCUIApplication+Helper.m */; }; + 05DB44E2FA2B136BEA2F0B3C /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BEB7B3F9D0E9FC3377B3F800 /* Foundation.framework */; }; + 0A405966B03B19257EE892F2 /* XCUIApplication+FBQuiescence.m in Sources */ = {isa = PBXBuildFile; fileRef = 52D8FAB32D6DD54F0015FAB3 /* XCUIApplication+FBQuiescence.m */; }; + 122FABEEC00A0F17E4F04811 /* FBLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 52D8FAAB2D6DD3A20015FAB3 /* FBLogger.m */; }; + 1941E2F4FA085893EC1177EF /* XCUIApplication+Helper.h in Headers */ = {isa = PBXBuildFile; fileRef = 521C1F082D80184D0024EE42 /* XCUIApplication+Helper.h */; }; + 1DCF3F02252DC8BB0A150045 /* devicekit-iosUITests-Bridging-Header.h in Headers */ = {isa = PBXBuildFile; fileRef = 52D8FA922D6DCF2C0015FAB3 /* devicekit-iosUITests-Bridging-Header.h */; }; + 22704AE8F9FB64145D90DF72 /* DeviceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7865E4C62F423ACA00D0F45F /* DeviceInfo.swift */; }; + 235F49B2463019B3FBAB9B01 /* IOOrientationGet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7865E4C22F423A8700D0F45F /* IOOrientationGet.swift */; }; + 244DD46D3E8F596EB3BFFCD8 /* JSONRPCDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 787C47922F227F6B0027A012 /* JSONRPCDispatcher.swift */; }; + 259CD442315E5B812702C64B /* JSONRPCModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 787C47932F227F6B0027A012 /* JSONRPCModels.swift */; }; + 286C8DE34DA6FDEE5643630F /* AppsTerminate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EA9F902F36611B008B17D3 /* AppsTerminate.swift */; }; + 28984ED83F9513779C061381 /* CDStructures.h in Headers */ = {isa = PBXBuildFile; fileRef = 52D8FA332D6DCC600015FAB3 /* CDStructures.h */; }; + 2F386FCD920B2C009F1A6BBC /* XCAXClient_iOS+FBSnapshotReqParams.h in Headers */ = {isa = PBXBuildFile; fileRef = 526111772DE5E8D0007C356B /* XCAXClient_iOS+FBSnapshotReqParams.h */; }; + 30151A4111977439D0303164 /* IOSwipe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78147FCA2F27C03500E61E3B /* IOSwipe.swift */; }; + 316F0F773353E7A652D56807 /* EventRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 612DE413298426EF003C2BE0 /* EventRecord.swift */; }; + 355EC369900195D9605B763C /* XCAccessibilityElement.h in Headers */ = {isa = PBXBuildFile; fileRef = 521C1F042D8016950024EE42 /* XCAccessibilityElement.h */; }; + 3CE3C8A2ABF9D07AD61EEA73 /* FBScreenshot.m in Sources */ = {isa = PBXBuildFile; fileRef = 785281482F2BEC40002A7531 /* FBScreenshot.m */; }; + 3EA30189877288CD244B9A0B /* XCTestManager_ManagerInterface-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 52D8FA622D6DCC600015FAB3 /* XCTestManager_ManagerInterface-Protocol.h */; }; + 4567136EF51F97DF02F7009A /* AXElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6124329D2A4DA5BB00F5F619 /* AXElement.swift */; }; + 4DC8EC2D308A07DFAD64F3EB /* Screenshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781480362F27CEF000E61E3B /* Screenshot.swift */; }; + 4EB43A87FBDB74F092EE24E6 /* DumpUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 787C47DC2F228B770027A012 /* DumpUI.swift */; }; 52049BF8293503A200807AA3 /* devicekit_iosUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52049BF7293503A200807AA3 /* devicekit_iosUITests.swift */; }; 521C1F032D8014410024EE42 /* AXClientProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = 521C1F022D8014340024EE42 /* AXClientProxy.h */; }; 521C1F052D8016950024EE42 /* XCAccessibilityElement.h in Headers */ = {isa = PBXBuildFile; fileRef = 521C1F042D8016950024EE42 /* XCAccessibilityElement.h */; }; @@ -35,14 +58,17 @@ 52D8FAB42D6DD54F0015FAB3 /* XCUIApplication+FBQuiescence.m in Sources */ = {isa = PBXBuildFile; fileRef = 52D8FAB32D6DD54F0015FAB3 /* XCUIApplication+FBQuiescence.m */; }; 52D8FAB52D6DD54F0015FAB3 /* XCUIApplication+FBQuiescence.h in Headers */ = {isa = PBXBuildFile; fileRef = 52D8FAB22D6DD54F0015FAB3 /* XCUIApplication+FBQuiescence.h */; }; 52E35D432A654F67001D97A8 /* RunningApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52E35D422A654F67001D97A8 /* RunningApp.swift */; }; + 54BD87EBA4EBCC2897C7B08F /* IOLongpress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7814800A2F27C55500E61E3B /* IOLongpress.swift */; }; + 56AD8C619AFABA24BB9BF726 /* XCTestServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 787C47952F227F6B0027A012 /* XCTestServer.swift */; }; 610D58FB2A49E3CA00B4BBEB /* AXClientSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 610D58FA2A49E3CA00B4BBEB /* AXClientSwizzler.swift */; }; 6124329E2A4DA5BB00F5F619 /* AXElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6124329D2A4DA5BB00F5F619 /* AXElement.swift */; }; 612DE40229801F71003C2BE0 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 612DE40129801F71003C2BE0 /* XCTest.framework */; }; 612DE4122984114B003C2BE0 /* RunnerDaemonProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 612DE4112984114B003C2BE0 /* RunnerDaemonProxy.swift */; }; 612DE414298426EF003C2BE0 /* EventRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 612DE413298426EF003C2BE0 /* EventRecord.swift */; }; 613E87D7299A64BD00FF8551 /* PointerEventPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613E87D6299A64BD00FF8551 /* PointerEventPath.swift */; }; + 624D02949107BFAA1A01CE29 /* XCUIApplicationImpl.h in Headers */ = {isa = PBXBuildFile; fileRef = 52D8FA7E2D6DCC600015FAB3 /* XCUIApplicationImpl.h */; }; + 7270F40D1DD94D314AD9556F /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781480742F27D2B900E61E3B /* URL.swift */; }; 78147F0F2F27AA6B00E61E3B /* IOText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78147F0E2F27AA6B00E61E3B /* IOText.swift */; }; - 78E4A1B12F5C110200D4F1AA /* IOKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78E4A1B02F5C110200D4F1AA /* IOKeys.swift */; }; 78147F3D2F27BDBC00E61E3B /* AppsLaunch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78147F3C2F27BDBC00E61E3B /* AppsLaunch.swift */; }; 78147FCB2F27C04400E61E3B /* IOSwipe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78147FCA2F27C03500E61E3B /* IOSwipe.swift */; }; 7814800B2F27C56100E61E3B /* IOLongpress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7814800A2F27C55500E61E3B /* IOLongpress.swift */; }; @@ -65,11 +91,49 @@ 787C48482F228D960027A012 /* JSONRPCMessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 787C48472F228D950027A012 /* JSONRPCMessageHandler.swift */; }; 787C95982F17F23500AB3BF1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 787C95972F17F23500AB3BF1 /* Assets.xcassets */; }; 78A190B62F3A3ADA007F24E0 /* IOButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78A190B52F3A3ADA007F24E0 /* IOButton.swift */; }; + B10F0C05E0000000000A0002 /* IOFocus.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10F0C05E0000000000A0001 /* IOFocus.swift */; }; + 78E4A1B12F5C110200D4F1AA /* IOKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78E4A1B02F5C110200D4F1AA /* IOKeys.swift */; }; 78EA9F912F36611B008B17D3 /* AppsTerminate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EA9F902F36611B008B17D3 /* AppsTerminate.swift */; }; 78F689392F17EF1B0025C8AC /* devicekit_iosApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78F689372F17EF1B0025C8AC /* devicekit_iosApp.swift */; }; + 7D450004153BD0CAA803391A /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79019D07340032631918358D /* XCTest.framework */; }; + 884A0247F2AFAC2C8E289FBF /* FBLogger.h in Headers */ = {isa = PBXBuildFile; fileRef = 52D8FAAA2D6DD3A20015FAB3 /* FBLogger.h */; }; + 8E8BBE0BBBB73B8074357641 /* IOOrientationSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7865E4C32F423A8700D0F45F /* IOOrientationSet.swift */; }; + 938D7BDFE204AC0B8AC3E1CF /* OrientationGeometry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9811C9082C49751D00DDACA0 /* OrientationGeometry.swift */; }; 94826F4429DB082100795E9B /* SystemPermissionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94826F4329DB082100795E9B /* SystemPermissionManager.swift */; }; 9811C9092C49751D00DDACA0 /* OrientationGeometry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9811C9082C49751D00DDACA0 /* OrientationGeometry.swift */; }; + 9B63CAE92B8CF4FBBA5140A5 /* XCTestDaemonsProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = 521C1F0D2D8051890024EE42 /* XCTestDaemonsProxy.h */; }; + 9BB1385B2D6CB97B5E1DF262 /* XCUIDevice.h in Headers */ = {isa = PBXBuildFile; fileRef = 52D8FA812D6DCC600015FAB3 /* XCUIDevice.h */; }; + 9C765909539AE04A217DD1F4 /* AppsLaunch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78147F3C2F27BDBC00E61E3B /* AppsLaunch.swift */; }; + A00D5AB8F6E0F5CE06377310 /* AXClientSwizzler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 610D58FA2A49E3CA00B4BBEB /* AXClientSwizzler.swift */; }; + A1A0950E2FF49EEB56A7931A /* XCUIApplicationProcess.h in Headers */ = {isa = PBXBuildFile; fileRef = 52D8FA7F2D6DCC600015FAB3 /* XCUIApplicationProcess.h */; }; + A543801BC12FCCAE0623EAD8 /* AXClientProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = 521C1F022D8014340024EE42 /* AXClientProxy.h */; }; + A6E5ACA822551BCC70942D55 /* JSONRPCHTTPHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 787C48452F228D8C0027A012 /* JSONRPCHTTPHandler.swift */; }; + A7F7596026F95FBD8297CC7F /* AppsForeground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7865E50E2F42596200D0F45F /* AppsForeground.swift */; }; AAAA00022F82000100000001 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AAAA00012F82000100000001 /* Assets.xcassets */; }; + AC75A950D43275503666ECC8 /* devicekit_iosUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52049BF7293503A200807AA3 /* devicekit_iosUITests.swift */; }; + AD3EADC830FE8CCA1585818B /* FBConfiguration.h in Headers */ = {isa = PBXBuildFile; fileRef = 52D8FAAE2D6DD4100015FAB3 /* FBConfiguration.h */; }; + B073667315FB4D9E4332C5AE /* XCAXClient_iOS+FBSnapshotReqParams.m in Sources */ = {isa = PBXBuildFile; fileRef = 526111792DE5E8E5007C356B /* XCAXClient_iOS+FBSnapshotReqParams.m */; }; + B3E24A58415777E32C267B79 /* IOTap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 787C47DD2F228B770027A012 /* IOTap.swift */; }; + B636674E8B56967896BD08C6 /* IOGesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781480FE2F27DEE600E61E3B /* IOGesture.swift */; }; + B8FBFB840AABD10B05DEF0E8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AAAA00012F82000100000001 /* Assets.xcassets */; }; + B95556082A6382C6D0A9C3EC /* IOText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78147F0E2F27AA6B00E61E3B /* IOText.swift */; }; + C7330BE333EB2E786B2D9C74 /* JSONRPCMessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 787C48472F228D950027A012 /* JSONRPCMessageHandler.swift */; }; + CF4FB3A7202D31A03341B7B3 /* XCUIApplicationProcess+FBQuiescence.m in Sources */ = {isa = PBXBuildFile; fileRef = 52D8FA9C2D6DCFAF0015FAB3 /* XCUIApplicationProcess+FBQuiescence.m */; }; + D7B293D07B4CB605C14C044C /* AXClientProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = 521C1F062D8017460024EE42 /* AXClientProxy.m */; }; + D7DA3A8AF6A06A325423F66E /* FBConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = 52D8FAAF2D6DD4100015FAB3 /* FBConfiguration.m */; }; + DAC965FEC9FB0DA876F81C2F /* RPCMethodHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 787C47942F227F6B0027A012 /* RPCMethodHandler.swift */; }; + DBC6B81C12180306B0D14C54 /* XCUIApplication+FBQuiescence.h in Headers */ = {isa = PBXBuildFile; fileRef = 52D8FAB22D6DD54F0015FAB3 /* XCUIApplication+FBQuiescence.h */; }; + DCF95ADBF618D3E7DEAC31EC /* RunningApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52E35D422A654F67001D97A8 /* RunningApp.swift */; }; + DFD8C5E67F731688B7AD8276 /* XCUIApplication.h in Headers */ = {isa = PBXBuildFile; fileRef = 52D8FA7D2D6DCC600015FAB3 /* XCUIApplication.h */; }; + E2433B8447C892108A11FC74 /* RunnerDaemonProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 612DE4112984114B003C2BE0 /* RunnerDaemonProxy.swift */; }; + EC7C56E5A57A24F6310F1A09 /* XCUIApplicationProcess+FBQuiescence.h in Headers */ = {isa = PBXBuildFile; fileRef = 52D8FA9B2D6DCFAF0015FAB3 /* XCUIApplicationProcess+FBQuiescence.h */; }; + EC9B50B9764C1544B5B91FD2 /* SystemPermissionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94826F4329DB082100795E9B /* SystemPermissionManager.swift */; }; + ECDFA922C0117DEBE58D1F69 /* XCTestDaemonsProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = 521C1F0F2D8051F20024EE42 /* XCTestDaemonsProxy.m */; }; + F0881FEA5B0136EB00959663 /* IOKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78E4A1B02F5C110200D4F1AA /* IOKeys.swift */; }; + F696916E157EB09BDBCF5132 /* FlyingFox in Frameworks */ = {isa = PBXBuildFile; productRef = EBF28227FF61B70D19DDE281 /* FlyingFox */; }; + FE73645026930316D712AFBB /* IOButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78A190B52F3A3ADA007F24E0 /* IOButton.swift */; }; + B10F0C05E0000000000A0003 /* IOFocus.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10F0C05E0000000000A0001 /* IOFocus.swift */; }; + FF8B283A8DF6D51B1A7E4CD0 /* PointerEventPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 613E87D6299A64BD00FF8551 /* PointerEventPath.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -83,6 +147,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 06DB3D932EBB93028B539685 /* devicekit-tvosUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "devicekit-tvosUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 52049BD32935039F00807AA3 /* devicekit-ios.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "devicekit-ios.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 52049BF3293503A200807AA3 /* devicekit-iosUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "devicekit-iosUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 52049BF7293503A200807AA3 /* devicekit_iosUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = devicekit_iosUITests.swift; sourceTree = ""; }; @@ -125,7 +190,6 @@ 612DE413298426EF003C2BE0 /* EventRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventRecord.swift; sourceTree = ""; }; 613E87D6299A64BD00FF8551 /* PointerEventPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointerEventPath.swift; sourceTree = ""; }; 78147F0E2F27AA6B00E61E3B /* IOText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IOText.swift; sourceTree = ""; }; - 78E4A1B02F5C110200D4F1AA /* IOKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IOKeys.swift; sourceTree = ""; }; 78147F3C2F27BDBC00E61E3B /* AppsLaunch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppsLaunch.swift; sourceTree = ""; }; 78147FCA2F27C03500E61E3B /* IOSwipe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IOSwipe.swift; sourceTree = ""; }; 7814800A2F27C55500E61E3B /* IOLongpress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IOLongpress.swift; sourceTree = ""; }; @@ -148,17 +212,41 @@ 787C48472F228D950027A012 /* JSONRPCMessageHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONRPCMessageHandler.swift; sourceTree = ""; }; 787C95972F17F23500AB3BF1 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 78A190B52F3A3ADA007F24E0 /* IOButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IOButton.swift; sourceTree = ""; }; + B10F0C05E0000000000A0001 /* IOFocus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IOFocus.swift; sourceTree = ""; }; + 78E4A1B02F5C110200D4F1AA /* IOKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IOKeys.swift; sourceTree = ""; }; 78EA9F902F36611B008B17D3 /* AppsTerminate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppsTerminate.swift; sourceTree = ""; }; 78F689132F17EE190025C8AC /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; 78F689372F17EF1B0025C8AC /* devicekit_iosApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = devicekit_iosApp.swift; sourceTree = ""; }; + 79019D07340032631918358D /* XCTest.framework */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/AppleTVOS.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; 94826F4329DB082100795E9B /* SystemPermissionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemPermissionManager.swift; sourceTree = ""; }; 9811C9082C49751D00DDACA0 /* OrientationGeometry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrientationGeometry.swift; sourceTree = ""; }; AAAA00012F82000100000001 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + BEB7B3F9D0E9FC3377B3F800 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS18.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ - 787C95D52F17F59E00AB3BF1 /* View */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = View; sourceTree = ""; }; - 78C54BD12F2D67E300A9369C /* Streamer */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Streamer; sourceTree = ""; }; + 787C95D52F17F59E00AB3BF1 /* View */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = View; + sourceTree = ""; + }; + 78C54BD12F2D67E300A9369C /* Streamer */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = Streamer; + sourceTree = ""; + }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -178,6 +266,16 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 8A77574C67E658C83CA4A70C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 05DB44E2FA2B136BEA2F0B3C /* Foundation.framework in Frameworks */, + 7D450004153BD0CAA803391A /* XCTest.framework in Frameworks */, + F696916E157EB09BDBCF5132 /* FlyingFox in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -196,6 +294,7 @@ children = ( 52049BD32935039F00807AA3 /* devicekit-ios.app */, 52049BF3293503A200807AA3 /* devicekit-iosUITests.xctest */, + 06DB3D932EBB93028B539685 /* devicekit-tvosUITests.xctest */, ); name = Products; sourceTree = ""; @@ -231,6 +330,7 @@ children = ( 612DE40129801F71003C2BE0 /* XCTest.framework */, 78F689132F17EE190025C8AC /* UIKit.framework */, + A8FA10DE933F63445BAE2595 /* tvOS */, ); name = Frameworks; sourceTree = ""; @@ -336,6 +436,7 @@ 781480362F27CEF000E61E3B /* Screenshot.swift */, 7814800A2F27C55500E61E3B /* IOLongpress.swift */, 78A190B52F3A3ADA007F24E0 /* IOButton.swift */, + B10F0C05E0000000000A0001 /* IOFocus.swift */, 78147FCA2F27C03500E61E3B /* IOSwipe.swift */, 7865E50E2F42596200D0F45F /* AppsForeground.swift */, 78147F3C2F27BDBC00E61E3B /* AppsLaunch.swift */, @@ -348,6 +449,14 @@ path = Handlers; sourceTree = ""; }; + A8FA10DE933F63445BAE2595 /* tvOS */ = { + isa = PBXGroup; + children = ( + BEB7B3F9D0E9FC3377B3F800 /* Foundation.framework */, + ); + name = tvOS; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -376,9 +485,58 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + A2691B44CC31F76D90452444 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 624D02949107BFAA1A01CE29 /* XCUIApplicationImpl.h in Headers */, + 00C48C967AE3D00B221ADC02 /* FBScreenshot.h in Headers */, + DBC6B81C12180306B0D14C54 /* XCUIApplication+FBQuiescence.h in Headers */, + A1A0950E2FF49EEB56A7931A /* XCUIApplicationProcess.h in Headers */, + 9BB1385B2D6CB97B5E1DF262 /* XCUIDevice.h in Headers */, + DFD8C5E67F731688B7AD8276 /* XCUIApplication.h in Headers */, + 2F386FCD920B2C009F1A6BBC /* XCAXClient_iOS+FBSnapshotReqParams.h in Headers */, + 1941E2F4FA085893EC1177EF /* XCUIApplication+Helper.h in Headers */, + 884A0247F2AFAC2C8E289FBF /* FBLogger.h in Headers */, + 3EA30189877288CD244B9A0B /* XCTestManager_ManagerInterface-Protocol.h in Headers */, + AD3EADC830FE8CCA1585818B /* FBConfiguration.h in Headers */, + 0161FB34A40138543E558E15 /* XCAXClient_iOS.h in Headers */, + 9B63CAE92B8CF4FBBA5140A5 /* XCTestDaemonsProxy.h in Headers */, + 355EC369900195D9605B763C /* XCAccessibilityElement.h in Headers */, + 28984ED83F9513779C061381 /* CDStructures.h in Headers */, + 1DCF3F02252DC8BB0A150045 /* devicekit-iosUITests-Bridging-Header.h in Headers */, + EC7C56E5A57A24F6310F1A09 /* XCUIApplicationProcess+FBQuiescence.h in Headers */, + A543801BC12FCCAE0623EAD8 /* AXClientProxy.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ + 297871A835D8238B41FA004B /* devicekit-tvosUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = CECC47FDB7287DB2B421819E /* Build configuration list for PBXNativeTarget "devicekit-tvosUITests" */; + buildPhases = ( + A2691B44CC31F76D90452444 /* Headers */, + F73CF66FF777D0663EBA7715 /* Sources */, + 8A77574C67E658C83CA4A70C /* Frameworks */, + 57D5ECFBBD4579E6247685AB /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 78C54BD12F2D67E300A9369C /* Streamer */, + ); + name = "devicekit-tvosUITests"; + packageProductDependencies = ( + EBF28227FF61B70D19DDE281 /* FlyingFox */, + ); + productName = "devicekit-tvosUITests"; + productReference = 06DB3D932EBB93028B539685 /* devicekit-tvosUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; 52049BD22935039F00807AA3 /* devicekit-ios */ = { isa = PBXNativeTarget; buildConfigurationList = 52049BFD293503A200807AA3 /* Build configuration list for PBXNativeTarget "devicekit-ios" */; @@ -395,8 +553,6 @@ 787C95D52F17F59E00AB3BF1 /* View */, ); name = "devicekit-ios"; - packageProductDependencies = ( - ); productName = "maestro-driver-ios"; productReference = 52049BD32935039F00807AA3 /* devicekit-ios.app */; productType = "com.apple.product-type.application"; @@ -464,6 +620,7 @@ targets = ( 52049BD22935039F00807AA3 /* devicekit-ios */, 52049BF2293503A200807AA3 /* devicekit-iosUITests */, + 297871A835D8238B41FA004B /* devicekit-tvosUITests */, ); }; /* End PBXProject section */ @@ -485,6 +642,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 57D5ECFBBD4579E6247685AB /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B8FBFB840AABD10B05DEF0E8 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -516,6 +681,7 @@ 787C47E02F228B770027A012 /* DumpUI.swift in Sources */, 521C1F102D8051FC0024EE42 /* XCTestDaemonsProxy.m in Sources */, 78A190B62F3A3ADA007F24E0 /* IOButton.swift in Sources */, + B10F0C05E0000000000A0002 /* IOFocus.swift in Sources */, 613E87D7299A64BD00FF8551 /* PointerEventPath.swift in Sources */, 78147FCB2F27C04400E61E3B /* IOSwipe.swift in Sources */, 787C48482F228D960027A012 /* JSONRPCMessageHandler.swift in Sources */, @@ -543,6 +709,54 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + F73CF66FF777D0663EBA7715 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A6E5ACA822551BCC70942D55 /* JSONRPCHTTPHandler.swift in Sources */, + B636674E8B56967896BD08C6 /* IOGesture.swift in Sources */, + E2433B8447C892108A11FC74 /* RunnerDaemonProxy.swift in Sources */, + EC9B50B9764C1544B5B91FD2 /* SystemPermissionManager.swift in Sources */, + 122FABEEC00A0F17E4F04811 /* FBLogger.m in Sources */, + 3CE3C8A2ABF9D07AD61EEA73 /* FBScreenshot.m in Sources */, + A7F7596026F95FBD8297CC7F /* AppsForeground.swift in Sources */, + D7B293D07B4CB605C14C044C /* AXClientProxy.m in Sources */, + 22704AE8F9FB64145D90DF72 /* DeviceInfo.swift in Sources */, + B073667315FB4D9E4332C5AE /* XCAXClient_iOS+FBSnapshotReqParams.m in Sources */, + 9C765909539AE04A217DD1F4 /* AppsLaunch.swift in Sources */, + A00D5AB8F6E0F5CE06377310 /* AXClientSwizzler.swift in Sources */, + B3E24A58415777E32C267B79 /* IOTap.swift in Sources */, + 4EB43A87FBDB74F092EE24E6 /* DumpUI.swift in Sources */, + ECDFA922C0117DEBE58D1F69 /* XCTestDaemonsProxy.m in Sources */, + FE73645026930316D712AFBB /* IOButton.swift in Sources */, + B10F0C05E0000000000A0003 /* IOFocus.swift in Sources */, + FF8B283A8DF6D51B1A7E4CD0 /* PointerEventPath.swift in Sources */, + 30151A4111977439D0303164 /* IOSwipe.swift in Sources */, + C7330BE333EB2E786B2D9C74 /* JSONRPCMessageHandler.swift in Sources */, + 938D7BDFE204AC0B8AC3E1CF /* OrientationGeometry.swift in Sources */, + 316F0F773353E7A652D56807 /* EventRecord.swift in Sources */, + 259CD442315E5B812702C64B /* JSONRPCModels.swift in Sources */, + 8E8BBE0BBBB73B8074357641 /* IOOrientationSet.swift in Sources */, + 235F49B2463019B3FBAB9B01 /* IOOrientationGet.swift in Sources */, + DAC965FEC9FB0DA876F81C2F /* RPCMethodHandler.swift in Sources */, + 54BD87EBA4EBCC2897C7B08F /* IOLongpress.swift in Sources */, + 4DC8EC2D308A07DFAD64F3EB /* Screenshot.swift in Sources */, + 56AD8C619AFABA24BB9BF726 /* XCTestServer.swift in Sources */, + 7270F40D1DD94D314AD9556F /* URL.swift in Sources */, + 244DD46D3E8F596EB3BFFCD8 /* JSONRPCDispatcher.swift in Sources */, + 034CAE017DBB0653A564F523 /* XCUIApplication+Helper.m in Sources */, + CF4FB3A7202D31A03341B7B3 /* XCUIApplicationProcess+FBQuiescence.m in Sources */, + AC75A950D43275503666ECC8 /* devicekit_iosUITests.swift in Sources */, + 286C8DE34DA6FDEE5643630F /* AppsTerminate.swift in Sources */, + D7DA3A8AF6A06A325423F66E /* FBConfiguration.m in Sources */, + B95556082A6382C6D0A9C3EC /* IOText.swift in Sources */, + F0881FEA5B0136EB00959663 /* IOKeys.swift in Sources */, + 4567136EF51F97DF02F7009A /* AXElement.swift in Sources */, + DCF95ADBF618D3E7DEAC31EC /* RunningApp.swift in Sources */, + 0A405966B03B19257EE892F2 /* XCUIApplication+FBQuiescence.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -554,6 +768,28 @@ /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ + 32B97BC56CF509E075A39851 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; + CODE_SIGNING_ALLOWED = NO; + CODE_SIGNING_REQUIRED = NO; + CODE_SIGN_STYLE = Manual; + DEVELOPMENT_TEAM = ""; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = "Device Kit"; + PRODUCT_BUNDLE_IDENTIFIER = "com.mobilenext.devicekit-tvosUITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OBJC_BRIDGING_HEADER = "DeviceKitTests/Categories/devicekit-iosUITests-Bridging-Header.h"; + SWIFT_VERSION = 5.9; + TARGETED_DEVICE_FAMILY = 3; + TVOS_DEPLOYMENT_TARGET = 16.0; + }; + name = Debug; + }; 52049BFB293503A200807AA3 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -800,6 +1036,29 @@ }; name = Release; }; + 5DF6AA80074B9E86C4D4CFAB /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; + CODE_SIGNING_ALLOWED = NO; + CODE_SIGNING_REQUIRED = NO; + CODE_SIGN_STYLE = Manual; + DEVELOPMENT_TEAM = ""; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = "Device Kit"; + PRODUCT_BUNDLE_IDENTIFIER = "com.mobilenext.devicekit-tvosUITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OBJC_BRIDGING_HEADER = "DeviceKitTests/Categories/devicekit-iosUITests-Bridging-Header.h"; + SWIFT_VERSION = 5.9; + TARGETED_DEVICE_FAMILY = 3; + TVOS_DEPLOYMENT_TARGET = 16.0; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -830,6 +1089,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + CECC47FDB7287DB2B421819E /* Build configuration list for PBXNativeTarget "devicekit-tvosUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5DF6AA80074B9E86C4D4CFAB /* Release */, + 32B97BC56CF509E075A39851 /* Debug */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ @@ -849,6 +1117,11 @@ package = 52049C062935E04D00807AA3 /* XCRemoteSwiftPackageReference "FlyingFox" */; productName = FlyingFox; }; + EBF28227FF61B70D19DDE281 /* FlyingFox */ = { + isa = XCSwiftPackageProductDependency; + package = 52049C062935E04D00807AA3 /* XCRemoteSwiftPackageReference "FlyingFox" */; + productName = FlyingFox; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 52049BCB2935039F00807AA3 /* Project object */; diff --git a/devicekit-ios.xcodeproj/xcshareddata/xcschemes/devicekit-tvos.xcscheme b/devicekit-ios.xcodeproj/xcshareddata/xcschemes/devicekit-tvos.xcscheme new file mode 100644 index 0000000..9e7f28f --- /dev/null +++ b/devicekit-ios.xcodeproj/xcshareddata/xcschemes/devicekit-tvos.xcscheme @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/patch-tvos-runner.sh b/scripts/patch-tvos-runner.sh new file mode 100755 index 0000000..ae82930 --- /dev/null +++ b/scripts/patch-tvos-runner.sh @@ -0,0 +1,85 @@ +#!/bin/sh +# Embed the Swift Testing support libraries that the tvOS Simulator runtime does +# not ship in its dyld shared cache. Without this, the XCTest runner links +# (transitively, via XCTestCore) against Testing.framework -> lib_TestingInterop.dylib +# and _Testing_Foundation.framework, which are present on iOS simulators but missing +# on tvOS simulators. The runner therefore launches fine via `xcodebuild +# test-without-building` (which injects toolchain search paths) but aborts at dyld +# time when started directly via `simctl launch` (how mobilecli starts the agent). +# +# This script makes the runner self-contained by copying the missing libraries into +# the bundle's Frameworks directory, adding an @rpath so they resolve as siblings, +# and re-signing the modified Mach-O files ad-hoc for the simulator. +# +# It also patches the display name and app icon on the auto-generated Runner.app, +# mirroring what scripts/patch-runner.sh does for the iOS runner. tvOS icons only +# work through a compiled asset catalog (Assets.car), so instead of copying loose +# PNGs we reuse the Assets.car + CFBundleIcons/CFBundleDisplayName that Xcode already +# compiled into the nested .xctest bundle (from the "App Icon & Top Shelf Image" +# brandassets in DeviceKitTests/Assets.xcassets) and copy them to the Runner.app root, +# where the Home Screen actually looks for them. +# +# The Swift Testing libraries are pulled from the AppleTVSimulator platform for +# simulator runners and from the AppleTVOS platform for real-device runners. The +# ad-hoc signature applied here to the copied binaries is a placeholder for the +# simulator; real-device runners are re-signed with a provisioning profile at +# install time (mobilecli's ResignIPA deep-signs Frameworks/), which replaces it. +# +# Usage: scripts/patch-tvos-runner.sh [simulator|device] +set -e + +APP="${1:?Usage: $0 [simulator|device]}" +TARGET="${2:-simulator}" +FW="$APP/Frameworks" + +case "$TARGET" in + device) + TVDIR="$(xcode-select -p)/Platforms/AppleTVOS.platform/Developer" + ;; + simulator) + TVDIR="$(xcode-select -p)/Platforms/AppleTVSimulator.platform/Developer" + ;; + *) + echo "error: unknown target '$TARGET' (expected 'simulator' or 'device')" + exit 1 + ;; +esac + +if [ ! -d "$APP" ]; then + echo "error: runner app not found at $APP" + exit 1 +fi + +cp "$TVDIR/usr/lib/lib_TestingInterop.dylib" "$FW/" +cp -R "$TVDIR/Library/Frameworks/_Testing_Foundation.framework" "$FW/" + +# Make each Swift Testing binary resolve its @rpath siblings from Frameworks/. +add_rpath() { + install_name_tool -add_rpath '@loader_path/..' "$1" 2>/dev/null || true +} +add_rpath "$FW/Testing.framework/Testing" +add_rpath "$FW/_Testing_Foundation.framework/_Testing_Foundation" + +# Re-sign modified Mach-O files (ad-hoc) for the simulator. +codesign --force --sign - "$FW/lib_TestingInterop.dylib" +codesign --force --sign - "$FW/Testing.framework" +codesign --force --sign - "$FW/_Testing_Foundation.framework" + +echo "Patched tvOS runner with Swift Testing support libraries" + +# Set display name to match the iOS runner. +/usr/bin/plutil -replace CFBundleDisplayName -string "Device Kit" "${APP}/Info.plist" + +# Copy the compiled app icon catalog from the nested .xctest bundle to the +# Runner.app root and point the Runner's Info.plist at it, so the icon shows +# up on the Home Screen instead of the default placeholder. +XCTEST_DIR="${APP}/PlugIns/devicekit-tvosUITests.xctest" +if [ -f "${XCTEST_DIR}/Assets.car" ]; then + cp "${XCTEST_DIR}/Assets.car" "${APP}/Assets.car" + /usr/bin/plutil -replace CFBundleIcons -json \ + '{"CFBundlePrimaryIcon":"App Icon"}' \ + "${APP}/Info.plist" + echo "Patched tvOS runner: display name and icon applied" +else + echo "warning: no Assets.car found in ${XCTEST_DIR}, skipping icon patch" +fi