diff --git a/.gitignore b/.gitignore index 0178180..b55c24e 100644 --- a/.gitignore +++ b/.gitignore @@ -90,4 +90,4 @@ fastlane/test_output iOSInjectionProject/ -.DS_Store \ No newline at end of file +.DS_Store diff --git a/MiniSim.xcodeproj/project.pbxproj b/MiniSim.xcodeproj/project.pbxproj index cf3833c..2e03863 100644 --- a/MiniSim.xcodeproj/project.pbxproj +++ b/MiniSim.xcodeproj/project.pbxproj @@ -19,6 +19,7 @@ 76059BF52AD4361C0008D38B /* SetupPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76059BF42AD4361C0008D38B /* SetupPreferences.swift */; }; 76059BF72AD449DC0008D38B /* OnboardingHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76059BF62AD449DC0008D38B /* OnboardingHeader.swift */; }; 76059BF92AD558C30008D38B /* SetupItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76059BF82AD558C30008D38B /* SetupItemView.swift */; }; + 760DEACE2B0DFB6600253576 /* ShellStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 760DEACD2B0DFB6600253576 /* ShellStub.swift */; }; 7610992D2A3F95850067885A /* MiniSim.sdef in Resources */ = {isa = PBXBuildFile; fileRef = 7610992C2A3F95850067885A /* MiniSim.sdef */; }; 7610992F2A3F95D90067885A /* NSScriptCommand+utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7610992E2A3F95D90067885A /* NSScriptCommand+utils.swift */; }; 7625140B2992B46D0060A225 /* Pasteboard+utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7625140A2992B46D0060A225 /* Pasteboard+utils.swift */; }; @@ -75,8 +76,11 @@ 76AC9AF62A0EA82C00864A8B /* CustomCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76AC9AF52A0EA82C00864A8B /* CustomCommands.swift */; }; 76AC9AF92A0EB50800864A8B /* SymbolPicker in Frameworks */ = {isa = PBXBuildFile; productRef = 76AC9AF82A0EB50800864A8B /* SymbolPicker */; }; 76B70F7E2B0D361A009D87A4 /* UserDefaultsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76B70F7D2B0D361A009D87A4 /* UserDefaultsTests.swift */; }; + 76B70F822B0D50FE009D87A4 /* ADBTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76B70F812B0D50FE009D87A4 /* ADBTests.swift */; }; + 76B70F842B0D5AB4009D87A4 /* Shell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76B70F832B0D5AB4009D87A4 /* Shell.swift */; }; 76BF0AD92C8CB3E6003BE568 /* AcknowList in Frameworks */ = {isa = PBXBuildFile; productRef = 76BF0AD82C8CB3E6003BE568 /* AcknowList */; }; 76BF0ADB2C8CB4CD003BE568 /* Package.resolved in Resources */ = {isa = PBXBuildFile; fileRef = 76BF0ADA2C8CB4CD003BE568 /* Package.resolved */; }; + 76BF0ADD2C8DF660003BE568 /* AccessibilityElementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76BF0ADC2C8DF660003BE568 /* AccessibilityElementTests.swift */; }; 76C1396A2C849A3F006CD80C /* MenuIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76C139692C849A3F006CD80C /* MenuIcons.swift */; }; 76E4451229D4391000039025 /* Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76E4451129D4391000039025 /* Onboarding.swift */; }; 76E4451429D4403F00039025 /* NSNotificationName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76E4451329D4403F00039025 /* NSNotificationName.swift */; }; @@ -115,6 +119,7 @@ 76059BF42AD4361C0008D38B /* SetupPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupPreferences.swift; sourceTree = ""; }; 76059BF62AD449DC0008D38B /* OnboardingHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingHeader.swift; sourceTree = ""; }; 76059BF82AD558C30008D38B /* SetupItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupItemView.swift; sourceTree = ""; }; + 760DEACD2B0DFB6600253576 /* ShellStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellStub.swift; sourceTree = ""; }; 7610992C2A3F95850067885A /* MiniSim.sdef */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = MiniSim.sdef; sourceTree = ""; }; 7610992E2A3F95D90067885A /* NSScriptCommand+utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSScriptCommand+utils.swift"; sourceTree = ""; }; 7625140A2992B46D0060A225 /* Pasteboard+utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Pasteboard+utils.swift"; sourceTree = ""; }; @@ -169,7 +174,10 @@ 76AC9AF52A0EA82C00864A8B /* CustomCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomCommands.swift; sourceTree = ""; }; 76B70F742B0D359D009D87A4 /* MiniSimTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MiniSimTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 76B70F7D2B0D361A009D87A4 /* UserDefaultsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsTests.swift; sourceTree = ""; }; + 76B70F812B0D50FE009D87A4 /* ADBTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ADBTests.swift; sourceTree = ""; }; + 76B70F832B0D5AB4009D87A4 /* Shell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shell.swift; sourceTree = ""; }; 76BF0ADA2C8CB4CD003BE568 /* Package.resolved */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = Package.resolved; path = MiniSim.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved; sourceTree = SOURCE_ROOT; }; + 76BF0ADC2C8DF660003BE568 /* AccessibilityElementTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityElementTests.swift; sourceTree = ""; }; 76C139692C849A3F006CD80C /* MenuIcons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MenuIcons.swift; sourceTree = ""; }; 76E4451129D4391000039025 /* Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Onboarding.swift; sourceTree = ""; }; 76E4451329D4403F00039025 /* NSNotificationName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSNotificationName.swift; sourceTree = ""; }; @@ -230,6 +238,14 @@ path = Terminal; sourceTree = ""; }; + 760DEACC2B0DFB5B00253576 /* Mocks */ = { + isa = PBXGroup; + children = ( + 760DEACD2B0DFB6600253576 /* ShellStub.swift */, + ); + path = Mocks; + sourceTree = ""; + }; 762CF1E12981DDD400099999 /* Extensions */ = { isa = PBXGroup; children = ( @@ -297,6 +313,7 @@ 7645D4BD2982A1B100019227 /* DeviceService.swift */, 7699511C2C845B1900462287 /* DeviceParser.swift */, 76F04A10298A5AE000BF9CA3 /* ADB.swift */, + 76B70F832B0D5AB4009D87A4 /* Shell.swift */, ); path = Service; sourceTree = ""; @@ -396,8 +413,11 @@ 76B70F752B0D359D009D87A4 /* MiniSimTests */ = { isa = PBXGroup; children = ( + 760DEACC2B0DFB5B00253576 /* Mocks */, 76B70F7D2B0D361A009D87A4 /* UserDefaultsTests.swift */, 7699511E2C845CBA00462287 /* DeviceParserTests.swift */, + 76B70F812B0D50FE009D87A4 /* ADBTests.swift */, + 76BF0ADC2C8DF660003BE568 /* AccessibilityElementTests.swift */, ); path = MiniSimTests; sourceTree = ""; @@ -619,6 +639,7 @@ 7630B2752986D52900D8B57D /* NSAlert+showError.swift in Sources */, 4AFACC742AD730BE00EC369F /* SubMenuItem.swift in Sources */, 7630B25E2984339100D8B57D /* MainMenuActions.swift in Sources */, + 76B70F842B0D5AB4009D87A4 /* Shell.swift in Sources */, 76AC9AF62A0EA82C00864A8B /* CustomCommands.swift in Sources */, 76489D5C29BFCA330070EF03 /* OnboardingItem.swift in Sources */, 7645D5012982E6FA00019227 /* main.swift in Sources */, @@ -656,7 +677,10 @@ buildActionMask = 2147483647; files = ( 7699511F2C845CBA00462287 /* DeviceParserTests.swift in Sources */, + 76BF0ADD2C8DF660003BE568 /* AccessibilityElementTests.swift in Sources */, 76B70F7E2B0D361A009D87A4 /* UserDefaultsTests.swift in Sources */, + 760DEACE2B0DFB6600253576 /* ShellStub.swift in Sources */, + 76B70F822B0D50FE009D87A4 /* ADBTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/MiniSim/AccessibilityElement.swift b/MiniSim/AccessibilityElement.swift index 6d7463f..6f0f822 100644 --- a/MiniSim/AccessibilityElement.swift +++ b/MiniSim/AccessibilityElement.swift @@ -6,9 +6,10 @@ // import AppKit -import ShellOut class AccessibilityElement { + static var shell: ShellProtocol = Shell() + private let underlyingElement: AXUIElement required init(_ axUIElement: AXUIElement) { @@ -44,7 +45,7 @@ class AccessibilityElement { set frontmost of every process whose unix id is \(pid) to true end tell' """ - _ = try? shellOut(to: script) + _ = try? shell.execute(command: script) } } diff --git a/MiniSim/Service/Adb.swift b/MiniSim/Service/Adb.swift index 705e1ec..8a66659 100644 --- a/MiniSim/Service/Adb.swift +++ b/MiniSim/Service/Adb.swift @@ -6,17 +6,24 @@ // import Foundation -import ShellOut protocol ADBProtocol { + static var shell: ShellProtocol { get set } + static func getAdbPath() throws -> String static func getEmulatorPath() throws -> String - static func getAdbId(for deviceName: String, adbPath: String) throws -> String - static func checkAndroidHome(path: String) throws -> Bool - static func isAccesibilityOn(deviceId: String, adbPath: String) -> Bool + static func getAdbId(for deviceName: String) throws -> String + static func checkAndroidHome( + path: String, + fileManager: FileManager + ) throws -> Bool + static func isAccesibilityOn(deviceId: String) -> Bool + static func toggleAccesibility(deviceId: String) } final class ADB: ADBProtocol { + static var shell: ShellProtocol = Shell() + static let talkbackOn = "com.google.android.marvin.talkback/com.google.android.marvin.talkback.TalkBackService" static let talkbackOff = "com.android.talkback/com.google.android.marvin.talkback.TalkBackService" @@ -55,13 +62,16 @@ final class ADB: ADBProtocol { /** Checks if passed path exists and points to `ANDROID_HOME`. */ - @discardableResult static func checkAndroidHome(path: String) throws -> Bool { - if !FileManager.default.fileExists(atPath: path) { + @discardableResult static func checkAndroidHome( + path: String, + fileManager: FileManager = .default + ) throws -> Bool { + if !fileManager.fileExists(atPath: path) { throw AndroidHomeError.pathNotFound } do { - try shellOut(to: "\(path)" + Paths.emulator.rawValue, arguments: ["-list-avds"]) + try shell.execute(command: "\(path)" + Paths.emulator.rawValue, arguments: ["-list-avds"]) } catch { throw AndroidHomeError.pathNotCorrect } @@ -72,14 +82,20 @@ final class ADB: ADBProtocol { try getAndroidHome() + Paths.emulator.rawValue } - static func getAdbId(for deviceName: String, adbPath: String) throws -> String { - let onlineDevices = try shellOut(to: "\(adbPath) devices") + static func getAdbId(for deviceName: String) throws -> String { + let adbPath = try Self.getAdbPath() + let onlineDevices = try shell.execute(command: "\(adbPath) devices") let splitted = onlineDevices.components(separatedBy: "\n") for line in splitted { let device = line.match("^emulator-[0-9]+") guard let deviceId = device.first?.first else { continue } - let output = try? shellOut(to: "\(adbPath) -s \(deviceId) emu avd name").components(separatedBy: "\n") + + let output = try? shell.execute( + command: "\(adbPath) -s \(deviceId) emu avd name" + ) + .components(separatedBy: "\n") + if let name = output?.first { let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines) let trimmedDeviceName = deviceName.trimmingCharacters(in: .whitespacesAndNewlines) @@ -91,9 +107,12 @@ final class ADB: ADBProtocol { throw DeviceError.deviceNotFound } - static func isAccesibilityOn(deviceId: String, adbPath: String) -> Bool { + static func isAccesibilityOn(deviceId: String) -> Bool { + guard let adbPath = try? Self.getAdbPath() else { + return false + } let shellCommand = "\(adbPath) -s \(deviceId) shell settings get secure enabled_accessibility_services" - guard let result = try? shellOut(to: [shellCommand]) else { + guard let result = try? shell.execute(command: shellCommand) else { return false } @@ -103,4 +122,16 @@ final class ADB: ADBProtocol { return false } + + static func toggleAccesibility(deviceId: String) { + guard let adbPath = try? Self.getAdbPath() else { + return + } + let a11yIsEnabled = Self.isAccesibilityOn(deviceId: deviceId) + let value = a11yIsEnabled ? ADB.talkbackOff : ADB.talkbackOn + let shellCmd = "\(adbPath) -s \(deviceId) shell settings put secure enabled_accessibility_services \(value)" + + // Ignore the error if toggling a11y fails. + _ = try? shell.execute(command: shellCmd) + } } diff --git a/MiniSim/Service/DeviceParser.swift b/MiniSim/Service/DeviceParser.swift index b8e563c..62f15f4 100644 --- a/MiniSim/Service/DeviceParser.swift +++ b/MiniSim/Service/DeviceParser.swift @@ -116,12 +116,11 @@ class AndroidEmulatorParser: DeviceParser { } func parse(_ input: String) -> [Device] { - guard let adbPath = try? adb.getAdbPath() else { return [] } let deviceNames = input.components(separatedBy: .newlines) return deviceNames .filter { !$0.isEmpty && !$0.contains("Storing crashdata") } .compactMap { deviceName in - let adbId = try? adb.getAdbId(for: deviceName, adbPath: adbPath) + let adbId = try? adb.getAdbId(for: deviceName) return Device(name: deviceName, identifier: adbId, booted: adbId != nil, platform: .android, type: .virtual) } } diff --git a/MiniSim/Service/DeviceService.swift b/MiniSim/Service/DeviceService.swift index c723085..dba653f 100644 --- a/MiniSim/Service/DeviceService.swift +++ b/MiniSim/Service/DeviceService.swift @@ -406,7 +406,7 @@ extension DeviceService { throw DeviceError.deviceNotFound } - let a11yIsEnabled = ADB.isAccesibilityOn(deviceId: adbId, adbPath: adbPath) + let a11yIsEnabled = ADB.isAccesibilityOn(deviceId: adbId) let value = a11yIsEnabled ? ADB.talkbackOff : ADB.talkbackOn let shellCmd = "\(adbPath) -s \(adbId) shell settings put secure enabled_accessibility_services \(value)" _ = try? shellOut(to: shellCmd) diff --git a/MiniSim/Service/Shell.swift b/MiniSim/Service/Shell.swift new file mode 100644 index 0000000..7f03427 --- /dev/null +++ b/MiniSim/Service/Shell.swift @@ -0,0 +1,34 @@ +import Foundation +import ShellOut + +protocol ShellProtocol { + @discardableResult func execute( + command: String, + arguments: [String], + atPath: String + ) throws -> String +} + +extension ShellProtocol { + @discardableResult func execute( + command: String, + arguments: [String] = [], + atPath: String = "." + ) throws -> String { + try execute(command: command, arguments: arguments, atPath: atPath) + } +} + +final class Shell: ShellProtocol { + @discardableResult func execute( + command: String, + arguments: [String] = [], + atPath: String = "." + ) throws -> String { + try shellOut( + to: command, + arguments: arguments, + at: atPath + ) + } +} diff --git a/MiniSim/Views/About.swift b/MiniSim/Views/About.swift index a869a16..a8697a5 100644 --- a/MiniSim/Views/About.swift +++ b/MiniSim/Views/About.swift @@ -5,15 +5,15 @@ // Created by Oskar Kwaśniewski on 28/01/2023. // +import AcknowList import Sparkle import SwiftUI -import AcknowList struct About: View { private let updaterController: SPUStandardUpdaterController @Environment (\.openURL) private var openURL @State private var isAcknowledgementsListPresented = false - + init() { updaterController = SPUStandardUpdaterController( startingUpdater: true, @@ -21,12 +21,12 @@ struct About: View { userDriverDelegate: nil ) } - + let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String private let bottomPadding: Double = 10 private let minFrameWidth: Double = 650 private let minFrameHeight: Double = 450 - + var body: some View { VStack { Image(nsImage: NSImage(named: "AppIcon") ?? NSImage()) @@ -42,11 +42,11 @@ struct About: View { Label("Check for updates", systemImage: "gear") } .padding(.bottom, bottomPadding) - + Button("Acknowledgements") { isAcknowledgementsListPresented.toggle() } - + HStack { Button("GitHub") { openURL(URL(string: "https://github.com/okwasniewski/MiniSim")!) diff --git a/MiniSim/Views/Onboarding/SetupView.swift b/MiniSim/Views/Onboarding/SetupView.swift index a0a5c9b..4cab9a2 100644 --- a/MiniSim/Views/Onboarding/SetupView.swift +++ b/MiniSim/Views/Onboarding/SetupView.swift @@ -5,7 +5,6 @@ // Created by Oskar Kwaśniewski on 15/03/2023. // -import ShellOut import SwiftUI struct SetupView: View { diff --git a/MiniSim/Views/Preferences.swift b/MiniSim/Views/Preferences.swift index 0e275ce..c24381d 100644 --- a/MiniSim/Views/Preferences.swift +++ b/MiniSim/Views/Preferences.swift @@ -8,7 +8,6 @@ import KeyboardShortcuts import LaunchAtLogin import Settings -import ShellOut import SwiftUI struct Preferences: View { diff --git a/MiniSimTests/ADBTests.swift b/MiniSimTests/ADBTests.swift new file mode 100644 index 0000000..c8f6666 --- /dev/null +++ b/MiniSimTests/ADBTests.swift @@ -0,0 +1,132 @@ +import XCTest + +@testable import MiniSim + +final class ADBTests: XCTestCase { + var shellStub: ShellStub! + + class FileManagerStub: FileManager { + override func fileExists(atPath path: String) -> Bool { + true + } + } + + class FileManagerEmptyStub: FileManager { + override func fileExists(atPath path: String) -> Bool { + false + } + } + + let savedAndroidHome = UserDefaults.standard.androidHome + let defaultHomePath = "/Users/\(NSUserName())/Library/Android/sdk" + + override func setUp() { + super.setUp() + shellStub = ShellStub() + ADB.shell = shellStub + UserDefaults.standard.removeObject(forKey: UserDefaults.Keys.androidHome) + } + + override func tearDown() { + UserDefaults.standard.androidHome = savedAndroidHome + shellStub.tearDown() + super.tearDown() + } + + func testGetAndroidHome() throws { + let androidHome = try ADB.getAndroidHome() + + XCTAssertEqual(androidHome, defaultHomePath) + + UserDefaults.standard.androidHome = "customAndroidHome" + let customAndroidHome = try ADB.getAndroidHome() + + XCTAssertEqual( + customAndroidHome, + "customAndroidHome", + "Setting custom androidHome overrides default one" + ) + } + + func testCheckAndroidHome() throws { + let output = try ADB.checkAndroidHome( + path: defaultHomePath, + fileManager: FileManagerStub() + ) + XCTAssertEqual(output, true) + XCTAssertEqual(shellStub.lastExecutedCommand, defaultHomePath + "/emulator/emulator") + XCTAssertEqual(shellStub.lastPassedArguments, ["-list-avds"]) + + XCTAssertThrowsError( + try ADB.checkAndroidHome( + path: defaultHomePath, + fileManager: FileManagerEmptyStub() + ) + ) + } + + func testGetUtilPaths() throws { + let adbPath = try ADB.getAdbPath() + let avdPath = try ADB.getAvdPath() + + XCTAssertEqual( + adbPath, + defaultHomePath + "/platform-tools/adb" + ) + XCTAssertEqual( + avdPath, + defaultHomePath + "/cmdline-tools/latest/bin/avdmanager" + ) + } + + func testGetAdbId() throws { + shellStub.mockedExecute = { command, _, _ in + if command.contains("devices") { + return """ + List of devices attached + emulator-5554 device + emulator-5556 device + """ + } + + if command.contains("avd name") { + return """ + Pixel_XL_API_32 + OK + """ + } + + return "" + } + let adbId = try ADB.getAdbId(for: "Pixel_XL_API_32") + + XCTAssertEqual(adbId, "emulator-5554") + + XCTAssertThrowsError( + try ADB.getAdbId(for: "Pixel_Not_Found") + ) + } + + func testIsAccesibilityOn() throws { + var isA11yOn: Bool + isA11yOn = ADB.isAccesibilityOn(deviceId: "emulator-5544") + XCTAssertFalse(isA11yOn) + + shellStub.mockedExecute = { _, _, _ in + "com.google.android.marvin.talkback/com.google.android.marvin.talkback.TalkBackService" + } + isA11yOn = ADB.isAccesibilityOn(deviceId: "emulator-5544") + XCTAssertTrue(isA11yOn) + } + + func testToggle11y() { + UserDefaults.standard.androidHome = "adbPath" + let expectedCommand = """ + adbPath/platform-tools/adb -s emulator-5544 shell settings put secure enabled_accessibility_services com.google.android.marvin.talkback/com.google.android.marvin.talkback.TalkBackService + """ + + ADB.toggleAccesibility(deviceId: "emulator-5544") + XCTAssertEqual(shellStub.lastExecutedCommand, expectedCommand) + XCTAssertEqual(shellStub.lastPassedArguments, []) + } +} diff --git a/MiniSimTests/AccessibilityElementTests.swift b/MiniSimTests/AccessibilityElementTests.swift new file mode 100644 index 0000000..15460ad --- /dev/null +++ b/MiniSimTests/AccessibilityElementTests.swift @@ -0,0 +1,73 @@ +@testable import MiniSim +import XCTest + +class AccessibilityElementTests: XCTestCase { + var shellStub: ShellStub! + var mockElement: AXUIElement! + var accessibilityElement: AccessibilityElement! + + override func setUp() { + super.setUp() + mockElement = AXUIElementCreateSystemWide() + accessibilityElement = AccessibilityElement(mockElement) + shellStub = ShellStub() + AccessibilityElement.shell = shellStub + } + + override func tearDown() { + shellStub.tearDown() + super.tearDown() + } + + func testPerformAction() { + let result = accessibilityElement.performAction(key: "AXPress") + XCTAssertFalse(result == .success, "Action should not succeed on mock element") + } + + func testSetAttribute() { + // Again, this is tricky to test without mocking AXUIElementSetAttributeValue + // This test just ensures the method doesn't crash + accessibilityElement.setAttribute(key: "AXFocused", value: true as CFBoolean) + // We can't easily verify the result, but we can at least check that it doesn't crash + } + + func testForceFocus() { + let expectation = self.expectation(description: "Force focus completed") + + shellStub.mockedExecute = { _, _, _ in + expectation.fulfill() + return "" + } + + AccessibilityElement.forceFocus(pid: 1_234) + + waitForExpectations(timeout: 5) { error in + if let error { + XCTFail("waitForExpectationsWithTimeout errored: \(error)") + } + } + + let expectedScript = """ + osascript -e 'tell application "System Events" + set frontmost of every process whose unix id is 1234 to true + end tell' + """ + + XCTAssertEqual(shellStub.lastExecutedCommand, expectedScript) + } + + func testHasA11yAccess() { + // This test depends on the system state and might not be reliable + // You might want to mock this in a real scenario + let hasAccess = AccessibilityElement.hasA11yAccess(prompt: false) + // Instead of asserting true, we'll just print the result + print("Accessibility access is \(hasAccess ? "granted" : "not granted")") + } + + func testAllWindowsForPID() { + // This test is also tricky without proper mocking + // In a real scenario, you'd want to inject a mock AXUIElement creator + let windows = AccessibilityElement.allWindowsForPID(1_234) + XCTAssertTrue(windows.isEmpty, "Windows should be empty for mock element") + } +} diff --git a/MiniSimTests/DeviceParserTests.swift b/MiniSimTests/DeviceParserTests.swift index 6800dfd..73b5625 100644 --- a/MiniSimTests/DeviceParserTests.swift +++ b/MiniSimTests/DeviceParserTests.swift @@ -1,33 +1,38 @@ @testable import MiniSim import XCTest -// Mock ADB class for testing -class ADB: ADBProtocol { - static func getEmulatorPath() throws -> String { - "" - } +class DeviceParserTests: XCTestCase { + // Mock ADB class for testing + class ADB: ADBProtocol { + static var shell: ShellProtocol = Shell() - static func checkAndroidHome(path: String) throws -> Bool { - true - } + static func getAdbId(for deviceName: String) throws -> String { + if deviceName == "Nexus_5X_API_28" { + throw NSError(domain: "ADBError", code: 1, userInfo: nil) + } + return "mock_adb_id_for_\(deviceName)" + } - static func isAccesibilityOn(deviceId: String, adbPath: String) -> Bool { - false - } + static func checkAndroidHome(path: String, fileManager: FileManager) throws -> Bool { + true + } - static func getAdbPath() throws -> String { - "/mock/adb/path" - } + static func isAccesibilityOn(deviceId: String) -> Bool { + false + } + + static func toggleAccesibility(deviceId: String) { + } - static func getAdbId(for deviceName: String, adbPath: String) throws -> String { - if deviceName == "Nexus_5X_API_28" { - throw NSError(domain: "ADBError", code: 1, userInfo: nil) + static func getEmulatorPath() throws -> String { + "" + } + + static func getAdbPath() throws -> String { + "/mock/adb/path" } - return "mock_adb_id_for_\(deviceName)" } -} -class DeviceParserTests: XCTestCase { func testDeviceParserFactory() { let iosParser = DeviceParserFactory().getParser(.iosSimulator) XCTAssertTrue(iosParser is IOSSimulatorParser) @@ -277,24 +282,29 @@ class DeviceParserTests: XCTestCase { func testAndroidEmulatorParserWithADBFailure() { class FailingADB: ADBProtocol { - static func getEmulatorPath() throws -> String { - "" + static var shell: ShellProtocol = Shell() + + static func getAdbId(for deviceName: String) throws -> String { + throw NSError(domain: "ADBError", code: 2, userInfo: nil) } - static func checkAndroidHome(path: String) throws -> Bool { + static func getAdbPath() throws -> String { + throw NSError(domain: "ADBError", code: 1, userInfo: nil) + } + + static func checkAndroidHome(path: String, fileManager: FileManager) throws -> Bool { true } - static func isAccesibilityOn(deviceId: String, adbPath: String) -> Bool { + static func isAccesibilityOn(deviceId: String) -> Bool { false } - static func getAdbPath() throws -> String { - throw NSError(domain: "ADBError", code: 1, userInfo: nil) + static func toggleAccesibility(deviceId: String) { } - static func getAdbId(for deviceName: String, adbPath: String) throws -> String { - throw NSError(domain: "ADBError", code: 2, userInfo: nil) + static func getEmulatorPath() throws -> String { + "" } } @@ -303,6 +313,8 @@ class DeviceParserTests: XCTestCase { let devices = parser.parse(input) - XCTAssertTrue(devices.isEmpty) + XCTAssertFalse(devices.isEmpty) + XCTAssertEqual(devices[0].name, "Pixel_3a_API_30_x86") + XCTAssertFalse(devices[0].booted) } } diff --git a/MiniSimTests/Mocks/ShellStub.swift b/MiniSimTests/Mocks/ShellStub.swift new file mode 100644 index 0000000..4e5fd5f --- /dev/null +++ b/MiniSimTests/Mocks/ShellStub.swift @@ -0,0 +1,50 @@ +import Foundation +@testable import MiniSim + +class ShellStub: ShellProtocol { + private let queue = DispatchQueue(label: "com.minisim.shellstub", attributes: .concurrent) + + private var _lastExecutedCommand: String = "" + private var _lastPassedArguments: [String] = [] + private var _lastPassedPath: String = "" + private var _mockedExecute: ((String, [String], String) -> String)? + + var lastExecutedCommand: String { + queue.sync { _lastExecutedCommand } + } + + var lastPassedArguments: [String] { + queue.sync { _lastPassedArguments } + } + + var lastPassedPath: String { + queue.sync { _lastPassedPath } + } + + var mockedExecute: ((String, [String], String) -> String)? { + get { queue.sync { _mockedExecute } } + set { queue.async(flags: .barrier) { self._mockedExecute = newValue } } + } + + func execute(command: String, arguments: [String], atPath: String) throws -> String { + queue.async(flags: .barrier) { + self._lastExecutedCommand = command + self._lastPassedArguments = arguments + self._lastPassedPath = atPath + } + + if let mockedExecute = queue.sync(execute: { _mockedExecute }) { + return mockedExecute(command, arguments, atPath) + } + return "" + } + + func tearDown() { + queue.async(flags: .barrier) { + self._lastExecutedCommand = "" + self._lastPassedArguments = [] + self._lastPassedPath = "" + self._mockedExecute = nil + } + } +}