Create multiple instances of the same app

I am doing automated testing for a MacOS app, and want to start multiple instances of the same app to check whether actions in one instance will affect the other app instances. To do this in command line, I can just use open -n /Desktop/MyMacOSapp.app. However, if I do that, I can’t automate that app instance since it is not connected to a driver. How can I automate different instances of the app at the same time, do I need more than one driver? My code below uses only one driver but it does not start multiple app instances now.

import time
import subprocess
from appium import webdriver as appium_webdriver
from appium.options.mac import Mac2Options
from appium.webdriver.appium_service import AppiumService
from appium.webdriver.common.appiumby import AppiumBy

APPIUM_PORT = '4723'
APPIUM_HOST = '127.0.0.1'
BUNDLE_ID = "com.apple.TextEdit"   #example of MacOS app bundle id

options = Mac2Options()
options.bundle_id = BUNDLE_ID
options.show_server_logs = True
options.system_port = 10100
options.wait_for_quiescence = False

appium_service = AppiumService()
appium_service.start(args=['--relaxed-security'])

driver = appium_webdriver.Remote(f"http://{APPIUM_HOST}:{APPIUM_PORT}", options=options)

driver.execute_script('macos: activateApp', {'bundleId': BUNDLE_ID})
time.sleep(2)
print("open second")
driver.execute_script('macos: activateApp', {'bundleId': BUNDLE_ID})
time.sleep(2)
print("open third")
driver.execute_script('macos: activateApp', {'bundleId': BUNDLE_ID})
time.sleep(5)

appium_service.stop()

The big problem you will have here is keeping the different versions of the app straight. If you only use one driver it will be pretty difficult, using more than one driver also has its complications.

However-> can you talk to developers and ask that they create 3 binaries that have slightly different bundle id’s? Let’s say this is your current bundle id:

com.mycompany.myapp

What if you had 2 additional binaries called com.mycompany.myapp.one, and com.mycompany.myapp.two. If you did that then you wouldn’t have any trouble switching between your apps by bundle id and you could do it all within one driver. Script the builds into CI and it’s not a lot of effort for anyone.

Try to provide appium:appPath capability value while starting the session. It should ensure the correct app is started even if there are multiple apps with the same bundle identifier.

@wreed @mykola-mokhnach Thanks for the suggestions, does that mean that Mac2 driver can handle parallel execution of different binaries within one driver? Will the code below work for different binaries?

driver.execute_script('macos: activateApp', {'bundleId': com.mycompany.myapp})
time.sleep(2)
print("open second")
driver.execute_script('macos: activateApp', {'bundleId': com.mycompany.myapp.one})
time.sleep(2)
print("open third")
driver.execute_script('macos: activateApp', {'bundleId': com.mycompany.myapp.two})

On another note, my app opens another native MacOS app(not browser) during the login process, is it possible for one driver to automate both? I want to get page source of the second MacOS app that is opened

Theoretically this should work. Switching between apps is like switching contexts - you cannot interact with all of them at the same time, it is necessary first to activate one.

@mykola-mokhnach @wreed
Hi, I tried out appium:appPath with one driver for each app(each driver uses a different Appium server port), each app has the same bundleId but are located in different directory (I copied the app from one directory to another). however only one app is activated at a time currently. Here is the code example and issue

I assume at the time the activation API only supports providing of bundle identifiers. I have created a new PR, which should also allow to use absolute paths: https://github.com/appium/appium-mac2-driver/pull/270

Feel free to test it and share your feedback

Hi, I tested the absolute path out, works fine! I have one question, what is the use of skip_app_kill? I assume it is to save the state of the app, so later when you re-activate the app using macos: activateApp, the state is resumed from previous state?

import pathlib
import os
import time
import psutil
import subprocess
from collections import defaultdict
import subprocess
import jwt
import zipfile
import random
import urllib
import pyautogui
import threading
from dotenv import load_dotenv

from threading import Thread
from appium import webdriver as appium_webdriver
from appium.options.mac import Mac2Options
from appium.webdriver.appium_service import AppiumService
from appium.webdriver.common.appiumby import AppiumBy
from selenium import webdriver as selenium_webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.common.actions.action_builder import ActionBuilder
from selenium.webdriver.common.keys import Keys

APPIUM_PORT = '4723'
APPIUM_HOST = '127.0.0.1'
BUNDLE_ID = "com.compay.app"
APPLICATION_NAME = "CompanyApp"

options = Mac2Options()
options.skip_app_kill = True
options.bundle_id = BUNDLE_ID
options.show_server_logs = True
options.system_port = 10101
options.no_reset = True
options.appPath = '/Users/usr/first/path/to/app'

appium_service = AppiumService()
appium_service.start(args=['--relaxed-security'])

driver = appium_webdriver.Remote(f"http://{APPIUM_HOST}:{APPIUM_PORT}", options=options)

options2 = Mac2Options()
options2.bundle_id = BUNDLE_ID
options2.show_server_logs = True
options2.system_port = 10103
options2.appPath = '/Users/usr/second/path/to/app'
driver2 = appium_webdriver.Remote(f"http://{APPIUM_HOST}:{APPIUM_PORT}", options=options2)
driver2.execute_script('macos: launchApp', {'bundleId': BUNDLE_ID})  
print("second instance launched")
time.sleep(3)

driver2.execute_script('macos: activateApp', {'bundleId': BUNDLE_ID, 'path': '/Users/usr/first/path/to/app'})  
time.sleep(5)

driver.quit()
driver2.quit()

appium_service.stop()

I am now able to launch the second instance of the app using the absolute path options2.appPath = '/Users/ngm1/second/path/to/app', but the first instance of the app gets closed when this happens. I want to relaunch the app at path first/path/to/app or keep both instances running together, but the first app instance is getting restarted currently. How do I keep the state of the first/path/to/app?

you must provide path argument to launchApp as well. Also, not sure why two drivers are needed here: GitHub - appium/appium-mac2-driver: Next-gen Appium macOS driver, backed by Apple XCTest. It will for sure break the flow and create other unexpected issues.

Not sure if it’s a bug, but I have to terminate the first app before I can switch to the second app even if I specify bundle id and path of second app. I used one driver at first using the code below to launch the first app, run some actions on the second app, and switch back to the first app.

Problem with terminating the first app is that previous information of the first app is lost when i switch back to the first app.

APPIUM_PORT = '4723'
APPIUM_HOST = '127.0.0.1'
BUNDLE_ID = "com.company.companyApp"
APPLICATION_NAME = "CompanyApp"

options = Mac2Options()
options.bundle_id = BUNDLE_ID
options.system_port = 10101
options.no_reset = True

appium_service = AppiumService()
appium_service.start(args=['--relaxed-security'])

driver = appium_webdriver.Remote(f"http://{APPIUM_HOST}:{APPIUM_PORT}", options=options)
driver.execute_script("macos: launchApp", {"bundleId": BUNDLE_ID, "path": '/path/to/first/app'})

#perform actions on first app

#Problem: this is needed to launch second app
driver.execute_script('macos: terminateApp', {'bundleId': BUNDLE_ID})

driver.execute_script('macos: launchApp', {'bundleId': BUNDLE_ID,'path': '/path/to/second/app'}) 

driver.execute_script('macos: terminateApp', {'bundleId': BUNDLE_ID})

#Problem: reactivate first app restarts app and previous state caused by actions are lost
driver.execute_script('macos: activateApp', {'bundleId': BUNDLE_ID, 'path': '/path/to/first/app'})  
  • You may start the driver without providing any bundle identifier
  • you must provide path to all app management methods: terminateApp, launchApp, activateApp and queryAppState if you want them to properly distinguish between running apps. If path argument is provided then there is no need to provide bundleId one, it has the priority anyway.

also make sure your driver is up to date

I removed the bundle identifier from the options argument like below.

options = Mac2Options()
options.system_port = 10101
options.no_reset = True
driver = appium_webdriver.Remote(f"http://{APPIUM_HOST}:{APPIUM_PORT}", options=options)

#1.10.1 throws error here and does not launch app
driver.execute_script("macos: launchApp", {"bundleId": BUNDLE_ID, "path": '/path/to/first/app'})

However, I realised the latest version(1.10.1) will throw an error shown below when I call launchApp, 1.10.0 works fine without the bundle id in options:

[WebDriverAgentMac] [xcodebuild] XCTExpectFailure: matcher accepted Assertion Failure: Failed to launch com.company.companyApp: 
You do not have permission to run the application “companyApp”. You don’t have permission. 
To view or change permissions, select the item in the Finder and choose File > Get Info. (Underlying Error: The operation couldn’t be completed. Operation not permitted)
[WebDriverAgentMac] [xcodebuild] 2024-01-16 13:18:06.661133+0800 WebDriverAgentRunner-Runner[20975:202037] Issue type: 0
[WebDriverAgentMac] 2024-01-16 13:18:06.661190+0800 WebDriverAgentRunner-Runner[20975:202037] 
Enqueue Failure: Failed to launch com.company.companyApp: You do not have permission to run the application “companyApp”. You don’t have permission. 
To view or change permissions, select the item in the Finder and choose File > Get Info. 
(Underlying Error: The operation couldn’t be completed. Operation not permitted) ((null)) (null) 0 0

@mykola-mokhnach Can I check why there is a difference in behaviour when I update the driver to the latest version? I understand that initialising the driver with no bundle id will just lead the driver to automate the finder app according to the server logs. However, I do not expect an error as shown above. Should I raise a Github issue regarding this?

I don’t think anyone would have an answer to that except of Apple itself. The initWithURL: | Apple Developer Documentation API is used there.

Ok, I created an issue here regarding the XCTest API being used by Mac2 driver https://developer.apple.com/forums/thread/744772