Simple Automation using Python - Atomac in Mac OS X
The Product Automation in MAC OS X is quite easy if we have enough knowledge in the following languages and tools
Cross Platform GUI Test Automation tool Linux version is LDTP, Windows version is Cobra and Mac version is PyATOM + ATOMac.
- Bash Scripts
- Apple Scripts
- Python especially ATOMac
- Knowledge in Accessibility.
For more understanding of the LDTP:
LDTP - Tutorial is written by Nagappan Alagappan. He has given the clear idea and architecture of the LDTP and its working.
I have used the contents from the tutorial for knowledge sharing. Following are the few information and contents from the LDTP - Tutorial.
Few interesting key points,
- Linux Desktop Testing Project (LDTP) is aimed at producing high quality test automation framework and cutting-edge tools that can be used to test GNU/Linux Desktop and improve it.
- It uses the Accessibility libraries to poke through the application's user interface.
- This idea has been extended to Microsoft Windows as Cobra, Mac OS X as ATOMac.
- LDTP is now known to work on Windows / Mac /Linux / Palm Source / Solaris / NetBSD / FreeBSD.
- LDTP can test any .NET / GNOME / KDE (QT >= 4.8) application which are accessibility enabled, Mozilla, Open Office/Libre Office, any Java application (should have a UI based on swing)
Cross Platform GUI Test Automation tool Linux version is LDTP, Windows version is Cobra and Mac version is PyATOM + ATOMac.
About testing
Why testing ?
Testing is a process to identify defects in a (software) system. For more information -
Why automation ?
Testing an application multiple times with same steps, automated process can do a better job.
Complexity of GUI testing ?
- Identification of object in a window (push button, menu item)
- Should be context sensitive (Window specific operation)
- Handling of unexpected pop-up windows
- Keeping the test script in sync with the UI changes
- Failures has to be verified on each operation
- Rendering of images / text in the display area
What type of testing can be done using LDTP ?
LDTP can be used to test the functionality of any accessibility enabled application.
Advantage of accessibility based testing
• Accessibility libraries provide applications property, state, its child items etc.
• No need to work in toolkit (GTK, AWT, QT) level
LDTP Features
- LDTP concepts are derived from Software Automation Framework Support
- LDTP supports verification of actions performed (guiexist, verifystate, etc) - API Reference
- Writing test scripts are very easy, the script writer need not know about the object hierarchy
- CPU / Memory performance monitoring of application-under-test can be measured - Class pstats
################## AUTOMATION IN MAC #######################
Now its time to learn the automation in MAC OS X. Before that make sure you the following app and setup to test your
Prerequisite
- Install Xcode with UNIX development option
- sudo easy_install atomac [or] sudo pip atomac
sudo easy_install atomac [or] sudo pip atomac
3. Install Pycharm IDE for running python
Enabling Accessibility feature and adding the app in it.
To reach Accessibility,
choose
System Preferences --> Security & Privacy --> Accessibility
We should add Terminal.app or PyCharm CE.app to Accessibility to get UI controls in the Mac Product.
We can add app directly by dragging the app to Accessibility
or
by adding the entries directly to the TCC.db where Accessibility permissions for the app will be stored locally in machine.
sudo sqlite3 /Library/Application\ Support/com.apple.TCC/TCC.db "INSERT or REPLACE INTO access VALUES('kTCCServiceAccessibility','/usr/sbin/jamfAgent',1,1,1,NULL)"
For more learnings regarding accessibility, Jacob Salmela has written a good python tool to interact with TCC.db
+
__author__ = 'vijay' import os import atomac from atomac.AXKeyCodeConstants import * import time import commands from atomac import AXKeyCodeConstants # Open the Notes. Store the new content. Close it. Reopen and validate the stored content in the Note.app global log_dir global log_file global log_fp notes = 'com.apple.Notes' # CFBundleIdentifier def logger(msg=''): ''' TO Log the activites of the program ''' try: global log_fp global log_dir global log_file if os.path.isfile(log_file): log_fp.write("\n") print msg log_fp.write(msg) else: print "[+] Looks like the Log Directory or Log File is not found" print "[+] Creating log file and directory for the first time" os.system("mkdir -p "+log_dir) os.system("touch "+log_file) log_fp = open(log_file, 'a') log_fp.write("\n") print msg log_fp.write(msg) except Exception as er: print "[-] Exception occurred while calling the logger " print "[-] Error is "+str(er) def open_app(cbundleID): # cbundleID = 'com.apple.Notes' ''' To Open the Notes App ''' try: logger("[+] Opening the Notes.app") atomac.launchAppByBundleId(cbundleID) time.sleep(3) notes_id = atomac.getAppRefByBundleId(cbundleID) notes_id.activate() except Exception as er: logger("[-] Exception while opening the notes app") logger("[-] Error @open_app is "+str(er)) def close_app(cbundleID): # cbundleID = 'com.apple.Notes' ''' To Close the Notes App ''' try: logger("[+] Closing the Notes.app") atomac.terminateAppByBundleId(cbundleID) logger("Force Killing the process with signal") os.system("ps -eaf|grep -i 'Notes'|grep -v grep|awk '{print $2}'|xargs kill ") time.sleep(3) except Exception as er: logger("[-] Exception while closing the notes app") logger("[-] Error @close_app is "+str(er)) def click_left(note): ''' mouse left click action ''' position = note.AXPosition size = note.AXSize clickpoint = ((position[0] + size[0] / 2), (position[1] + size[1] / 2)) try: note.clickMouseButtonLeft(clickpoint) except Exception as er: logger("[-] Error @add_notes") logger('[-] Error is '+str(er)) def click_right(note): ''' mouse right click action ''' position = note.AXPosition size = note.AXSize clickpoint = ((position[0] + size[0] / 2), (position[1] + size[1] / 2)) try: note.clickMouseButtonRight(clickpoint) except Exception as er: logger("[-] Error @add_notes") logger('[-] Error is '+str(er)) def add_notes(): ''' Click Add Notes button ''' try: notes_id = atomac.getAppRefByBundleId(notes) note = notes_id.windows()[0].findAllR(AXRole='AXButton', AXRoleDescription='button', AXDescription='add')[0] click_left(note) except Exception as er: logger("[-] Exception while adding the notes") logger("[-] Error @add_notes is "+str(er)) def edit_notes(msg_note=''): ''' Enter the input given to the notes ''' try: notes_id = atomac.getAppRefByBundleId(notes) notes_id.activate() note = notes_id.windows()[0].findAllR(AXRole='AXWebArea', AXRoleDescription='HTML content', AXDescription='')[0] click_left(note) note.sendKeys(msg_note) time.sleep(1) note.sendKeys('\n Vijay Anand Pandian ') time.sleep(2) note.sendKeyWithModifiers(";", [SHIFT]) note.sendKeyWithModifiers("0", [SHIFT]) time.sleep(3) except Exception as er: logger("[-] Exception while editing the notes") logger("[-] Error @edit_notes is "+str(er)) def validate_notes(msg_validate=''): ''' simple validation of UI function ''' try: notes_id = atomac.getAppRefByBundleId(notes) notes_id.activate() check = True if len(msg_validate) > 1: for i in range(len(notes_id.windows()[0].staticTextsR())): try: text = str(notes_id.windows()[0].staticTextsR()[i].AXValue.replace(u"\xa0", u"")) if msg_validate == text: logger("Validation Succesfull") print "Match is Found: "+text check = False break except: pass if check: logger("Validation Failed") print "Match not Found" else: logger("Enter the correct value to validate") except Exception as er: logger("[-] Exception while validating the notes") logger("[-] Error @validate_notes is "+str(er)) # # def delete_notes(note_delete=''): # try: # notes_id = atomac.getAppRefByBundleId(notes) # notes_id.activate() # if len(note_delete) > 1: # #click_right(notes_id.windows()[0].findAllR(AXRole='AXStaticText', AXRoleDescription='text', AXValue=note_delete+'\xa0')[0]) # time.sleep(1) # click_left(notes_id.windows()[0].findAllR(AXRole='AXStaticText', AXRoleDescription='text', AXValue=note_delete+'\xa0')[0]) # time.sleep(1) # notes_id.windows()[0].findAllR(AXRole='AXStaticText', AXRoleDescription='text', AXValue=note_delete+'\xa0')[0].sendKeyWithModifiers('q',[DELETE]) # time.sleep(1) # # click_left(notes_id.windows()[0].findAllR(AXRole='AXMenuItem', AXRoleDescription='menu item', AXTitle='Delete')[0]) # # time.sleep(1) # # click_left(notes_id.windows()[0].findAllR(AXRole='AXButton', AXRoleDescription='button', AXTitle='Delete Note')[0]) # # time.sleep(1) # else: # logger("Enter the correct value to validate") # except Exception as er: # logger("[-] Exception while deleting the notes") # logger("[-] Error @deleting_notes is "+str(er)) def main(): global log_dir global log_file global log_fp status, username = commands.getstatusoutput("users") log_dir = '/Users/'+username+'/log/notes_test/' log_file = '/Users/'+username+'/log/notes_test/notes_test.log' try: log_fp = open(log_file, 'a') except: pass if __name__ == "__main__": main() open_app(notes) time.sleep(3) add_notes() time.sleep(1) msg_note="""FirstNote \n \n Hi This is My First Tutorial on ATOMac \n \n Glad You found this \n \n \n """ edit_notes(msg_note) add_notes() time.sleep(1) msg_note="""SecondNote \n \n Hi This is My First Tutorial on ATOMac \n \n Glad You found this second time \n \n \n """ edit_notes(msg_note) validate_notes('FirstNote') time.sleep(2) validate_notes('SecondNote') time.sleep(2) # delete_notes('FirstNote') # time.sleep(5) # delete_notes('SecondNote') # time.sleep(5) close_app(notes) logger("[*] Successfully opened and closed the Notes.app")
Thanks for guide and mentioning tccutil.py, that is one awesome tool!
ReplyDeleteYou are Welcome :). If you need a framework for automation. Here is my open source projecct for UI automation esp for Accessibility enabled software on Mac OS X. https://github.com/vijayanandrp/Automation---Mac-OS-X---Applications
ReplyDeleteHi
ReplyDeleteCan you tell me how to get all the buttons in the screen