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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 45 additions & 17 deletions MiniSim/Service/AppleUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,50 @@ class AppleUtils {
}
}

static func launchSimulatorApp(uuid: String) throws {
let isSimulatorRunning = workspace.runningApplications
.contains { $0.bundleIdentifier == "com.apple.iphonesimulator" }

if !isSimulatorRunning {
guard let activeDeveloperDir = try? shell.execute(
command: DeviceConstants.ProcessPaths.xcodeSelect.rawValue,
arguments: ["-p"]
)
.trimmingCharacters(in: .whitespacesAndNewlines) else {
throw DeviceError.xcodeError
}
try shell.execute(
command: "\(activeDeveloperDir)/Applications/Simulator.app/Contents/MacOS/Simulator",
arguments: ["--args", "-CurrentDeviceUDID", uuid]
)
}
static func launchSimulatorApp(uuid: String) throws {
let isSimulatorRunning = workspace.runningApplications
.contains { $0.bundleIdentifier == "com.apple.iphonesimulator" }

guard !isSimulatorRunning else { return }

guard let activeDeveloperDir = try? shell.execute(
command: DeviceConstants.ProcessPaths.xcodeSelect.rawValue,
arguments: ["-p"]
)
.trimmingCharacters(in: .whitespacesAndNewlines),
!activeDeveloperDir.isEmpty else {
throw DeviceError.xcodeError
}

let fileManager = FileManager.default
let developerURL = URL(fileURLWithPath: activeDeveloperDir)

// Older Xcode: Simulator.app is inside Developer/Applications.
let simulatorExecutable = developerURL
.appendingPathComponent("Applications/Simulator.app/Contents/MacOS/Simulator")
.path

if fileManager.isExecutableFile(atPath: simulatorExecutable) {
try shell.execute(
command: simulatorExecutable,
arguments: ["--args", "-CurrentDeviceUDID", uuid]
)
return
}

// Newer Xcode: DeviceHub.app is alongside the Developer directory.
let deviceHubURL = developerURL
.deletingLastPathComponent()
.appendingPathComponent("Applications/DeviceHub.app")

guard fileManager.fileExists(atPath: deviceHubURL.path) else {
throw DeviceError.xcodeError
}

try shell.execute(
command: "/usr/bin/open",
arguments: ["-a", deviceHubURL.path]
)
}

}
9 changes: 9 additions & 0 deletions MiniSim/Service/DeviceConstants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,19 @@ enum DeviceConstants {
enum ProcessPaths: String {
case xcrun = "/usr/bin/xcrun"
case xcodeSelect = "/usr/bin/xcode-select"
case open = "/usr/bin/open"
}

enum BundleURL: String {
case emulator = "qemu-system-aarch64"
case simulator = "Simulator.app"
case deviceHub = "DeviceHub.app"
}

/// Paths relative to the directory returned by `xcode-select -p`.
enum AppRelativePaths: String {
case simulatorExecutable =
"Applications/Simulator.app/Contents/MacOS/Simulator"
case deviceHub = "../Applications/DeviceHub.app"
}
}
86 changes: 48 additions & 38 deletions MiniSim/Service/DeviceService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,45 +29,55 @@ extension Device {
}
}

extension DeviceServiceCommon {
func focusDevice() {
Thread.assertBackgroundThread()

let runningApps = NSWorkspace.shared.runningApplications.filter { $0.activationPolicy == .regular }

if let uuid = device.identifier, device.platform == .ios {
try? AppleUtils.launchSimulatorApp(uuid: uuid)
extension DeviceServiceCommon {
func focusDevice() {
Thread.assertBackgroundThread()

if let uuid = device.identifier, device.platform == .ios {
try? AppleUtils.launchSimulatorApp(uuid: uuid)
}

let runningApps = NSWorkspace.shared.runningApplications
.filter { $0.activationPolicy == .regular }

for app in runningApps {
guard let bundleURL = app.bundleURL?.absoluteString else {
continue
}

let isAndroid = bundleURL.contains(
DeviceConstants.BundleURL.emulator.rawValue
)
let isAppleSimulator = bundleURL.contains(
DeviceConstants.BundleURL.simulator.rawValue
)
let isDeviceHub = bundleURL.contains(
DeviceConstants.BundleURL.deviceHub.rawValue
)

guard isAndroid || isAppleSimulator || isDeviceHub else {
continue
}

for window in AccessibilityElement.allWindowsForPID(app.processIdentifier) {
guard
let windowTitle = window.attribute(key: .title, type: String.self),
!windowTitle.isEmpty,
matchDeviceTitle(windowTitle: windowTitle, device: device)
else {
continue
}

if isAndroid {
AccessibilityElement.forceFocus(pid: app.processIdentifier)
} else {
window.performAction(key: kAXRaiseAction)
app.activate(options: [.activateIgnoringOtherApps])
}
}
}

}

for app in runningApps {
guard
let bundleURL = app.bundleURL?.absoluteString,
bundleURL.contains(DeviceConstants.BundleURL.simulator.rawValue) ||
bundleURL.contains(DeviceConstants.BundleURL.emulator.rawValue) else {
continue
}
let isAndroid = bundleURL.contains(DeviceConstants.BundleURL.emulator.rawValue)

for window in AccessibilityElement.allWindowsForPID(app.processIdentifier) {
guard let windowTitle = window.attribute(key: .title, type: String.self),
!windowTitle.isEmpty else {
continue
}

if !matchDeviceTitle(windowTitle: windowTitle, device: device) {
continue
}

if isAndroid {
AccessibilityElement.forceFocus(pid: app.processIdentifier)
} else {
window.performAction(key: kAXRaiseAction)
app.activate(options: [.activateIgnoringOtherApps])
}
}
}
}

private func matchDeviceTitle(windowTitle: String, device: Device) -> Bool {
if device.platform == .android {
let deviceName = windowTitle.match(#"(?<=- ).*?(?=:)"#).first?.first
Expand Down
36 changes: 22 additions & 14 deletions MiniSimTests/AppleUtilsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,24 +77,32 @@ class AppleUtilsTests: XCTestCase {

waitForExpectations(timeout: 5, handler: nil)
}
func testLaunchDeviceHubWhenSimulatorAppIsMissing() {
let uuid = "test-uuid"
mockWorkspace.mockRunningApplications = [] // Simulator not running

func testLaunchSimulatorAppWhenNotRunning() {
let uuid = "test-uuid"
mockWorkspace.mockRunningApplications = [] // Simulator not running

shellStub.mockedExecute = { command, _, _ in
if command == DeviceConstants.ProcessPaths.xcodeSelect.rawValue {
return "/Applications/Xcode.app/Contents/Developer"
}
return ""
shellStub.mockedExecute = { command, _, _ in
if command == DeviceConstants.ProcessPaths.xcodeSelect.rawValue {
return "/Applications/Xcode.app/Contents/Developer"
}

XCTAssertNoThrow(try AppleUtils.launchSimulatorApp(uuid: uuid))

XCTAssertEqual(shellStub.lastExecutedCommand, "/Applications/Xcode.app/Contents/Developer/Applications/Simulator.app/Contents/MacOS/Simulator")
XCTAssertEqual(shellStub.lastPassedArguments, ["--args", "-CurrentDeviceUDID", uuid])
return ""
}

XCTAssertNoThrow(try AppleUtils.launchSimulatorApp(uuid: uuid))

XCTAssertEqual(
shellStub.lastExecutedCommand,
DeviceConstants.ProcessPaths.open.rawValue
)
XCTAssertEqual(
shellStub.lastPassedArguments,
[
"-a",
"/Applications/Xcode.app/Contents/Applications/DeviceHub.app"
]
)
}

func testLaunchSimulatorAppWhenAlreadyRunning() {
let uuid = "test-uuid"
mockWorkspace.mockRunningApplications = [MockNSRunningApplication(bundleIdentifier: "com.apple.iphonesimulator")]
Expand Down