├── .gitignore ├── CHANGELOG.txt ├── COPYING.txt ├── MANIFEST.in ├── README.rst ├── atomac ├── AXCallbacks.py ├── AXClasses.py ├── AXKeyCodeConstants.py ├── AXKeyboard.py ├── Clipboard.py ├── Prefs.py ├── __init__.py ├── _a11y │ ├── a11ymodule.c │ ├── axlib.c │ ├── axlib.h │ ├── conversion.c │ └── conversion.h ├── ldtp │ ├── __init__.py │ ├── client.py │ ├── client_exception.py │ ├── log.py │ └── state.py ├── ldtpd │ ├── __init__.py │ ├── combo_box.py │ ├── constants.py │ ├── core.py │ ├── generic.py │ ├── keypress_actions.py │ ├── menu.py │ ├── mouse.py │ ├── page_tab_list.py │ ├── server_exception.py │ ├── table.py │ ├── test.py │ ├── text.py │ ├── utils.py │ └── value.py ├── ooldtp │ ├── __init__.py │ ├── client_exception.py │ ├── log.py │ └── state.py └── version.py ├── doc ├── Makefile ├── apireference.rst ├── conf.py ├── gettingstarted.rst ├── index.rst └── usage.rst ├── setup.py └── upload.sh /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | *.so 3 | 4 | *.egg 5 | *.egg-info 6 | dist 7 | build 8 | eggs 9 | parts 10 | bin 11 | var 12 | sdist 13 | develop-eggs 14 | MANIFEST 15 | .installed.cfg 16 | *.swp 17 | doc/_build/ 18 | .DS_Store 19 | pip-log.txt 20 | *~ 21 | .idea 22 | atomac/scripts 23 | -------------------------------------------------------------------------------- /CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | Version 1.1.1 2 | ??-???-2013 3 | Core ATOMac changes: 4 | 5 | LDTP related changes: 6 | 7 | Version 1.1.0 8 | 13-Feb-2013 9 | Core ATOMac changes: 10 | 11 | * [Nagappan Alagappan] Resolved a bug in getFrontmostApp that occurs when 12 | Google Chrome is running 13 | * [James Tatum] Calls to global keyboard events no longer try to get app PSN 14 | (issue 48) 15 | 16 | LDTP related changes: 17 | 18 | * [ebass] LDTP client updated to require Python 2.6 19 | * [Nagappan Alagappan] Minor LDTP client and server bug fixes 20 | * [Nagappan Alagappan] LDTP debugging can be enabled by setting the LDTP_DEBUG 21 | environment variable 22 | * [Nagappan Alagappan] Fixed an LDTP error involving unicode strings in 23 | getapplist() 24 | * [Nagappan Alagappan] Added automatic object refreshing in getwindowlist() 25 | and getobjectlist() 26 | * [Nagappan Alagappan] Added LDTP support for new object types popup button 27 | and application 28 | * [Nagappan Alagappan] New LDTP process stat monitoring support (same as 29 | Linux) 30 | * [Nagappan Alagappan] Can now manipulate menus in applications that have 31 | no open window (by using appApplicationName as the window name) 32 | * [Nagappan Alagappan] Fixed doubleclick() 33 | * [Nagappan Alagappan] Resolved some issues with click() on 10.7 34 | * [Nagappan Alagappan] gettextvalue() arguments updated for compatibility 35 | * [Nagappan Alagappan] AXColumn, AXHeading and AXGenericElement objects now 36 | supported in LDTP as col, tch and gen, respectively 37 | * [Nagappan Alagappan] Automatically click on menus when trying to access 38 | contents 39 | * [Nagappan Alagappan] Applications that also have a menu in the notification 40 | area can now be automated 41 | * [Nagappan Alagappan] Some applications with certain common UI elements that 42 | have poor accessibility implementations were throwing exceptions in 43 | _populate_appmap 44 | * [Satpal Chander] enterstring() now supports strings 45 | * [Satpal Chander] keyboard functions didn't work with uppercase characters 46 | * [Nagappan Alagappan] Python 3 support for LDTP client 47 | * [Nagappan Alagappan] closewindow(), minimizewindow(), maximizewindow(), and 48 | activatewindow() now supported 49 | * [Nagappan Alagappan] selectitem() and selectindex() now works with popup 50 | buttons 51 | * [Nagappan Alagappan] Windows that are closed and reopened now work properly 52 | * [Nagappan Alagappan] click() changed to use mouse events rather than 53 | accessibility 54 | * [Nagappan Alagappan] Added appundertest() which narrows down the search 55 | scope for UI elements to the specified application 56 | * ["John" Yingjun Li] Added getchild() which allows you to search the 57 | accessibility hierarchy for children of the specified element 58 | * [Nagappan Alagappan] Updated keyboard controls for parity with Linux LDTP 59 | * [Nagappan Alagappan] getaccesskey() added 60 | * [Nagappan Alagappan] Implemented verifyselect() and getcombovalue() 61 | 62 | Version 1.0.1 63 | 12-Oct-2012 64 | * [Nagappan Alagappan] Fixing 'Connection refused' error when importing ldtp 65 | 66 | Version 1.0.0 67 | 12-Oct-2012 68 | * [Nagappan Alagappan] Added LDTP client and server to ATOMac. See README 69 | for details. 70 | * [jonathanw] Fix sending function keys (issue 23) 71 | * [Andrew Wu] Documentation added (issue 20) 72 | * [Andrew Wu] sendKeys() now supports newlines (issue 22) 73 | * [James Tatum] Null attributes now return None rather than raising a 74 | atomac.Error exception (AXError -25212) (issue 27) 75 | * [James Tatum] CFRange attribute values like AXSelectedText are now supported 76 | (issue 29) 77 | * [James Tatum] Attribute values like AXSize and AXPosition now properly 78 | returned as floating point numbers 79 | * [Andrew Wu] atomac.getFrontmostApp() - New class method to return the 80 | frontmost app 81 | 82 | Version 0.9.3 83 | 09-Jul-2011 84 | * New NativeUIElement convenience methods sliders() and slidersR() 85 | * Small update to Prefs to remove a seemingly unneeded exception handler 86 | * Project renamed from PyATOM to ATOMac - now listed in PyPI as atomac and 87 | module import matches - import atomac. Apologies for that. 88 | 89 | Version 0.9.2 90 | 06-Jul-2011 91 | NativeUIElement: 92 | * floating point values can now be read and written (such as AXValue on a 93 | slider control) 94 | * fixed a couple of improperly handled error conditions 95 | 96 | Version 0.9.1 97 | 27-May-2011 98 | 99 | NativeUIElement: 100 | * added sendGlobalKeyWithModifiers - used to press system wide shortcut keys 101 | * added terminateAppByBundleId 102 | * pressModifiers and ReleaseModifiers - hold modifier keys down rather than 103 | pressing them in concert with another key or mouse click 104 | * Small bug fixes 105 | 106 | Prefs: 107 | * New proxy class for manipulating application preferences before tests 108 | 109 | Thanks to: 110 | Andrew Wu, Ken Song, Julián Romero 111 | 112 | Version 0.9 113 | 29-Apr-2011 114 | 115 | * Initial release 116 | 117 | -------------------------------------------------------------------------------- /COPYING.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include COPYING.txt 3 | global-include *.h 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ================================= 2 | ATOMac - Automated Testing on Mac 3 | ================================= 4 | Introduction 5 | ============ 6 | We are pleased to introduce the first Python library to fully enable GUI testing of Mac applications via the Apple Accessibility API. This library was created out of desperation. Existing tools such as using appscript to send messages to accessibility objects are painful to write and slow to use. ATOMac has direct access to the API. It's fast and easy to use to write tests. 7 | 8 | Getting started 9 | =============== 10 | ATOMac requires a system running OS X and Xcode installed. It has been tested extensively on 10.6, 10.7, and 10.8. 10.5 may work. If you experience issues with ATOMac on a particular version of OS X, please open a ticket in the issue tracker. 11 | 12 | Systemwide accessibility must be enabled. Check the checkbox: System Preferences > Universal Access > Enable access for assistive devices. Failure to enable this will result in ErrorAPIDisabled exceptions during some module usage. 13 | 14 | Installation should be as simple as running the following command line, which will download, build and install ATOMac:: 15 | 16 | $ sudo easy_install atomac 17 | 18 | Usage 19 | ===== 20 | Once the atomac module is installed, you should be able to use it to launch an application:: 21 | 22 | >>> import atomac 23 | >>> atomac.launchAppByBundleId('com.apple.Automator') 24 | 25 | This should launch Automator. Next, get a reference to the UI Element for the application itself:: 26 | 27 | >>> automator = atomac.getAppRefByBundleId('com.apple.Automator') 28 | >>> automator 29 | 30 | 31 | Now, we can find objects in the accessibility hierarchy:: 32 | 33 | >>> window = automator.windows()[0] 34 | >>> window.AXTitle 35 | u'Untitled' 36 | >>> sheet = window.sheets()[0] 37 | 38 | Note that we retrieved an accessibility attribute from the Window object - AXTitle. ATOMac supports reading and writing of most attributes. Using Xcode's included accessibility inspector can provide a quick way to find these attributes. 39 | 40 | There is a shortcut for getting the sheet object which bypasses accessing it through the Window object - ATOMac can search all objects in the hierarchy:: 41 | 42 | >>> sheet = automator.sheetsR()[0] 43 | 44 | There are search methods for most types of accessibility objects. Each search method, such as ``windows``, has a corresponding recursive search function, such as ``windowsR``. The recursive search finds items that aren't just direct children, but children of children. These search methods can be given terms to identify specific elements. Note that * and ? can be used as wildcard match characters in all ATOMac search methods:: 45 | 46 | >>> close = sheet.buttons('Close')[0] 47 | 48 | ATOMac has a method to search for UI Elements that match any number of criteria. The criteria are accessibility attributes:: 49 | 50 | >>> close = sheet.findFirst(AXRole='AXButton', AXTitle='Close') 51 | 52 | ``FindFirst`` and ``FindFirstR`` return the first item found to match the criteria or None. ``FindAll`` and ``FindAllR`` return a list of all items that match the criteria or an empty list. 53 | 54 | Objects are fairly versatile. You can get a list of supported attributes and actions on an object:: 55 | 56 | >>> close.getAttributes() 57 | [u'AXRole', u'AXRoleDescription', u'AXHelp', u'AXEnabled', u'AXFocused', 58 | u'AXParent', u'AXWindow', u'AXTopLevelUIElement', u'AXPosition', u'AXSize', 59 | u'AXTitle'] 60 | >>> close.AXTitle 61 | u'Close' 62 | >>> close.getActions() 63 | [u'Press'] 64 | 65 | Performing an action is as natural as:: 66 | 67 | >>> close.Press() 68 | 69 | Any action can be triggered this way. 70 | 71 | LDTP 72 | ==== 73 | Starting with version 1.0.0, ATOMac now includes compatibility with LDTP, a cross platform automation library. This allows testers to write a single script that will automate test cases on Linux, Windows, and now Mac OS X. Information and documentation on LDTP can be found at the `LDTP home page`_. 74 | 75 | .. _`LDTP home page`: http://ldtp.freedesktop.org/ 76 | 77 | LDTP operation is virtually identical to the operation on Linux. The import mechanism is slightly different, since it is shipped with ATOMac. Cross platform scripts executing on the System Under Test should import the LDTP client as follows:: 78 | 79 | try: 80 | import ldtp 81 | except ImportError: 82 | import atomac.ldtp as ldtp 83 | 84 | In the future, the LDTP client may be broken out into a separate platform independent module to ameliorate this issue. 85 | 86 | Like the Linux platform, the LDTP daemon may be run on the SUT, enabling client/server testing by executing 'ldtp' at a shell prompt. See the LDTP documentation for more details on client/server operation. 87 | 88 | Todo and contributing 89 | ===================== 90 | Although ATOMac is fully functional and drives hundreds of automated test cases at VMware, we have a to-do list to make the project even better. 91 | 92 | * Formatting - this code is not currently PEP-8 compliant. 93 | * Better mouse handling - for example, a method to smoothly drag from one UI Element to another. 94 | * Cleanup the search methods - We could use currying to define all the search methods in AXClasses in a cleaner way. 95 | 96 | Feel free to submit pull requests against the project on Github. If you're interested in developing ATOMac itself, sign up to the pyatom-dev mailing list. 97 | 98 | See also 99 | ======== 100 | * The ATOMac `home page`_ 101 | * `Changelog`_ 102 | * `Mailing lists`_ 103 | * `Source code`_ on Github 104 | * `Issue tracker`_ 105 | 106 | .. _`home page`: http://pyatom.com 107 | .. _`changelog` : https://raw.github.com/pyatom/pyatom/master/CHANGELOG.txt 108 | .. _`mailing lists`: http://lists.pyatom.com 109 | .. _`source code`: https://github.com/pyatom/pyatom 110 | .. _`issue tracker`: https://github.com/pyatom/pyatom/issues 111 | 112 | License 113 | ======= 114 | 115 | ATOMac is released under the GNU General Public License. See COPYING.txt for more details. 116 | 117 | Authors 118 | ======= 119 | 120 | James Tatum , 121 | Andrew Wu, 122 | Jesse Mendonca, 123 | Ken Song, 124 | Nagappan Alagappan, 125 | Yingjun Li, 126 | 127 | And other contributors listed in the CHANGELOG file. Thank you so much! 128 | -------------------------------------------------------------------------------- /atomac/AXCallbacks.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2010 VMware, Inc. All Rights Reserved. 2 | 3 | # This file is part of ATOMac. 4 | 5 | # ATOMac is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by the Free 7 | # Software Foundation version 2 and no later version. 8 | 9 | # ATOMac is distributed in the hope that it will be useful, but 10 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License version 2 12 | # for more details. 13 | 14 | # You should have received a copy of the GNU General Public License along with 15 | # this program; if not, write to the Free Software Foundation, Inc., 51 Franklin 16 | # St, Fifth Floor, Boston, MA 02110-1301 USA. 17 | 18 | def elemDisappearedCallback(retelem, obj, **kwargs): 19 | ''' Callback for checking if a UI element is no longer onscreen 20 | 21 | kwargs should contains some unique set of identifier (e.g. title / 22 | value, role) 23 | Returns: Boolean 24 | ''' 25 | return (not obj.findFirstR(**kwargs)) 26 | 27 | 28 | def returnElemCallback(retelem): 29 | ''' Callback for when a sheet appears 30 | 31 | Returns: element returned by observer callback 32 | ''' 33 | return retelem 34 | -------------------------------------------------------------------------------- /atomac/AXKeyCodeConstants.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2010 VMware, Inc. All Rights Reserved. 2 | 3 | # This file is part of ATOMac. 4 | 5 | # ATOMac is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by the Free 7 | # Software Foundation version 2 and no later version. 8 | 9 | # ATOMac is distributed in the hope that it will be useful, but 10 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License version 2 12 | # for more details. 13 | 14 | # You should have received a copy of the GNU General Public License along with 15 | # this program; if not, write to the Free Software Foundation, Inc., 51 Franklin 16 | # St, Fifth Floor, Boston, MA 02110-1301 USA. 17 | 18 | # Special keys 19 | TAB = '' 20 | RETURN = '' 21 | SPACE = '' 22 | ESCAPE = '' 23 | CAPS_LOCK = '' 24 | DELETE = '' 25 | NUM_LOCK = '' 26 | SCROLL_LOCK = '' 27 | PAUSE = '' 28 | BACKSPACE = '' 29 | INSERT = '' 30 | 31 | # Cursor movement 32 | UP = '' 33 | DOWN = '' 34 | LEFT = '' 35 | RIGHT = '' 36 | PAGE_UP = '' 37 | PAGE_DOWN = '' 38 | HOME = '' 39 | END = '' 40 | 41 | # Numeric keypad 42 | NUM_0 = '' 43 | NUM_1 = '' 44 | NUM_2 = '' 45 | NUM_3 = '' 46 | NUM_4 = '' 47 | NUM_5 = '' 48 | NUM_6 = '' 49 | NUM_7 = '' 50 | NUM_8 = '' 51 | NUM_9 = '' 52 | NUM_ENTER = '' 53 | NUM_PERIOD = '' 54 | NUM_PLUS = '' 55 | NUM_MINUS = '' 56 | NUM_MULTIPLY = '' 57 | NUM_DIVIDE = '' 58 | 59 | # Function keys 60 | F1 = '' 61 | F2 = '' 62 | F3 = '' 63 | F4 = '' 64 | F5 = '' 65 | F6 = '' 66 | F7 = '' 67 | F8 = '' 68 | F9 = '' 69 | F10 = '' 70 | F11 = '' 71 | F12 = '' 72 | 73 | # Modifier keys 74 | COMMAND_L = '' 75 | SHIFT_L = '' 76 | OPTION_L = '' 77 | CONTROL_L = '' 78 | 79 | COMMAND_R = '' 80 | SHIFT_R = '' 81 | OPTION_R = '' 82 | CONTROL_R = '' 83 | 84 | # Default modifier keys -> left: 85 | COMMAND = COMMAND_L 86 | SHIFT = SHIFT_L 87 | OPTION = OPTION_L 88 | CONTROL = CONTROL_L 89 | 90 | 91 | # Define a dictionary representing characters mapped to their virtual key codes 92 | # Lifted from the mappings found in kbdptr.h in the osxvnc project 93 | # Mapping is: character -> virtual keycode for each character / symbol / key 94 | # as noted below 95 | 96 | US_keyboard = { 97 | # Letters 98 | 'a': 0, 99 | 'b': 11, 100 | 'c': 8, 101 | 'd': 2, 102 | 'e': 14, 103 | 'f': 3, 104 | 'g': 5, 105 | 'h': 4, 106 | 'i': 34, 107 | 'j': 38, 108 | 'k': 40, 109 | 'l': 37, 110 | 'm': 46, 111 | 'n': 45, 112 | 'o': 31, 113 | 'p': 35, 114 | 'q': 12, 115 | 'r': 15, 116 | 's': 1, 117 | 't': 17, 118 | 'u': 32, 119 | 'v': 9, 120 | 'w': 13, 121 | 'x': 7, 122 | 'y': 16, 123 | 'z': 6, 124 | 125 | # Numbers 126 | '0': 29, 127 | '1': 18, 128 | '2': 19, 129 | '3': 20, 130 | '4': 21, 131 | '5': 23, 132 | '6': 22, 133 | '7': 26, 134 | '8': 28, 135 | '9': 25, 136 | 137 | # Symbols 138 | '!': 18, 139 | '@': 19, 140 | '#': 20, 141 | '$': 21, 142 | '%': 23, 143 | '^': 22, 144 | '&': 26, 145 | '*': 28, 146 | '(': 25, 147 | ')': 29, 148 | '-': 27, # Dash 149 | '_': 27, # Underscore 150 | '=': 24, 151 | '+': 24, 152 | '`': 50, # Backtick 153 | '~': 50, 154 | '[': 33, 155 | ']': 30, 156 | '{': 33, 157 | '}': 30, 158 | ';': 41, 159 | ':': 41, 160 | "'": 39, 161 | '"': 39, 162 | ',': 43, 163 | '<': 43, 164 | '.': 47, 165 | '>': 47, 166 | '/': 44, 167 | '?': 44, 168 | '\\': 42, 169 | '|': 42, # Pipe 170 | TAB: 48, # Tab: Shift-Tab sent for Tab 171 | SPACE: 49, 172 | ' ': 49, # Space 173 | 174 | # Characters that on the US keyboard require use with Shift 175 | 'upperSymbols': [ 176 | '!', 177 | '@', 178 | '#', 179 | '$', 180 | '%', 181 | '^', 182 | '&', 183 | '*', 184 | '(', 185 | ')', 186 | '_', 187 | '+', 188 | '~', 189 | '{', 190 | '}', 191 | ':', 192 | '"', 193 | '<', 194 | '>', 195 | '?', 196 | '|', 197 | ] 198 | } 199 | 200 | 201 | # Mapping for special (meta) keys 202 | specialKeys = { 203 | # Special keys 204 | RETURN: 36, 205 | DELETE: 117, 206 | TAB: 48, 207 | SPACE: 49, 208 | ESCAPE: 53, 209 | CAPS_LOCK: 57, 210 | NUM_LOCK: 71, 211 | SCROLL_LOCK: 107, 212 | PAUSE: 113, 213 | BACKSPACE: 51, 214 | INSERT: 114, 215 | 216 | # Cursor movement 217 | UP: 126, 218 | DOWN: 125, 219 | LEFT: 123, 220 | RIGHT: 124, 221 | PAGE_UP: 116, 222 | PAGE_DOWN: 121, 223 | 224 | # Numeric keypad 225 | NUM_0: 82, 226 | NUM_1: 83, 227 | NUM_2: 84, 228 | NUM_3: 85, 229 | NUM_4: 86, 230 | NUM_5: 87, 231 | NUM_6: 88, 232 | NUM_7: 89, 233 | NUM_8: 91, 234 | NUM_9: 92, 235 | NUM_ENTER: 76, 236 | NUM_PERIOD: 65, 237 | NUM_PLUS: 69, 238 | NUM_MINUS: 78, 239 | NUM_MULTIPLY: 67, 240 | NUM_DIVIDE: 75, 241 | 242 | # Function keys 243 | F1: 122, 244 | F2: 120, 245 | F3: 99, 246 | F4: 118, 247 | F5: 96, 248 | F6: 97, 249 | F7: 98, 250 | F8: 100, 251 | F9: 101, 252 | F10: 109, 253 | F11: 103, 254 | F12: 111, 255 | 256 | # Modifier keys 257 | COMMAND_L: 55, 258 | SHIFT_L: 56, 259 | OPTION_L: 58, 260 | CONTROL_L: 59, 261 | 262 | COMMAND_R: 54, 263 | SHIFT_R: 60, 264 | OPTION_R: 61, 265 | CONTROL_R: 62, 266 | } 267 | 268 | # Default keyboard layout 269 | DEFAULT_KEYBOARD = US_keyboard 270 | -------------------------------------------------------------------------------- /atomac/AXKeyboard.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2010 VMware, Inc. All Rights Reserved. 2 | 3 | # This file is part of ATOMac. 4 | 5 | # ATOMac is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by the Free 7 | # Software Foundation version 2 and no later version. 8 | 9 | # ATOMac is distributed in the hope that it will be useful, but 10 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License version 2 12 | # for more details. 13 | 14 | # You should have received a copy of the GNU General Public License along with 15 | # this program; if not, write to the Free Software Foundation, Inc., 51 Franklin 16 | # St, Fifth Floor, Boston, MA 02110-1301 USA. 17 | 18 | import Quartz 19 | 20 | from AXKeyCodeConstants import * 21 | 22 | 23 | # Based on the flags provided in the Quartz documentation it does not seem 24 | # that we can distinguish between left and right modifier keys, even though 25 | # there are different virtual key codes offered between the two sets. 26 | # Thus for now we offer only a generic modifier key set w/o L-R distinction. 27 | modKeyFlagConstants = { 28 | COMMAND: Quartz.kCGEventFlagMaskCommand, 29 | SHIFT: Quartz.kCGEventFlagMaskShift, 30 | OPTION: Quartz.kCGEventFlagMaskAlternate, 31 | CONTROL: Quartz.kCGEventFlagMaskControl, 32 | } 33 | 34 | 35 | def loadKeyboard(): 36 | ''' Load a given keyboard mapping (of characters to virtual key codes) 37 | 38 | Default is US keyboard 39 | Parameters: None (relies on the internationalization settings) 40 | Returns: A dictionary representing the current keyboard mapping (of 41 | characters to keycodes) 42 | ''' 43 | # Currently assumes US keyboard 44 | keyboardLayout = {} 45 | keyboardLayout = DEFAULT_KEYBOARD 46 | keyboardLayout.update(specialKeys) 47 | 48 | return keyboardLayout 49 | -------------------------------------------------------------------------------- /atomac/Clipboard.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2010 VMware, Inc. All Rights Reserved. 2 | 3 | # This file is part of ATOMac. 4 | 5 | # ATOMac is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by the Free 7 | # Software Foundation version 2 and no later version. 8 | 9 | # ATOMac is distributed in the hope that it will be useful, but 10 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License version 2 12 | # for more details. 13 | 14 | # You should have received a copy of the GNU General Public License along with 15 | # this program; if not, write to the Free Software Foundation, Inc., 51 Franklin 16 | # St, Fifth Floor, Boston, MA 02110-1301 USA. 17 | 18 | import types 19 | import AppKit 20 | import pprint 21 | import logging 22 | import Foundation 23 | 24 | 25 | class Clipboard(object): 26 | ''' Class to represent clipboard-related operations for text ''' 27 | 28 | # String encoding type 29 | utf8encoding = Foundation.NSUTF8StringEncoding 30 | 31 | # Class attributes to distinguish types of data: 32 | # Reference: 33 | # http://developer.apple.com/mac/library/documentation/Cocoa/Reference/ 34 | # ApplicationKit/Classes/NSPasteboard_Class/Reference/Reference.html 35 | 36 | # Text data type 37 | STRING = AppKit.NSStringPboardType 38 | # Rich-text format data type (e.g. rtf documents) 39 | RTF = AppKit.NSRTFPboardType 40 | # Image datatype (e.g. tiff) 41 | IMAGE = AppKit.NSTIFFPboardType 42 | # URL data type (not just web but file locations also) 43 | URL = AppKit.NSURLPboardType 44 | # Color datatype - not sure if we'll have to use this one 45 | # Supposedly replaced in 10.6 but the pyobjc AppKit module doesn't have the 46 | # new data type as an attribute 47 | COLOR = AppKit.NSColorPboardType 48 | 49 | # You can extend this list of data types 50 | # e.g. File copy and paste between host and guest 51 | # Not sure if text copy and paste between host and guest falls under STRING/ 52 | # RTF or not 53 | # List of PboardTypes I found in AppKit: 54 | # NSColorPboardType 55 | # NSCreateFileContentsPboardType 56 | # NSCreateFilenamePboardType 57 | # NSDragPboard 58 | # NSFileContentsPboardType 59 | # NSFilenamesPboardType 60 | # NSFilesPromisePboardType 61 | # NSFindPanelSearchOptionsPboardType 62 | # NSFindPboard 63 | # NSFontPboard 64 | # NSFontPboardType 65 | # NSGeneralPboard 66 | # NSHTMLPboardType 67 | # NSInkTextPboardType 68 | # NSMultipleTextSelectionPboardType 69 | # NSPDFPboardType 70 | # NSPICTPboardType 71 | # NSPostScriptPboardType 72 | # NSRTFDPboardType 73 | # NSRTFPboardType 74 | # NSRulerPboard 75 | # NSRulerPboardType 76 | # NSSoundPboardType 77 | # NSStringPboardType 78 | # NSTIFFPboardType 79 | # NSTabularTextPboardType 80 | # NSURLPboardType 81 | # NSVCardPboardType 82 | 83 | @classmethod 84 | def paste(cls): 85 | ''' Method to get the clipboard data ('Paste') 86 | 87 | Returns: Data (string) retrieved or None if empty. Exceptions from 88 | AppKit will be handled by caller. 89 | ''' 90 | data = None 91 | 92 | pb = AppKit.NSPasteboard.generalPasteboard() 93 | 94 | # If we allow for multiple data types (e.g. a list of data types) 95 | # we will have to add a condition to check just the first in the 96 | # list of datatypes) 97 | data = pb.stringForType_(cls.STRING) 98 | return data 99 | 100 | @classmethod 101 | def copy(cls, data): 102 | ''' Method to set the clipboard data ('Copy') 103 | 104 | Parameters: data to set (string) 105 | Optional: datatype if it's not a string 106 | Returns: True / False on successful copy, Any exception raised (like 107 | passes the NSPasteboardCommunicationError) should be caught 108 | by the caller. 109 | ''' 110 | pp = pprint.PrettyPrinter() 111 | 112 | copyData = 'Data to copy (put in pasteboard): %s' 113 | logging.debug(copyData % pp.pformat(data)) 114 | 115 | # Clear the pasteboard first: 116 | cleared = cls.clearAll() 117 | if (not cleared): 118 | logging.warning('Clipboard could not clear properly') 119 | return False 120 | 121 | # Prepare to write the data 122 | # If we just use writeObjects the sequence to write to the clipboard is 123 | # a) Call clearContents() 124 | # b) Call writeObjects() with a list of objects to write to the clipboard 125 | if (type(data) is not types.ListType): 126 | data = [data] 127 | 128 | pb = AppKit.NSPasteboard.generalPasteboard() 129 | pbSetOk = pb.writeObjects_(data) 130 | 131 | return bool(pbSetOk) 132 | 133 | @classmethod 134 | def clearContents(cls): 135 | ''' Clear contents of general pasteboard 136 | 137 | Future enhancement can include specifying which clipboard to clear 138 | Returns: True on success; caller should expect to catch exceptions, 139 | probably from AppKit (ValueError) 140 | ''' 141 | logMsg = 'Request to clear contents of pasteboard: general' 142 | logging.debug(logMsg) 143 | pb = AppKit.NSPasteboard.generalPasteboard() 144 | pb.clearContents() 145 | return True 146 | 147 | @classmethod 148 | def clearProperties(cls): 149 | ''' Clear properties of general pasteboard 150 | 151 | Future enhancement can include specifying which clipboard's properties 152 | to clear 153 | Returns: True on success; caller should catch exceptions raised, 154 | e.g. from AppKit (ValueError) 155 | ''' 156 | logMsg = 'Request to clear properties of pasteboard: general' 157 | logging.debug(logMsg) 158 | pb = AppKit.NSPasteboard.generalPasteboard() 159 | pb.clearProperties() 160 | 161 | return True 162 | 163 | @classmethod 164 | def clearAll(cls): 165 | ''' Clear contents and properties of general pasteboard 166 | 167 | Future enhancement can include specifying which clipboard's properties 168 | to clear 169 | Returns: Boolean True on success; caller should handle exceptions 170 | ''' 171 | contentsCleared = cls.clearContents() 172 | propsCleared = cls.clearProperties() 173 | 174 | return True 175 | 176 | @classmethod 177 | def isEmpty(cls, datatype=None): 178 | ''' Method to test if the general pasteboard is empty or not with respect 179 | to the type of object you want 180 | 181 | Parameters: datatype (defaults to strings) 182 | Returns: Boolean True (empty) / False (has contents); Raises 183 | exception (passes any raised up) 184 | ''' 185 | if (not datatype): 186 | datatype = AppKit.NSString 187 | if (type(datatype) is not types.ListType): 188 | datatype = [datatype] 189 | pp = pprint.PrettyPrinter() 190 | logging.debug('Desired datatypes: %s' % pp.pformat(datatype)) 191 | optDict = {} 192 | logging.debug('Results filter is: %s' % pp.pformat(optDict)) 193 | 194 | try: 195 | logMsg = 'Request to verify pasteboard is empty' 196 | logging.debug(logMsg) 197 | pb = AppKit.NSPasteboard.generalPasteboard() 198 | # canReadObjectForClasses_options_() seems to return an int (> 0 if 199 | # True) 200 | # Need to negate to get the sense we want (True if can not read the 201 | # data type from the pasteboard) 202 | itsEmpty = not bool(pb.canReadObjectForClasses_options_(datatype, 203 | optDict)) 204 | except ValueError, error: 205 | logging.error(error) 206 | raise 207 | 208 | return bool(itsEmpty) 209 | 210 | -------------------------------------------------------------------------------- /atomac/Prefs.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) 2011 Julián Romero. 4 | 5 | # This file is part of ATOMac. 6 | 7 | # ATOMac is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by the Free 9 | # Software Foundation version 2 and no later version. 10 | 11 | # ATOMac is distributed in the hope that it will be useful, but 12 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License version 2 14 | # for more details. 15 | 16 | # You should have received a copy of the GNU General Public License along with 17 | # this program; if not, write to the Free Software Foundation, Inc., 51 Franklin 18 | # St, Fifth Floor, Boston, MA 02110-1301 USA. 19 | 20 | from AppKit import NSWorkspace, NSUserDefaults, NSDictionary, NSMutableDictionary 21 | from UserDict import UserDict 22 | from os import path 23 | 24 | __all__ = ["Prefs"] 25 | 26 | class Prefs(UserDict): 27 | ''' NSUserDefaults proxy to read/write application preferences. 28 | It has been conceived to prepare the preferences before a test launch the app. 29 | Once a Prefs instance is created, it doesn't detect prefs changed elsewhere, 30 | so for now you need to create the instance right before reading/writing a pref. 31 | Defaults.plist with default values is expected to exist on the app bundle. 32 | 33 | p = Prefs('com.example.App') 34 | coolStuff = p['CoolStuff'] 35 | p['CoolStuff'] = newCoolStuff 36 | 37 | ''' 38 | def __init__(self, bundleID, bundlePath=None, defaultsPlistName='Defaults'): 39 | ''' bundleId: the application bundle identifier 40 | bundlePath: the full bundle path (useful to test a Debug build) 41 | defaultsPlistName: the name of the plist that contains default values 42 | ''' 43 | self.__bundleID = bundleID 44 | self.__bundlePath = bundlePath 45 | UserDict.__init__(self) 46 | self.__setup(defaultsPlistName) 47 | 48 | def __setup(self, defaultsPlistName=None): 49 | NSUserDefaults.resetStandardUserDefaults() 50 | prefs = NSUserDefaults.standardUserDefaults() 51 | self.defaults = self.__defaults(defaultsPlistName) 52 | domainData = prefs.persistentDomainForName_(self.__bundleID) 53 | if domainData: 54 | self.data = domainData 55 | else: 56 | self.data = NSDictionary.dictionary() 57 | 58 | def __defaults(self, plistName='Defaults'): 59 | if self.__bundlePath is None: 60 | self.__bundlePath = NSWorkspace.sharedWorkspace().absolutePathForAppBundleWithIdentifier_(self.__bundleID) 61 | if self.__bundlePath: 62 | plistPath = path.join(self.__bundlePath, "Contents/Resources/%s.plist" % plistName) 63 | plist = NSDictionary.dictionaryWithContentsOfFile_(plistPath) 64 | if plist: 65 | return plist 66 | return NSDictionary.dictionary() 67 | 68 | def get(self, key): 69 | return self.__getitem__(key) 70 | 71 | def __getitem__(self, key): 72 | result = self.data.get(key, None) 73 | if result is None or result == '': 74 | if self.defaults: 75 | result = self.defaults.get(key, None) 76 | return result 77 | 78 | def set(self, key, value): 79 | self.__setitem__(key, value) 80 | 81 | def __setitem__(self, key, value): 82 | mutableData = self.data.mutableCopy() 83 | mutableData[key] = value 84 | self.data = mutableData 85 | prefs = NSUserDefaults.standardUserDefaults() 86 | prefs.setPersistentDomain_forName_(self.data, self.__bundleID) 87 | 88 | -------------------------------------------------------------------------------- /atomac/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2010 VMware, Inc. All Rights Reserved. 2 | 3 | # This file is part of ATOMac. 4 | 5 | # ATOMac is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by the Free 7 | # Software Foundation version 2 and no later version. 8 | 9 | # ATOMac is distributed in the hope that it will be useful, but 10 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License version 2 12 | # for more details. 13 | 14 | # You should have received a copy of the GNU General Public License along with 15 | # this program; if not, write to the Free Software Foundation, Inc., 51 Franklin 16 | # St, Fifth Floor, Boston, MA 02110-1301 USA. 17 | 18 | from _a11y import Error, ErrorAPIDisabled, ErrorInvalidUIElement, ErrorCannotComplete, ErrorUnsupported, ErrorNotImplemented 19 | from AXClasses import NativeUIElement 20 | from Clipboard import Clipboard 21 | from version import __version__ 22 | from Prefs import Prefs 23 | 24 | 25 | getAppRefByLocalizedName = NativeUIElement.getAppRefByLocalizedName 26 | terminateAppByBundleId = NativeUIElement.terminateAppByBundleId 27 | launchAppByBundlePath = NativeUIElement.launchAppByBundlePath 28 | setSystemWideTimeout = NativeUIElement.setSystemWideTimeout 29 | getAppRefByBundleId = NativeUIElement.getAppRefByBundleId 30 | launchAppByBundleId = NativeUIElement.launchAppByBundleId 31 | getFrontmostApp = NativeUIElement.getFrontmostApp 32 | getAppRefByPid = NativeUIElement.getAppRefByPid 33 | -------------------------------------------------------------------------------- /atomac/_a11y/axlib.c: -------------------------------------------------------------------------------- 1 | /* ********************************************************** 2 | * Copyright (C) 2010 VMware, Inc. All rights reserved. 3 | * **********************************************************/ 4 | 5 | /* ************************************************************************** 6 | * This file is part of ATOMac. 7 | * 8 | * ATOMac is free software; you can redistribute it and/or modify it under 9 | * the terms of the GNU General Public License as published by the Free 10 | * Software Foundation version 2 and no later version. 11 | * 12 | * ATOMac is distributed in the hope that it will be useful, but WITHOUT ANY 13 | * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 14 | * FOR A PARTICULAR PURPOSE. See the GNU General Public License version 2 15 | * for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License along with 18 | * this program; if not, write to the Free Software Foundation, Inc., 51 19 | * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 | * **************************************************************************/ 21 | 22 | /* 23 | * axlib.c -- 24 | * 25 | * Library of Apple accessibility functions used in ATOMac 26 | */ 27 | 28 | #include "axlib.h" 29 | 30 | /* 31 | *----------------------------------------------------------------------------- 32 | * 33 | * AXEnabled -- 34 | * 35 | * Determine whether a11y is enabled or not. 36 | * 37 | * Results: 38 | * TRUE if enabled, FALSE if not. 39 | * 40 | * Side effects: 41 | * None. 42 | * 43 | *----------------------------------------------------------------------------- 44 | */ 45 | 46 | Boolean 47 | AXEnabled(void) 48 | { 49 | if (AXAPIEnabled() != 0) { 50 | return TRUE; 51 | } else { 52 | return FALSE; 53 | } 54 | } 55 | 56 | /* 57 | *----------------------------------------------------------------------------- 58 | * 59 | * getFrontmostPID -- 60 | * 61 | * Get the PID of the front most (active) window from a11y 62 | * 63 | * Results: 64 | * PID of the front most process. 65 | * 66 | * Side effects: 67 | * None. 68 | * 69 | *----------------------------------------------------------------------------- 70 | */ 71 | 72 | pid_t 73 | getFrontmostPID(void) 74 | { 75 | pid_t pid; 76 | ProcessSerialNumber psn; 77 | 78 | GetFrontProcess(&psn); 79 | GetProcessPID(&psn, &pid); 80 | 81 | return pid; 82 | } 83 | 84 | /* 85 | *----------------------------------------------------------------------------- 86 | * 87 | * getFrontMostWindowTitle -- 88 | * 89 | * Function to get the title of the frontmost window of an app. 90 | * Deprecated - for demo only. See notes in a11ymodule.c. 91 | * 92 | * Results: 93 | * CFStringRef pointing to the title of the window. 94 | * 95 | * Side effects: 96 | * None. 97 | * 98 | *----------------------------------------------------------------------------- 99 | */ 100 | 101 | CFTypeRef 102 | getFrontMostWindowTitle(pid_t pid) 103 | { 104 | AXUIElementRef app; 105 | CFTypeRef windowTitle; 106 | CFTypeRef temp; 107 | 108 | app = AXUIElementCreateApplication(pid); 109 | // check for nulls 110 | AXUIElementCopyAttributeValue(app, kAXFocusedWindowAttribute, 111 | (CFTypeRef *)&temp 112 | ); 113 | AXUIElementCopyAttributeValue( 114 | temp, kAXTitleAttribute, (CFTypeRef *)&windowTitle 115 | ); 116 | 117 | return windowTitle; 118 | } 119 | -------------------------------------------------------------------------------- /atomac/_a11y/axlib.h: -------------------------------------------------------------------------------- 1 | /* ********************************************************** 2 | * Copyright 2010 VMware, Inc. All rights reserved. 3 | * **********************************************************/ 4 | 5 | /* ************************************************************************** 6 | * This file is part of ATOMac. 7 | * 8 | * ATOMac is free software; you can redistribute it and/or modify it under 9 | * the terms of the GNU General Public License as published by the Free 10 | * Software Foundation version 2 and no later version. 11 | * 12 | * ATOMac is distributed in the hope that it will be useful, but WITHOUT ANY 13 | * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 14 | * FOR A PARTICULAR PURPOSE. See the GNU General Public License version 2 15 | * for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License along with 18 | * this program; if not, write to the Free Software Foundation, Inc., 51 19 | * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 | * **************************************************************************/ 21 | 22 | /* 23 | * axlib.h -- 24 | * 25 | * Header file for apple accessibility functions. 26 | */ 27 | 28 | #ifndef _AXLIB_H_ 29 | #define _AXLIB_H_ 30 | 31 | #include 32 | #include 33 | 34 | /* 35 | * Global functions 36 | */ 37 | 38 | Boolean AXEnabled(void); 39 | pid_t getFrontmostPID(void); 40 | CFTypeRef getFrontMostWindowTitle(pid_t pid); 41 | 42 | #endif // ifndef _AXLIB_H_ 43 | -------------------------------------------------------------------------------- /atomac/_a11y/conversion.c: -------------------------------------------------------------------------------- 1 | /* ********************************************************** 2 | * Copyright (C) 2010 VMware, Inc. All rights reserved. 3 | * **********************************************************/ 4 | 5 | /* ************************************************************************** 6 | * This file is part of ATOMac. 7 | * 8 | * ATOMac is free software; you can redistribute it and/or modify it under 9 | * the terms of the GNU General Public License as published by the Free 10 | * Software Foundation version 2 and no later version. 11 | * 12 | * ATOMac is distributed in the hope that it will be useful, but WITHOUT ANY 13 | * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 14 | * FOR A PARTICULAR PURPOSE. See the GNU General Public License version 2 15 | * for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License along with 18 | * this program; if not, write to the Free Software Foundation, Inc., 51 19 | * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 | * **************************************************************************/ 21 | 22 | /* 23 | * conversion.c -- 24 | * 25 | * Functions to convert between Apple CoreFoundation objects and 26 | * Python objects. 27 | */ 28 | 29 | 30 | #include "conversion.h" 31 | 32 | /* 33 | * Local data 34 | */ 35 | 36 | /* 37 | * A note on ownership: 38 | * 39 | * Python and CF both have a notion of ownership. These functions convert from 40 | * type A to type B but do not touch ownership. So, if you pass in a CF string 41 | * and get a Python string back, you still own the CF string and ownership of 42 | * the Python string is given to you. You will need to account for this 43 | * somehow. Ownership should be documented in "side effects." Good luck! 44 | */ 45 | 46 | /* 47 | *----------------------------------------------------------------------------- 48 | * 49 | * CFStringRefToPyUnicode -- 50 | * 51 | * Convert an Apple CFString to a Python Unicode object 52 | * 53 | * Results: 54 | * Returns a Python Unicode string or NULL and a Python exception on 55 | * failure. 56 | * 57 | * Side effects: 58 | * Ownership of the Python string object is transferred to the caller. 59 | * Ownership of the CFString is unchanged. 60 | * 61 | *----------------------------------------------------------------------------- 62 | */ 63 | 64 | PyObject * 65 | CFStringRefToPyUnicode(CFStringRef source) // IN: CFString to convert 66 | { 67 | CFIndex buf_size; 68 | Boolean result; 69 | char *dest; 70 | PyObject *ret; 71 | 72 | buf_size = CFStringGetMaximumSizeForEncoding( 73 | CFStringGetLength(source), 74 | CFENCODING 75 | ) + 1; 76 | dest = (char *) PyMem_Malloc(buf_size); 77 | if (NULL == dest) { 78 | return NULL; 79 | } 80 | 81 | result = CFStringGetCString(source, dest, buf_size, CFENCODING); 82 | if (FALSE == result) { 83 | PyErr_SetString(PyExc_ValueError, 84 | "Error converting CFString to C string"); 85 | return NULL; 86 | } 87 | ret = PyUnicode_DecodeUTF8(dest, strlen(dest), NULL); 88 | PyMem_Free(dest); 89 | return ret; 90 | } 91 | 92 | /* 93 | *----------------------------------------------------------------------------- 94 | * 95 | * CGValueToPyTuple -- 96 | * 97 | * Convert a CG value (CGSize, CGPoint, or CFRange) to Python tuple 98 | * 99 | * Results: 100 | * Returns a Python Tuple or NULL and a Python exception on 101 | * failure. 102 | * 103 | * Side effects: 104 | * Ownership of the Python Tuple object is transferred to the caller. 105 | * 106 | *----------------------------------------------------------------------------- 107 | */ 108 | 109 | PyObject * 110 | CGValueToPyTuple(AXValueRef value) //IN: AXValueRef to convert 111 | { 112 | PyObject *tuple = PyTuple_New(2); 113 | 114 | if (kAXValueCGSizeType == AXValueGetType(value)) { 115 | CGSize size; 116 | double float1 = 0.0; 117 | double float2 = 0.0; 118 | if (AXValueGetValue(value,kAXValueCGSizeType,&size) == 0){ 119 | return NULL; 120 | } 121 | float1 = (double)size.width; 122 | float2 = (double)size.height; 123 | PyTuple_SetItem(tuple,0,Py_BuildValue("d",float1)); 124 | PyTuple_SetItem(tuple,1,Py_BuildValue("d",float2)); 125 | return tuple; 126 | } 127 | 128 | if (kAXValueCGPointType == AXValueGetType(value)){ 129 | CGPoint point; 130 | double float1 = 0.0; 131 | double float2 = 0.0; 132 | if (AXValueGetValue(value,kAXValueCGPointType,&point) == 0){ 133 | return NULL; 134 | } 135 | float1 = (double)point.x; 136 | float2 = (double)point.y; 137 | PyTuple_SetItem(tuple,0,Py_BuildValue("d",float1)); 138 | PyTuple_SetItem(tuple,1,Py_BuildValue("d",float2)); 139 | return tuple; 140 | } 141 | 142 | if (kAXValueCFRangeType == AXValueGetType(value)){ 143 | CFRange range; 144 | long index1 = 0; 145 | long index2 = 0; 146 | if (AXValueGetValue(value,kAXValueCFRangeType,&range) == 0){ 147 | return NULL; 148 | } 149 | index1 = range.location; 150 | index2 = range.length; 151 | PyTuple_SetItem(tuple,0,Py_BuildValue("l",index1)); 152 | PyTuple_SetItem(tuple,1,Py_BuildValue("l",index2)); 153 | return tuple; 154 | } 155 | 156 | // @@@TODO: Need to set a python exception here if not already set 157 | return NULL; 158 | } 159 | 160 | /* 161 | *----------------------------------------------------------------------------- 162 | * 163 | * PyUnicodeToCFStringRef -- 164 | * 165 | * Convert a Python Unicode object to an Apple CFString 166 | * 167 | * Results: 168 | * Returns an Apple CFStringRef or NULL and a Python exception on 169 | * failure. 170 | * 171 | * Side effects: 172 | * Ownership of the CFString is given to the caller. 173 | * Ownership of the Python string object is unchanged. 174 | * 175 | *----------------------------------------------------------------------------- 176 | */ 177 | 178 | CFStringRef 179 | PyUnicodeToCFStringRef(PyObject * source) // IN: Python string object 180 | { 181 | char *dest; 182 | PyObject *decoded; 183 | CFStringRef ret; 184 | 185 | if (PyUnicode_Check(source)) { 186 | decoded = PyUnicode_AsUTF8String(source); 187 | if (NULL == decoded) { 188 | return NULL; 189 | } 190 | dest = (char *) PyString_AsString(decoded); // dest is read-only here 191 | if (NULL == dest) { 192 | return NULL; 193 | } 194 | } else { 195 | dest = (char *) PyString_AsString(source); 196 | if (NULL == dest) { 197 | return NULL; 198 | } 199 | } 200 | 201 | ret = CFStringCreateWithCString(NULL, dest, CFENCODING); 202 | if (NULL == ret) 203 | { 204 | PyErr_SetString(PyExc_ValueError, 205 | "Error creating CFString from C string"); 206 | return NULL; 207 | } 208 | return ret; 209 | } 210 | -------------------------------------------------------------------------------- /atomac/_a11y/conversion.h: -------------------------------------------------------------------------------- 1 | /* ********************************************************** 2 | * Copyright 2010 VMware, Inc. All rights reserved. 3 | * **********************************************************/ 4 | 5 | /* ************************************************************************** 6 | * This file is part of ATOMac. 7 | * 8 | * ATOMac is free software; you can redistribute it and/or modify it under 9 | * the terms of the GNU General Public License as published by the Free 10 | * Software Foundation version 2 and no later version. 11 | * 12 | * ATOMac is distributed in the hope that it will be useful, but WITHOUT ANY 13 | * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 14 | * FOR A PARTICULAR PURPOSE. See the GNU General Public License version 2 15 | * for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License along with 18 | * this program; if not, write to the Free Software Foundation, Inc., 51 19 | * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 | * **************************************************************************/ 21 | 22 | /* 23 | * conversion.h -- 24 | * 25 | * Header file for CF to Python conversion library. 26 | */ 27 | 28 | #ifndef _CONVERSION_H_ 29 | #define _CONVERSION_H_ 30 | 31 | #include 32 | #include 33 | /* 34 | * Global constants 35 | */ 36 | 37 | /* 38 | * Let's standardize on UTF-8 as the intermediate format for strings. 39 | */ 40 | static const CFStringEncoding CFENCODING = kCFStringEncodingUTF8; 41 | 42 | /* 43 | * Global functions 44 | */ 45 | 46 | PyObject * CFStringRefToPyUnicode (CFStringRef source); 47 | CFStringRef PyUnicodeToCFStringRef(PyObject * source); 48 | PyObject * CGValueToPyTuple(AXValueRef value); 49 | 50 | #endif // ifndef _CONVERSION_H_ 51 | -------------------------------------------------------------------------------- /atomac/ldtp/client.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Nagappan Alagappan All Rights Reserved. 2 | 3 | # This file is part of ATOMac. 4 | 5 | #@author: Eitan Isaacson 6 | #@author: Nagappan Alagappan 7 | #@copyright: Copyright (c) 2009 Eitan Isaacson 8 | #@copyright: Copyright (c) 2009-13 Nagappan Alagappan 9 | 10 | #http://ldtp.freedesktop.org 11 | 12 | # ATOMac is free software; you can redistribute it and/or modify 13 | # it under the terms of the GNU General Public License as published by the Free 14 | # Software Foundation version 2 and no later version. 15 | 16 | # ATOMac is distributed in the hope that it will be useful, but 17 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 18 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License version 2 19 | # for more details. 20 | 21 | # You should have received a copy of the GNU General Public License along with 22 | # this program; if not, write to the Free Software Foundation, Inc., 51 Franklin 23 | # St, Fifth Floor, Boston, MA 02110-1301 USA. 24 | """client routines for LDTP""" 25 | 26 | import os 27 | import re 28 | import sys 29 | import time 30 | import signal 31 | import platform 32 | import traceback 33 | import subprocess 34 | from socket import error as SocketError 35 | from atomac.ldtp.log import logger 36 | from atomac.ldtp.client_exception import LdtpExecutionError, ERROR_CODE 37 | 38 | try: 39 | import xmlrpclib 40 | except ImportError: 41 | import xmlrpc.client as xmlrpclib 42 | _python3 = False 43 | _python26 = False 44 | if sys.version_info[:2] <= (2, 6): 45 | _python26 = True 46 | if sys.version_info[:2] >= (3, 0): 47 | _python3 = True 48 | _ldtp_windows_env = False 49 | if 'LDTP_DEBUG' in os.environ: 50 | _ldtp_debug = os.environ['LDTP_DEBUG'] 51 | else: 52 | _ldtp_debug = None 53 | if 'LDTP_XML_DEBUG' in os.environ: 54 | verbose = 1 55 | else: 56 | verbose = 0 57 | if 'LDTP_SERVER_ADDR' in os.environ: 58 | _ldtp_server_addr = os.environ['LDTP_SERVER_ADDR'] 59 | else: 60 | _ldtp_server_addr = 'localhost' 61 | if 'LDTP_SERVER_PORT' in os.environ: 62 | _ldtp_server_port = os.environ['LDTP_SERVER_PORT'] 63 | else: 64 | _ldtp_server_port = '4118' 65 | if 'LDTP_WINDOWS' in os.environ or (sys.platform.find('darwin') == -1 and 66 | sys.platform.find('win') != -1): 67 | if 'LDTP_LINUX' in os.environ: 68 | _ldtp_windows_env = False 69 | else: 70 | _ldtp_windows_env = True 71 | else: 72 | _ldtp_windows_env = False 73 | 74 | class _Method(xmlrpclib._Method): 75 | def __call__(self, *args, **kwargs): 76 | if _ldtp_debug: 77 | logger.debug('%s(%s)' % (self.__name, \ 78 | ', '.join(map(repr, args) + ['%s=%s' % (k, repr(v)) \ 79 | for k, v in kwargs.items()]))) 80 | return self.__send(self.__name, args) 81 | 82 | class Transport(xmlrpclib.Transport): 83 | def _handle_signal(self, signum, frame): 84 | if _ldtp_debug: 85 | if signum == signal.SIGCHLD: 86 | print("ldtpd exited!") 87 | elif signum == signal.SIGUSR1: 88 | print("SIGUSR1 received. ldtpd ready for requests.") 89 | elif signum == signal.SIGALRM: 90 | print("SIGALRM received. Timeout waiting for SIGUSR1.") 91 | 92 | def _spawn_daemon(self): 93 | pid = os.getpid() 94 | if _ldtp_windows_env: 95 | if _ldtp_debug: 96 | cmd = 'start cmd /K CobraWinLDTP.exe' 97 | else: 98 | cmd = 'CobraWinLDTP.exe' 99 | subprocess.Popen(cmd, shell = True) 100 | self._daemon = True 101 | elif platform.mac_ver()[0] != '': 102 | pycmd = 'import atomac.ldtpd; atomac.ldtpd.main(parentpid=%s)' % pid 103 | self._daemon = os.spawnlp(os.P_NOWAIT, 'python', 104 | 'python', '-c', pycmd) 105 | else: 106 | pycmd = 'import ldtpd; ldtpd.main(parentpid=%s)' % pid 107 | self._daemon = os.spawnlp(os.P_NOWAIT, 'python', 108 | 'python', '-c', pycmd) 109 | # http://www.itkovian.net/base/transport-class-for-pythons-xml-rpc-lib/ 110 | ## 111 | # Connect to server. 112 | # 113 | # @param host Target host. 114 | # @return A connection handle. 115 | 116 | if not _python26 and not _python3: 117 | # Add to the class, only if > python 2.5 118 | def make_connection(self, host): 119 | # create a HTTP connection object from a host descriptor 120 | import httplib 121 | host, extra_headers, x509 = self.get_host_info(host) 122 | return httplib.HTTPConnection(host) 123 | ## 124 | # Send a complete request, and parse the response. 125 | # 126 | # @param host Target host. 127 | # @param handler Target PRC handler. 128 | # @param request_body XML-RPC request body. 129 | # @param verbose Debugging flag. 130 | # @return XML response. 131 | 132 | def request(self, host, handler, request_body, verbose=0): 133 | # issue XML-RPC request 134 | retry_count = 1 135 | while True: 136 | try: 137 | if _python26: 138 | # Noticed this in Hutlab environment (Windows 7 SP1) 139 | # Activestate python 2.5, use the old method 140 | return xmlrpclib.Transport.request( 141 | self, host, handler, request_body, verbose=verbose) 142 | if not _python3: 143 | # Follwing implementation not supported in Python <= 2.6 144 | h = self.make_connection(host) 145 | if verbose: 146 | h.set_debuglevel(1) 147 | 148 | self.send_request(h, handler, request_body) 149 | self.send_host(h, host) 150 | self.send_user_agent(h) 151 | self.send_content(h, request_body) 152 | else: 153 | h=self.send_request(host, handler, request_body, bool(verbose)) 154 | 155 | response = h.getresponse() 156 | 157 | if response.status != 200: 158 | raise xmlrpclib.ProtocolError(host + handler, response.status, 159 | response.reason, response.msg.headers) 160 | 161 | payload = response.read() 162 | parser, unmarshaller = self.getparser() 163 | parser.feed(payload) 164 | parser.close() 165 | 166 | return unmarshaller.close() 167 | except SocketError as e: 168 | if ((_ldtp_windows_env and e[0] == 10061) or \ 169 | (hasattr(e, 'errno') and (e.errno == 111 or \ 170 | e.errno == 61 or \ 171 | e.errno == 146))) \ 172 | and 'localhost' in host: 173 | if hasattr(self, 'close'): 174 | # On Windows XP SP3 / Python 2.5, close doesn't exist 175 | self.close() 176 | if retry_count == 1: 177 | retry_count += 1 178 | if not _ldtp_windows_env: 179 | sigusr1 = signal.signal(signal.SIGUSR1, self._handle_signal) 180 | sigalrm = signal.signal(signal.SIGALRM, self._handle_signal) 181 | sigchld = signal.signal(signal.SIGCHLD, self._handle_signal) 182 | self._spawn_daemon() 183 | if _ldtp_windows_env: 184 | time.sleep(5) 185 | else: 186 | signal.alarm(15) # Wait 15 seconds for ldtpd 187 | signal.pause() 188 | # restore signal handlers 189 | signal.alarm(0) 190 | signal.signal(signal.SIGUSR1, sigusr1) 191 | signal.signal(signal.SIGALRM, sigalrm) 192 | signal.signal(signal.SIGCHLD, sigchld) 193 | continue 194 | else: 195 | raise 196 | # else raise exception 197 | raise 198 | except xmlrpclib.Fault as e: 199 | if hasattr(self, 'close'): 200 | self.close() 201 | if e.faultCode == ERROR_CODE: 202 | raise LdtpExecutionError(e.faultString.encode('utf-8')) 203 | else: 204 | raise e 205 | 206 | def __del__(self): 207 | try: 208 | self.kill_daemon() 209 | except: 210 | # To fix https://github.com/pyatom/pyatom/issues/61 211 | pass 212 | 213 | def kill_daemon(self): 214 | try: 215 | if _ldtp_windows_env and self._daemon: 216 | # If started by the current current, then terminate 217 | # else, silently quit 218 | subprocess.Popen('taskkill /F /IM CobraWinLDTP.exe', 219 | shell = True, stdout = subprocess.PIPE, 220 | stderr = subprocess.PIPE).communicate() 221 | else: 222 | os.kill(self._daemon, signal.SIGKILL) 223 | except AttributeError: 224 | pass 225 | 226 | class LdtpClient(xmlrpclib.ServerProxy): 227 | def __init__(self, uri, encoding=None, verbose=0, use_datetime=0): 228 | xmlrpclib.ServerProxy.__init__( 229 | self, uri, Transport(), encoding, verbose, 1, use_datetime) 230 | 231 | def __getattr__(self, name): 232 | # magic method dispatcher 233 | return _Method(self._ServerProxy__request, name) 234 | 235 | def kill_daemon(self): 236 | self._ServerProxy__transport.kill_daemon() 237 | 238 | def setHost(self, host): 239 | setattr(self, '_ServerProxy__host', host) 240 | 241 | _client = LdtpClient('http://%s:%s' % (_ldtp_server_addr, _ldtp_server_port), 242 | verbose = verbose) 243 | -------------------------------------------------------------------------------- /atomac/ldtp/client_exception.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Nagappan Alagappan All Rights Reserved. 2 | 3 | # This file is part of ATOMac. 4 | 5 | #@author: Eitan Isaacson 6 | #@author: Nagappan Alagappan 7 | #@copyright: Copyright (c) 2009 Eitan Isaacson 8 | #@copyright: Copyright (c) 2009-13 Nagappan Alagappan 9 | 10 | #http://ldtp.freedesktop.org 11 | 12 | # ATOMac is free software; you can redistribute it and/or modify 13 | # it under the terms of the GNU General Public License as published by the Free 14 | # Software Foundation version 2 and no later version. 15 | 16 | # ATOMac is distributed in the hope that it will be useful, but 17 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 18 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License version 2 19 | # for more details. 20 | 21 | # You should have received a copy of the GNU General Public License along with 22 | # this program; if not, write to the Free Software Foundation, Inc., 51 Franklin 23 | # St, Fifth Floor, Boston, MA 02110-1301 USA. 24 | """Python LDTP exception""" 25 | 26 | ERROR_CODE = 123 27 | 28 | class LdtpExecutionError(Exception): 29 | pass 30 | -------------------------------------------------------------------------------- /atomac/ldtp/log.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Nagappan Alagappan All Rights Reserved. 2 | 3 | # This file is part of ATOMac. 4 | 5 | #@author: Eitan Isaacson 6 | #@author: Nagappan Alagappan 7 | #@copyright: Copyright (c) 2009 Eitan Isaacson 8 | #@copyright: Copyright (c) 2009-13 Nagappan Alagappan 9 | 10 | #http://ldtp.freedesktop.org 11 | 12 | # ATOMac is free software; you can redistribute it and/or modify 13 | # it under the terms of the GNU General Public License as published by the Free 14 | # Software Foundation version 2 and no later version. 15 | 16 | # ATOMac is distributed in the hope that it will be useful, but 17 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 18 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License version 2 19 | # for more details. 20 | 21 | # You should have received a copy of the GNU General Public License along with 22 | # this program; if not, write to the Free Software Foundation, Inc., 51 Franklin 23 | # St, Fifth Floor, Boston, MA 02110-1301 USA. 24 | """Log routines for LDTP""" 25 | 26 | from os import environ as env 27 | import logging 28 | 29 | AREA = 'ldtp.client' 30 | ENV_LOG_LEVEL = 'LDTP_LOG_LEVEL' 31 | ENV_LOG_OUT = 'LDTP_LOG_OUT' 32 | 33 | log_level = getattr(logging, env.get(ENV_LOG_LEVEL, 'NOTSET'), logging.NOTSET) 34 | 35 | logger = logging.getLogger(AREA) 36 | 37 | if ENV_LOG_OUT not in env: 38 | handler = logging.StreamHandler() 39 | handler.setFormatter( 40 | logging.Formatter('%(name)-11s %(levelname)-8s %(message)s')) 41 | else: 42 | handler = logging.FileHandler(env[ENV_LOG_OUT]) 43 | handler.setFormatter( 44 | logging.Formatter('%(asctime)s %(levelname)-8s %(message)s')) 45 | 46 | logger.addHandler(handler) 47 | 48 | logger.setLevel(log_level) 49 | -------------------------------------------------------------------------------- /atomac/ldtp/state.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Nagappan Alagappan All Rights Reserved. 2 | 3 | # This file is part of ATOMac. 4 | 5 | #@author: Nagappan Alagappan 6 | #@copyright: Copyright (c) 2009-13 Nagappan Alagappan 7 | 8 | #http://ldtp.freedesktop.org 9 | 10 | # ATOMac is free software; you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by the Free 12 | # Software Foundation version 2 and no later version. 13 | 14 | # ATOMac is distributed in the hope that it will be useful, but 15 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 16 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License version 2 17 | # for more details. 18 | 19 | # You should have received a copy of the GNU General Public License along with 20 | # this program; if not, write to the Free Software Foundation, Inc., 51 Franklin 21 | # St, Fifth Floor, Boston, MA 02110-1301 USA. 22 | """Python routines for LDTP""" 23 | 24 | ICONIFIED = "iconified" 25 | INVALID = "invalid" 26 | PRESSED = "pressed" 27 | EXPANDABLE = "expandable" 28 | VISIBLE = "visible" 29 | LAST_DEFINED = "last_defined" 30 | BUSY = "busy" 31 | EXPANDED = "expanded" 32 | MANAGES_DESCENDANTS = "manages_descendants" 33 | IS_DEFAULT = "is_default" 34 | INDETERMINATE = "indeterminate" 35 | REQUIRED = "required" 36 | TRANSIENT = "transient" 37 | CHECKED = "checked" 38 | SENSITIVE = "sensitive" 39 | COLLAPSED = "collapsed" 40 | STALE = "stale" 41 | OPAQUE = "opaque" 42 | ENABLED = "enabled" 43 | HAS_TOOLTIP = "has_tooltip" 44 | SUPPORTS_AUTOCOMPLETION = "supports_autocompletion" 45 | FOCUSABLE = "focusable" 46 | SELECTABLE = "selectable" 47 | ACTIVE = "active" 48 | HORIZONTAL = "horizontal" 49 | VISITED = "visited" 50 | INVALID_ENTRY = "invalid_entry" 51 | FOCUSED = "focused" 52 | MODAL = "modal" 53 | VERTICAL = "vertical" 54 | SELECTED = "selected" 55 | SHOWING = "showing" 56 | ANIMATED = "animated" 57 | EDITABLE = "editable" 58 | MULTI_LINE = "multi_line" 59 | SINGLE_LINE = "single_line" 60 | SELECTABLE_TEXT = "selectable_text" 61 | ARMED = "armed" 62 | DEFUNCT = "defunct" 63 | MULTISELECTABLE = "multiselectable" 64 | RESIZABLE = "resizable" 65 | TRUNCATED = "truncated" 66 | -------------------------------------------------------------------------------- /atomac/ldtpd/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012 VMware, Inc. All Rights Reserved. 2 | 3 | # This file is part of ATOMac. 4 | 5 | # ATOMac is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by the Free 7 | # Software Foundation version 2 and no later version. 8 | 9 | # ATOMac is distributed in the hope that it will be useful, but 10 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License version 2 12 | # for more details. 13 | 14 | # You should have received a copy of the GNU General Public License along with 15 | # this program; if not, write to the Free Software Foundation, Inc., 51 Franklin 16 | # St, Fifth Floor, Boston, MA 02110-1301 USA. 17 | """Main routines for LDTP daemon. 18 | 19 | The LDTP daemon listens on a socket for incoming connections. These 20 | connections send XMLRPC commands to do perform various GUI automation 21 | tasks. It's the server part of a client/server UI automation architecture. 22 | """ 23 | 24 | import os 25 | import sys 26 | import core 27 | import time 28 | import signal 29 | import socket 30 | import thread 31 | import traceback 32 | import SimpleXMLRPCServer 33 | from SimpleXMLRPCServer import SimpleXMLRPCRequestHandler 34 | 35 | # Restrict to a particular path. 36 | class RequestHandler(SimpleXMLRPCRequestHandler): 37 | rpc_paths = ('/RPC2',) 38 | encode_threshold = None 39 | 40 | class LDTPServer(SimpleXMLRPCServer.SimpleXMLRPCServer): 41 | '''Class to override some behavior in SimpleXMLRPCServer''' 42 | def server_bind(self, *args, **kwargs): 43 | '''Server Bind. Forces reuse of port.''' 44 | self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 45 | # Can't use super() here since SimpleXMLRPCServer is an old-style class 46 | SimpleXMLRPCServer.SimpleXMLRPCServer.server_bind(self, *args, **kwargs) 47 | 48 | def notifyclient(parentpid): 49 | time.sleep(1) 50 | os.kill(int(parentpid), signal.SIGUSR1) 51 | 52 | def main(port = 4118, parentpid=None): 53 | """Main entry point. Parse command line options and start up a server.""" 54 | if os.environ.has_key("LDTP_DEBUG"): 55 | _ldtp_debug=True 56 | else: 57 | _ldtp_debug=False 58 | _ldtp_debug_file = os.environ.get('LDTP_DEBUG_FILE', None) 59 | server = LDTPServer(('', port), allow_none=True, logRequests=_ldtp_debug, 60 | requestHandler=RequestHandler) 61 | server.register_introspection_functions() 62 | server.register_multicall_functions() 63 | ldtp_inst = core.Core() 64 | server.register_instance(ldtp_inst) 65 | if parentpid: 66 | thread.start_new_thread(notifyclient, (parentpid,)) 67 | try: 68 | server.serve_forever() 69 | except KeyboardInterrupt: 70 | pass 71 | except: 72 | if _ldtp_debug: 73 | print traceback.format_exc() 74 | if _ldtp_debug_file: 75 | with open(_ldtp_debug_file, "a") as fp: 76 | fp.write(traceback.format_exc()) 77 | -------------------------------------------------------------------------------- /atomac/ldtpd/combo_box.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012 VMware, Inc. All Rights Reserved. 2 | 3 | # This file is part of ATOMac. 4 | 5 | #@author: Nagappan Alagappan 6 | #@copyright: Copyright (c) 2009-12 Nagappan Alagappan 7 | #http://ldtp.freedesktop.org 8 | 9 | # ATOMac is free software; you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by the Free 11 | # Software Foundation version 2 and no later version. 12 | 13 | # ATOMac is distributed in the hope that it will be useful, but 14 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 15 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License version 2 16 | # for more details. 17 | 18 | # You should have received a copy of the GNU General Public License along with 19 | # this program; if not, write to the Free Software Foundation, Inc., 51 Franklin 20 | # St, Fifth Floor, Boston, MA 02110-1301 USA. 21 | """Combobox class.""" 22 | 23 | import re 24 | from atomac import AXKeyCodeConstants 25 | 26 | from utils import Utils 27 | from server_exception import LdtpServerException 28 | 29 | class ComboBox(Utils): 30 | def selectitem(self, window_name, object_name, item_name): 31 | """ 32 | Select combo box / layered pane item 33 | 34 | @param window_name: Window name to type in, either full name, 35 | LDTP's name convention, or a Unix glob. 36 | @type window_name: string 37 | @param object_name: Object name to type in, either full name, 38 | LDTP's name convention, or a Unix glob. 39 | @type object_name: string 40 | @param item_name: Item name to select 41 | @type object_name: string 42 | 43 | @return: 1 on success. 44 | @rtype: integer 45 | """ 46 | object_handle=self._get_object_handle(window_name, object_name) 47 | if not object_handle.AXEnabled: 48 | raise LdtpServerException(u"Object %s state disabled" % object_name) 49 | self._grabfocus(object_handle.AXWindow) 50 | try: 51 | object_handle.Press() 52 | except AttributeError: 53 | # AXPress doesn't work with Instruments 54 | # So did the following work around 55 | x, y, width, height=self._getobjectsize(object_handle) 56 | # Mouse left click on the object 57 | # Note: x + width/2, y + height / 2 doesn't work 58 | self.generatemouseevent(x + 5, y + 5, "b1c") 59 | self.wait(5) 60 | handle=self._get_sub_menu_handle(object_handle, item_name) 61 | x, y, width, height=self._getobjectsize(handle) 62 | # on OSX 10.7 default "b1c" doesn't work 63 | # so using "b1d", verified with Fusion test, this works 64 | self.generatemouseevent(x + 5, y + 5, "b1d") 65 | return 1 66 | # Required for menuitem to appear in accessibility list 67 | self.wait(1) 68 | menu_list=re.split(";", item_name) 69 | try: 70 | menu_handle=self._internal_menu_handler(object_handle, menu_list, 71 | True) 72 | # Required for menuitem to appear in accessibility list 73 | self.wait(1) 74 | if not menu_handle.AXEnabled: 75 | raise LdtpServerException(u"Object %s state disabled" % \ 76 | menu_list[-1]) 77 | menu_handle.Press() 78 | except LdtpServerException: 79 | object_handle.activate() 80 | object_handle.sendKey(AXKeyCodeConstants.ESCAPE) 81 | raise 82 | return 1 83 | 84 | # Since selectitem and comboselect implementation are same, 85 | # for Linux/Windows API compatibility let us assign selectitem to comboselect 86 | comboselect=selectitem 87 | 88 | def selectindex(self, window_name, object_name, item_index): 89 | """ 90 | Select combo box item / layered pane based on index 91 | 92 | @param window_name: Window name to type in, either full name, 93 | LDTP's name convention, or a Unix glob. 94 | @type window_name: string 95 | @param object_name: Object name to type in, either full name, 96 | LDTP's name convention, or a Unix glob. 97 | @type object_name: string 98 | @param item_index: Item index to select 99 | @type object_name: integer 100 | 101 | @return: 1 on success. 102 | @rtype: integer 103 | """ 104 | object_handle=self._get_object_handle(window_name, object_name) 105 | if not object_handle.AXEnabled: 106 | raise LdtpServerException(u"Object %s state disabled" % object_name) 107 | self._grabfocus(object_handle.AXWindow) 108 | try: 109 | object_handle.Press() 110 | except AttributeError: 111 | # AXPress doesn't work with Instruments 112 | # So did the following work around 113 | x, y, width, height=self._getobjectsize(object_handle) 114 | # Mouse left click on the object 115 | # Note: x + width/2, y + height / 2 doesn't work 116 | self.generatemouseevent(x + 5, y + 5, "b1c") 117 | # Required for menuitem to appear in accessibility list 118 | self.wait(2) 119 | if not object_handle.AXChildren: 120 | raise LdtpServerException(u"Unable to find menu") 121 | # Get AXMenu 122 | children=object_handle.AXChildren[0] 123 | if not children: 124 | raise LdtpServerException(u"Unable to find menu") 125 | children=children.AXChildren 126 | tmp_children=[] 127 | for child in children: 128 | role, label=self._ldtpize_accessible(child) 129 | # Don't add empty label 130 | # Menu separator have empty label's 131 | if label: 132 | tmp_children.append(child) 133 | children=tmp_children 134 | length=len(children) 135 | try: 136 | if item_index < 0 or item_index > length: 137 | raise LdtpServerException(u"Invalid item index %d" % item_index) 138 | menu_handle=children[item_index] 139 | if not menu_handle.AXEnabled: 140 | raise LdtpServerException(u"Object %s state disabled" % menu_list[-1]) 141 | self._grabfocus(menu_handle) 142 | x, y, width, height=self._getobjectsize(menu_handle) 143 | # on OSX 10.7 default "b1c" doesn't work 144 | # so using "b1d", verified with Fusion test, this works 145 | window=object_handle.AXWindow 146 | # For some reason, 147 | # self.generatemouseevent(x + 5, y + 5, "b1d") 148 | # doesn't work with Fusion settings 149 | # Advanced window, so work around with this 150 | # ldtp.selectindex('*Advanced', 'Automatic', 1) 151 | """ 152 | Traceback (most recent call last): 153 | File "build/bdist.macosx-10.8-intel/egg/atomac/ldtpd/utils.py", line 178, in _dispatch 154 | return getattr(self, method)(*args) 155 | File "build/bdist.macosx-10.8-intel/egg/atomac/ldtpd/combo_box.py", line 146, in selectindex 156 | self.generatemouseevent(x + 5, y + 5, "b1d") 157 | File "build/bdist.macosx-10.8-intel/egg/atomac/ldtpd/mouse.py", line 97, in generatemouseevent 158 | window=self._get_front_most_window() 159 | File "build/bdist.macosx-10.8-intel/egg/atomac/ldtpd/utils.py", line 185, in _get_front_most_window 160 | front_app=atomac.NativeUIElement.getFrontmostApp() 161 | File "build/bdist.macosx-10.8-intel/egg/atomac/AXClasses.py", line 114, in getFrontmostApp 162 | raise ValueError('No GUI application found.') 163 | ValueError: No GUI application found. 164 | """ 165 | window.doubleClickMouse((x + 5, y + 5)) 166 | # If menuitem already pressed, set child to None 167 | # So, it doesn't click back in combobox in finally block 168 | child=None 169 | finally: 170 | if child: 171 | child.Cancel() 172 | return 1 173 | 174 | # Since selectindex and comboselectindex implementation are same, 175 | # for backward compatibility let us assign selectindex to comboselectindex 176 | comboselectindex=selectindex 177 | 178 | def getallitem(self, window_name, object_name): 179 | """ 180 | Get all combo box item 181 | 182 | @param window_name: Window name to type in, either full name, 183 | LDTP's name convention, or a Unix glob. 184 | @type window_name: string 185 | @param object_name: Object name to type in, either full name, 186 | LDTP's name convention, or a Unix glob. 187 | @type object_name: string 188 | 189 | @return: list of string on success. 190 | @rtype: list 191 | """ 192 | object_handle=self._get_object_handle(window_name, object_name) 193 | if not object_handle.AXEnabled: 194 | raise LdtpServerException(u"Object %s state disabled" % object_name) 195 | object_handle.Press() 196 | # Required for menuitem to appear in accessibility list 197 | self.wait(1) 198 | child=None 199 | try: 200 | if not object_handle.AXChildren: 201 | raise LdtpServerException(u"Unable to find menu") 202 | # Get AXMenu 203 | children=object_handle.AXChildren[0] 204 | if not children: 205 | raise LdtpServerException(u"Unable to find menu") 206 | children=children.AXChildren 207 | items=[] 208 | for child in children: 209 | label = self._get_title(child) 210 | # Don't add empty label 211 | # Menu separator have empty label's 212 | if label: 213 | items.append(label) 214 | finally: 215 | if child: 216 | # Set it back, by clicking combo box 217 | child.Cancel() 218 | return items 219 | 220 | def showlist(self, window_name, object_name): 221 | """ 222 | Show combo box list / menu 223 | 224 | @param window_name: Window name to type in, either full name, 225 | LDTP's name convention, or a Unix glob. 226 | @type window_name: string 227 | @param object_name: Object name to type in, either full name, 228 | LDTP's name convention, or a Unix glob. 229 | @type object_name: string 230 | 231 | @return: 1 on success. 232 | @rtype: integer 233 | """ 234 | object_handle=self._get_object_handle(window_name, object_name) 235 | if not object_handle.AXEnabled: 236 | raise LdtpServerException(u"Object %s state disabled" % object_name) 237 | object_handle.Press() 238 | return 1 239 | 240 | def hidelist(self, window_name, object_name): 241 | """ 242 | Hide combo box list / menu 243 | 244 | @param window_name: Window name to type in, either full name, 245 | LDTP's name convention, or a Unix glob. 246 | @type window_name: string 247 | @param object_name: Object name to type in, either full name, 248 | LDTP's name convention, or a Unix glob. 249 | @type object_name: string 250 | 251 | @return: 1 on success. 252 | @rtype: integer 253 | """ 254 | object_handle=self._get_object_handle(window_name, object_name) 255 | object_handle.activate() 256 | object_handle.sendKey(AXKeyCodeConstants.ESCAPE) 257 | return 1 258 | 259 | def verifydropdown(self, window_name, object_name): 260 | """ 261 | Verify drop down list / menu poped up 262 | 263 | @param window_name: Window name to type in, either full name, 264 | LDTP's name convention, or a Unix glob. 265 | @type window_name: string 266 | @param object_name: Object name to type in, either full name, 267 | LDTP's name convention, or a Unix glob. 268 | @type object_name: string 269 | 270 | @return: 1 on success 0 on failure. 271 | @rtype: integer 272 | """ 273 | try: 274 | object_handle=self._get_object_handle(window_name, object_name) 275 | if not object_handle.AXEnabled or not object_handle.AXChildren: 276 | return 0 277 | # Get AXMenu 278 | children=object_handle.AXChildren[0] 279 | if children: 280 | return 1 281 | except LdtpServerException: 282 | pass 283 | return 0 284 | 285 | def verifyshowlist(self, window_name, object_name): 286 | """ 287 | Verify drop down list / menu poped up 288 | 289 | @param window_name: Window name to type in, either full name, 290 | LDTP's name convention, or a Unix glob. 291 | @type window_name: string 292 | @param object_name: Object name to type in, either full name, 293 | LDTP's name convention, or a Unix glob. 294 | @type object_name: string 295 | 296 | @return: 1 on success 0 on failure. 297 | @rtype: integer 298 | """ 299 | return self.verifydropdown(window_name, object_name) 300 | 301 | def verifyhidelist(self, window_name, object_name): 302 | """ 303 | Verify list / menu is hidden in combo box 304 | 305 | @param window_name: Window name to type in, either full name, 306 | LDTP's name convention, or a Unix glob. 307 | @type window_name: string 308 | @param object_name: Object name to type in, either full name, 309 | LDTP's name convention, or a Unix glob. 310 | @type object_name: string 311 | 312 | @return: 1 on success 0 on failure. 313 | @rtype: integer 314 | """ 315 | try: 316 | object_handle=self._get_object_handle(window_name, object_name) 317 | if not object_handle.AXEnabled: 318 | return 0 319 | if not object_handle.AXChildren: 320 | return 1 321 | # Get AXMenu 322 | children=object_handle.AXChildren[0] 323 | if not children: 324 | return 1 325 | return 1 326 | except LdtpServerException: 327 | pass 328 | return 0 329 | 330 | def verifyselect(self, window_name, object_name, item_name): 331 | """ 332 | Verify the item selected in combo box 333 | 334 | @param window_name: Window name to type in, either full name, 335 | LDTP's name convention, or a Unix glob. 336 | @type window_name: string 337 | @param object_name: Object name to type in, either full name, 338 | LDTP's name convention, or a Unix glob. 339 | @type object_name: string 340 | @param item_name: Item name to select 341 | @type object_name: string 342 | 343 | @return: 1 on success. 344 | @rtype: integer 345 | """ 346 | try: 347 | object_handle=self._get_object_handle(window_name, object_name) 348 | if not object_handle.AXEnabled: 349 | return 0 350 | role, label=self._ldtpize_accessible(object_handle) 351 | title=self._get_title(object_handle) 352 | if re.match(item_name, title, re.M | re.U | re.L) or \ 353 | re.match(item_name, label, re.M | re.U | re.L) or \ 354 | re.match(item_name, u"%u%u" % (role, label), 355 | re.M | re.U | re.L): 356 | return 1 357 | except LdtpServerException: 358 | pass 359 | return 0 360 | 361 | def getcombovalue(self, window_name, object_name): 362 | """ 363 | Get current selected combobox value 364 | 365 | @param window_name: Window name to type in, either full name, 366 | LDTP's name convention, or a Unix glob. 367 | @type window_name: string 368 | @param object_name: Object name to type in, either full name, 369 | LDTP's name convention, or a Unix glob. 370 | @type object_name: string 371 | 372 | @return: selected item on success, else LdtpExecutionError on failure. 373 | @rtype: string 374 | """ 375 | object_handle=self._get_object_handle(window_name, object_name) 376 | if not object_handle.AXEnabled: 377 | raise LdtpServerException(u"Object %s state disabled" % object_name) 378 | return self._get_title(object_handle) 379 | -------------------------------------------------------------------------------- /atomac/ldtpd/constants.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012 VMware, Inc. All Rights Reserved. 2 | 3 | # This file is part of ATOMac. 4 | 5 | #@author: Nagappan Alagappan 6 | #@copyright: Copyright (c) 2009-12 Nagappan Alagappan 7 | #http://ldtp.freedesktop.org 8 | 9 | # ATOMac is free software; you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by the Free 11 | # Software Foundation version 2 and no later version. 12 | 13 | # ATOMac is distributed in the hope that it will be useful, but 14 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 15 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License version 2 16 | # for more details. 17 | 18 | # You should have received a copy of the GNU General Public License along with 19 | # this program; if not, write to the Free Software Foundation, Inc., 51 Franklin 20 | # St, Fifth Floor, Boston, MA 02110-1301 USA. 21 | """Constants class.""" 22 | 23 | abbreviated_roles = { 24 | "AXWindow" : "frm", 25 | "AXTextArea" : "txt", 26 | "AXTextField" : "txt", 27 | "AXButton" : "btn", 28 | "AXStaticText" : "lbl", 29 | "AXRadioButton" : "rbtn", 30 | "AXSlider" : "sldr", 31 | "AXCell" : "tblc", 32 | "AXImage" : "img", 33 | "AXToolbar" : "tbar", 34 | "AXScrollBar" : "scbr", 35 | "AXMenuItem" : "mnu", 36 | "AXMenu" : "mnu", 37 | "AXMenuBar" : "mnu", 38 | "AXMenuBarItem" : "mnu", 39 | "AXCheckBox" : "chk", 40 | "AXTabGroup" : "ptl", 41 | "AXList" : "lst", 42 | # Not sure what"s the following object equivalent in LDTP 43 | "AXMenuButton" : "cbo", # Maybe combo-box ? 44 | "AXRow" : "tblc", 45 | "AXColumn" : "col", 46 | "AXTable" : "tbl", 47 | "AXScrollArea" : "sar", 48 | "AXOutline" : "otl", 49 | "AXValueIndicator" : "val", 50 | "AXDisclosureTriangle" : "dct", 51 | "AXGroup" : "grp", 52 | "AXPopUpButton" : "pubtn", 53 | "AXApplication" : "app", 54 | "AXDocItem" : "doc", 55 | "AXHeading" : "tch", 56 | "AXGenericElement" : "gen", 57 | } 58 | ldtp_class_type = { 59 | "AXWindow" : "frame", 60 | "AXApplication" : "application", 61 | "AXTextArea" : "text", 62 | "AXTextField" : "text", 63 | "AXButton" : "push_button", 64 | "AXStaticText" : "label", 65 | "AXRadioButton" : "radion_button", 66 | "AXSlider" : "slider", 67 | "AXCell" : "table_cell", 68 | "AXImage" : "image", 69 | "AXToolbar" : "toolbar", 70 | "AXScrollBar" : "scroll_bar", 71 | "AXMenuItem" : "menu_item", 72 | "AXMenu" : "menu", 73 | "AXMenuBar" : "menu_bar", 74 | "AXMenuBarItem" : "menu_bar_item", 75 | "AXCheckBox" : "check_box", 76 | "AXTabGroup" : "page_tab_list", 77 | "AXList" : "list", 78 | "AXColumn" : "column", 79 | "AXRow" : "table_cell", 80 | "AXTable" : "table", 81 | "AXScrollArea" : "scroll_area", 82 | "AXPopUpButton" : "popup_button", 83 | "AXDocItem" : "doc_item", 84 | "AXHeading" : "heading", 85 | "AXGenericElement" : "generic_element", 86 | } 87 | -------------------------------------------------------------------------------- /atomac/ldtpd/generic.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012 VMware, Inc. All Rights Reserved. 2 | 3 | # This file is part of ATOMac. 4 | 5 | #@author: Yingjun Li 6 | #@copyright: Copyright (c) 2009-12 Yingjun Li 7 | 8 | #http://ldtp.freedesktop.org 9 | 10 | # ATOMac is free software; you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by the Free 12 | # Software Foundation version 2 and no later version. 13 | 14 | # ATOMac is distributed in the hope that it will be useful, but 15 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 16 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License version 2 17 | # for more details. 18 | 19 | # You should have received a copy of the GNU General Public License along with 20 | # this program; if not, write to the Free Software Foundation, Inc., 51 Franklin 21 | # St, Fifth Floor, Boston, MA 02110-1301 USA. 22 | """Generic class.""" 23 | 24 | import os 25 | import tempfile 26 | from base64 import b64encode 27 | from AppKit import * 28 | from Quartz.CoreGraphics import * 29 | 30 | from utils import Utils 31 | from server_exception import LdtpServerException 32 | 33 | class Generic(Utils): 34 | def imagecapture(self, window_name = None, x = 0, y = 0, 35 | width = None, height = None): 36 | """ 37 | Captures screenshot of the whole desktop or given window 38 | 39 | @param window_name: Window name to look for, either full name, 40 | LDTP's name convention, or a Unix glob. 41 | @type window_name: string 42 | @param x: x co-ordinate value 43 | @type x: int 44 | @param y: y co-ordinate value 45 | @type y: int 46 | @param width: width co-ordinate value 47 | @type width: int 48 | @param height: height co-ordinate value 49 | @type height: int 50 | 51 | @return: screenshot with base64 encoded for the client 52 | @rtype: string 53 | """ 54 | if x or y or (width and width != -1) or (height and height != -1): 55 | raise LdtpServerException("Not implemented") 56 | if window_name: 57 | handle, name, app=self._get_window_handle(window_name) 58 | try: 59 | self._grabfocus(handle) 60 | except: 61 | pass 62 | rect = self._getobjectsize(handle) 63 | screenshot = CGWindowListCreateImage(NSMakeRect(rect[0], 64 | rect[1], rect[2], rect[3]), 1, 0, 0) 65 | else: 66 | screenshot = CGWindowListCreateImage(CGRectInfinite, 1, 0, 0) 67 | image = CIImage.imageWithCGImage_(screenshot) 68 | bitmapRep = NSBitmapImageRep.alloc().initWithCIImage_(image) 69 | blob = bitmapRep.representationUsingType_properties_(NSPNGFileType, None) 70 | tmpFile = tempfile.mktemp('.png', 'ldtpd_') 71 | blob.writeToFile_atomically_(tmpFile, False) 72 | rv = b64encode(open(tmpFile).read()) 73 | os.remove(tmpFile) 74 | return rv 75 | 76 | -------------------------------------------------------------------------------- /atomac/ldtpd/keypress_actions.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012 VMware, Inc. All Rights Reserved. 2 | 3 | # This file is part of ATOMac. 4 | 5 | #@author: Nagappan Alagappan 6 | #@copyright: Copyright (c) 2009-12 Nagappan Alagappan 7 | #http://ldtp.freedesktop.org 8 | 9 | # ATOMac is free software; you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by the Free 11 | # Software Foundation version 2 and no later version. 12 | 13 | # ATOMac is distributed in the hope that it will be useful, but 14 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 15 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License version 2 16 | # for more details. 17 | 18 | # You should have received a copy of the GNU General Public License along with 19 | # this program; if not, write to the Free Software Foundation, Inc., 51 Franklin 20 | # St, Fifth Floor, Boston, MA 02110-1301 USA. 21 | """KeyboardOp class.""" 22 | 23 | import time 24 | from atomac.AXClasses import NativeUIElement 25 | from atomac.AXKeyCodeConstants import * 26 | from server_exception import LdtpServerException 27 | 28 | class KeyCombo: 29 | def __init__(self): 30 | self.modifiers=False 31 | self.value='' 32 | self.modVal=None 33 | 34 | class KeyboardOp: 35 | def __init__(self): 36 | self._undefined_key=None 37 | self._max_tokens=256 38 | self._max_tok_size=15 39 | 40 | def _get_key_value(self, keyval): 41 | return_val=KeyCombo() 42 | if keyval == "command": 43 | keyval="command_l" 44 | elif keyval == "option" or keyval == "alt" or keyval == "alt_l": 45 | keyval="option_l" 46 | elif keyval == "alt_r": 47 | keyval="option_r" 48 | elif keyval == "control" or keyval == "ctrl" or keyval == "ctrl_l": 49 | keyval="control_l" 50 | elif keyval == "ctrl_r": 51 | keyval="control_r" 52 | elif keyval == "shift": 53 | keyval="shift_l" 54 | elif keyval == "left": 55 | keyval="cursor_left" 56 | elif keyval == "right": 57 | keyval="cursor_right" 58 | elif keyval == "up": 59 | keyval="cursor_up" 60 | elif keyval == "down": 61 | keyval="cursor_down" 62 | elif keyval == "bksp": 63 | keyval="backspace" 64 | elif keyval == "enter": 65 | keyval="return" 66 | elif keyval == "pgdown": 67 | keyval="page_down" 68 | elif keyval == "pagedown": 69 | keyval="page_down" 70 | elif keyval == "pgup": 71 | keyval="page_up" 72 | elif keyval == "pageup": 73 | keyval="page_up" 74 | elif keyval == "esc": 75 | keyval="escape" 76 | key="<%s>" % keyval 77 | # This will identify Modifiers 78 | if key in ["", "", 79 | "", "", 80 | "", "", 81 | "", ""]: 82 | return_val.modifiers=True 83 | return_val.modVal=[key] 84 | return return_val 85 | # This will identify all US_keyboard characters 86 | if keyval.lower() in US_keyboard: 87 | return_val.value=keyval 88 | return return_val 89 | # This will identify all specialKeys 90 | if key in specialKeys: 91 | return_val.value=key 92 | return return_val 93 | # Key Undefined 94 | return return_val 95 | 96 | def get_keyval_id(self, input_str): 97 | index=0 98 | key_vals=[] 99 | lastModifiers=None 100 | while index < len(input_str): 101 | token='' 102 | # Identified a Non Printing Key 103 | if input_str[index] == '<': 104 | index += 1 105 | i=0 106 | while input_str[index] != '>' and i < self._max_tok_size: 107 | token += input_str[index] 108 | index += 1 109 | i += 1 110 | if input_str[index] != '>': 111 | # Premature end of string without an opening '<' 112 | return None 113 | index += 1 114 | else: 115 | token=input_str[index] 116 | index += 1 117 | 118 | key_val=self._get_key_value(token) 119 | # Deal with modifier and undefined keys. 120 | # Modifiers: if we got modifier in previous 121 | # step, extend the previous KeyCombo object instead 122 | # of creating a new one. 123 | if lastModifiers and key_val.value != self._undefined_key: 124 | last_item = key_vals.pop() 125 | if key_val.modifiers: 126 | lastModifiers = key_val 127 | last_item.modVal.extend(key_val.modVal) 128 | key_val = last_item 129 | else: 130 | last_item.value = key_val.value 131 | key_val = last_item 132 | lastModifiers=None 133 | elif key_val.modifiers: 134 | if not lastModifiers: 135 | lastModifiers=key_val 136 | else: 137 | last_item=key_vals.pop() 138 | last_item.modVal.extend(key_val.modVal) 139 | key_val=last_item 140 | elif key_val.value == self._undefined_key: 141 | # Invalid key 142 | return None 143 | key_vals.append(key_val) 144 | return key_vals 145 | 146 | class KeyComboAction: 147 | """Used for sending keyboard events to the system.""" 148 | 149 | def __init__(self, data): 150 | """ 151 | @param data: data to type 152 | @type data: string 153 | """ 154 | self._data=data 155 | # Create dummy window, it has code for creating and queuing events. 156 | # We will send events 'globally' to the system so dummy window will 157 | # not receive the event. 158 | self._dummy_window=NativeUIElement() 159 | _keyOp=KeyboardOp() 160 | self._keyvalId=_keyOp.get_keyval_id(data) 161 | if not self._keyvalId: 162 | raise LdtpServerException("Unsupported keys passed") 163 | self._doCombo() 164 | 165 | def _doCombo(self): 166 | for key_val in self._keyvalId: 167 | if key_val.modifiers: 168 | self._dummy_window.sendGlobalKeyWithModifiers(key_val.value, 169 | key_val.modVal) 170 | else: 171 | self._dummy_window.sendGlobalKey(key_val.value) 172 | time.sleep(0.01) 173 | 174 | class KeyPressAction: 175 | def __init__(self, window, data): 176 | self._data=data 177 | self._window=window 178 | _keyOp=KeyboardOp() 179 | self._keyvalId=_keyOp.get_keyval_id(data) 180 | if not self._keyvalId: 181 | raise LdtpServerException("Unsupported keys passed") 182 | self._doPress() 183 | 184 | def _doPress(self): 185 | for key_val in self._keyvalId: 186 | if key_val.modifiers: 187 | self._window.pressModifiers(key_val.modVal) 188 | else: 189 | raise LdtpServerException("Unsupported modifiers") 190 | time.sleep(0.01) 191 | 192 | class KeyReleaseAction: 193 | def __init__(self, window, data): 194 | self._data=data 195 | self._window=window 196 | _keyOp=KeyboardOp() 197 | self._keyvalId=_keyOp.get_keyval_id(data) 198 | if not self._keyvalId: 199 | raise LdtpServerException("Unsupported keys passed") 200 | self._doRelease() 201 | 202 | def _doRelease(self): 203 | for key_val in self._keyvalId: 204 | if key_val.modifiers: 205 | self._window.releaseModifiers(key_val.modVal) 206 | else: 207 | raise LdtpServerException("Unsupported modifiers") 208 | time.sleep(0.01) 209 | -------------------------------------------------------------------------------- /atomac/ldtpd/menu.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012 VMware, Inc. All Rights Reserved. 2 | 3 | # This file is part of ATOMac. 4 | 5 | #@author: Nagappan Alagappan 6 | #@copyright: Copyright (c) 2009-12 Nagappan Alagappan 7 | #http://ldtp.freedesktop.org 8 | 9 | # ATOMac is free software; you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by the Free 11 | # Software Foundation version 2 and no later version. 12 | 13 | # ATOMac is distributed in the hope that it will be useful, but 14 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 15 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License version 2 16 | # for more details. 17 | 18 | # You should have received a copy of the GNU General Public License along with 19 | # this program; if not, write to the Free Software Foundation, Inc., 51 Franklin 20 | # St, Fifth Floor, Boston, MA 02110-1301 USA. 21 | """Menu class.""" 22 | 23 | import re 24 | import atomac 25 | 26 | from utils import Utils 27 | from server_exception import LdtpServerException 28 | 29 | class Menu(Utils): 30 | def _get_menu_handle(self, window_name, object_name, 31 | wait_for_window=True): 32 | menu_list=re.split(";", object_name) 33 | # Call base class get_menu_handle 34 | try: 35 | menu_handle=Utils._get_menu_handle(self, window_name, 36 | menu_list[0], wait_for_window) 37 | except (atomac._a11y.ErrorCannotComplete, atomac._a11y.ErrorInvalidUIElement): 38 | # During the test, when the window closed and reopened 39 | # ErrorCannotComplete exception will be thrown 40 | self._windows={} 41 | # Call the method again, after updating apps 42 | menu_handle=Utils._get_menu_handle(self, window_name, 43 | menu_list[0], wait_for_window) 44 | if len(menu_list) <= 1: 45 | # If only first level menu is given, return the handle 46 | return menu_handle 47 | return self._internal_menu_handler(menu_handle, menu_list[1:]) 48 | 49 | def selectmenuitem(self, window_name, object_name): 50 | """ 51 | Select (click) a menu item. 52 | 53 | @param window_name: Window name to look for, either full name, 54 | LDTP's name convention, or a Unix glob. 55 | @type window_name: string 56 | @param object_name: Object name to look for, either full name, 57 | LDTP's name convention, or a Unix glob. Or menu heirarchy 58 | @type object_name: string 59 | 60 | @return: 1 on success. 61 | @rtype: integer 62 | """ 63 | menu_handle=self._get_menu_handle(window_name, object_name) 64 | if not menu_handle.AXEnabled: 65 | raise LdtpServerException(u"Object %s state disabled" % object_name) 66 | menu_handle.Press() 67 | return 1 68 | 69 | def doesmenuitemexist(self, window_name, object_name): 70 | """ 71 | Check a menu item exist. 72 | 73 | @param window_name: Window name to look for, either full name, 74 | LDTP's name convention, or a Unix glob. 75 | @type window_name: string 76 | @param object_name: Object name to look for, either full name, 77 | LDTP's name convention, or a Unix glob. Or menu heirarchy 78 | @type object_name: string 79 | @param strict_hierarchy: Mandate menu hierarchy if set to True 80 | @type object_name: boolean 81 | 82 | @return: 1 on success. 83 | @rtype: integer 84 | """ 85 | try: 86 | menu_handle=self._get_menu_handle(window_name, object_name, 87 | False) 88 | return 1 89 | except LdtpServerException: 90 | return 0 91 | 92 | def menuitemenabled(self, window_name, object_name): 93 | """ 94 | Verify a menu item is enabled 95 | 96 | @param window_name: Window name to look for, either full name, 97 | LDTP's name convention, or a Unix glob. 98 | @type window_name: string 99 | @param object_name: Object name to look for, either full name, 100 | LDTP's name convention, or a Unix glob. Or menu heirarchy 101 | @type object_name: string 102 | 103 | @return: 1 on success. 104 | @rtype: integer 105 | """ 106 | try: 107 | menu_handle=self._get_menu_handle(window_name, object_name, 108 | False) 109 | if menu_handle.AXEnabled: 110 | return 1 111 | except LdtpServerException: 112 | pass 113 | return 0 114 | 115 | def listsubmenus(self, window_name, object_name): 116 | """ 117 | List children of menu item 118 | 119 | @param window_name: Window name to look for, either full name, 120 | LDTP's name convention, or a Unix glob. 121 | @type window_name: string 122 | @param object_name: Object name to look for, either full name, 123 | LDTP's name convention, or a Unix glob. Or menu heirarchy 124 | @type object_name: string 125 | 126 | @return: menu item in list on success. 127 | @rtype: list 128 | """ 129 | menu_handle=self._get_menu_handle(window_name, object_name) 130 | role, label=self._ldtpize_accessible(menu_handle) 131 | menu_clicked=False 132 | try: 133 | if not menu_handle.AXChildren: 134 | menu_clicked=True 135 | try: 136 | menu_handle.Press() 137 | self.wait(1) 138 | except atomac._a11y.ErrorCannotComplete: 139 | pass 140 | if not menu_handle.AXChildren: 141 | raise LdtpServerException(u"Unable to find children under menu %s" % \ 142 | label) 143 | children=menu_handle.AXChildren[0] 144 | sub_menus=[] 145 | for current_menu in children.AXChildren: 146 | role, label=self._ldtpize_accessible(current_menu) 147 | if not label: 148 | # All splitters have empty label 149 | continue 150 | sub_menus.append(u"%s%s" % (role, label)) 151 | finally: 152 | if menu_clicked: 153 | menu_handle.Cancel() 154 | return sub_menus 155 | 156 | def verifymenucheck(self, window_name, object_name): 157 | """ 158 | Verify a menu item is checked 159 | 160 | @param window_name: Window name to look for, either full name, 161 | LDTP's name convention, or a Unix glob. 162 | @type window_name: string 163 | @param object_name: Object name to look for, either full name, 164 | LDTP's name convention, or a Unix glob. Or menu heirarchy 165 | @type object_name: string 166 | 167 | @return: 1 on success. 168 | @rtype: integer 169 | """ 170 | try: 171 | menu_handle=self._get_menu_handle(window_name, object_name, 172 | False) 173 | try: 174 | if menu_handle.AXMenuItemMarkChar: 175 | # Checked 176 | return 1 177 | except atomac._a11y.Error: 178 | pass 179 | except LdtpServerException: 180 | pass 181 | return 0 182 | 183 | def verifymenuuncheck(self, window_name, object_name): 184 | """ 185 | Verify a menu item is un-checked 186 | 187 | @param window_name: Window name to look for, either full name, 188 | LDTP's name convention, or a Unix glob. 189 | @type window_name: string 190 | @param object_name: Object name to look for, either full name, 191 | LDTP's name convention, or a Unix glob. Or menu heirarchy 192 | @type object_name: string 193 | 194 | @return: 1 on success. 195 | @rtype: integer 196 | """ 197 | try: 198 | menu_handle=self._get_menu_handle(window_name, object_name, 199 | False) 200 | try: 201 | if not menu_handle.AXMenuItemMarkChar: 202 | # Unchecked 203 | return 1 204 | except atomac._a11y.Error: 205 | return 1 206 | except LdtpServerException: 207 | pass 208 | return 0 209 | 210 | def menucheck(self, window_name, object_name): 211 | """ 212 | Check (click) a menu item. 213 | 214 | @param window_name: Window name to look for, either full name, 215 | LDTP's name convention, or a Unix glob. 216 | @type window_name: string 217 | @param object_name: Object name to look for, either full name, 218 | LDTP's name convention, or a Unix glob. Or menu heirarchy 219 | @type object_name: string 220 | 221 | @return: 1 on success. 222 | @rtype: integer 223 | """ 224 | menu_handle=self._get_menu_handle(window_name, object_name) 225 | if not menu_handle.AXEnabled: 226 | raise LdtpServerException(u"Object %s state disabled" % object_name) 227 | try: 228 | if menu_handle.AXMenuItemMarkChar: 229 | # Already checked 230 | return 1 231 | except atomac._a11y.Error: 232 | pass 233 | menu_handle.Press() 234 | return 1 235 | 236 | def menuuncheck(self, window_name, object_name): 237 | """ 238 | Uncheck (click) a menu item. 239 | 240 | @param window_name: Window name to look for, either full name, 241 | LDTP's name convention, or a Unix glob. 242 | @type window_name: string 243 | @param object_name: Object name to look for, either full name, 244 | LDTP's name convention, or a Unix glob. Or menu heirarchy 245 | @type object_name: string 246 | 247 | @return: 1 on success. 248 | @rtype: integer 249 | """ 250 | menu_handle=self._get_menu_handle(window_name, object_name) 251 | if not menu_handle.AXEnabled: 252 | raise LdtpServerException(u"Object %s state disabled" % object_name) 253 | try: 254 | if not menu_handle.AXMenuItemMarkChar: 255 | # Already unchecked 256 | return 1 257 | except atomac._a11y.Error: 258 | return 1 259 | menu_handle.Press() 260 | return 1 261 | -------------------------------------------------------------------------------- /atomac/ldtpd/mouse.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012 VMware, Inc. All Rights Reserved. 2 | 3 | # This file is part of ATOMac. 4 | 5 | #@author: Nagappan Alagappan 6 | #@copyright: Copyright (c) 2009-14 Nagappan Alagappan 7 | #@author: Sigbjørn Vik 8 | #@Copyright (C) 2013-14 Opera Software ASA (generatemouseevent API). 9 | #http://ldtp.freedesktop.org 10 | 11 | # ATOMac is free software; you can redistribute it and/or modify 12 | # it under the terms of the GNU General Public License as published by the Free 13 | # Software Foundation version 2 and no later version. 14 | 15 | # ATOMac is distributed in the hope that it will be useful, but 16 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 17 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License version 2 18 | # for more details. 19 | 20 | # You should have received a copy of the GNU General Public License along with 21 | # this program; if not, write to the Free Software Foundation, Inc., 51 Franklin 22 | # St, Fifth Floor, Boston, MA 02110-1301 USA. 23 | """Mouse class.""" 24 | 25 | import time 26 | from Quartz import CGEventCreateMouseEvent,\ 27 | CGEventPost,\ 28 | kCGHIDEventTap,\ 29 | CGEventSetIntegerValueField,\ 30 | kCGMouseEventClickState,\ 31 | CGEventGetLocation,\ 32 | CGEventCreate 33 | from Quartz import kCGEventMouseMoved as move 34 | from Quartz import kCGEventLeftMouseDown as press_left 35 | from Quartz import kCGEventLeftMouseUp as release_left 36 | from Quartz import kCGEventLeftMouseDragged as drag_left 37 | from Quartz import kCGEventRightMouseDown as press_right 38 | from Quartz import kCGEventRightMouseUp as release_right 39 | from Quartz import kCGEventRightMouseDragged as drag_right 40 | from Quartz import kCGEventOtherMouseDown as press_other 41 | from Quartz import kCGEventOtherMouseUp as release_other 42 | from Quartz import kCGEventOtherMouseDragged as drag_other 43 | 44 | from Quartz import kCGMouseButtonLeft as left 45 | from Quartz import kCGMouseButtonRight as right 46 | from Quartz import kCGMouseButtonCenter as centre 47 | 48 | from utils import Utils 49 | from server_exception import LdtpServerException 50 | 51 | single_click = 1 52 | double_click = 2 53 | triple_click = 3 54 | 55 | drag_default_button = 100 56 | 57 | # Global value to remember if any button should be down during moves 58 | drag_button_remembered = None 59 | 60 | mouse_click_override = {'single_click': single_click, 'double_click': double_click, 61 | 'triple_click': triple_click, 'move': move, 62 | 'press_left': press_left, 'release_left': release_left, 63 | 'drag_left': drag_left, 'press_right': press_right, 64 | 'release_right': release_right, 'drag_right': drag_right, 65 | 'press_other': press_other, 'release_other': release_other, 66 | 'drag_other': drag_other, 'left': left, 'right': right, 67 | 'centre': centre, 'drag_default_button': drag_default_button} 68 | 69 | class Mouse(Utils): 70 | def mouseleftclick(self, window_name, object_name): 71 | """ 72 | Mouse left click on an object. 73 | 74 | @param window_name: Window name to look for, either full name, 75 | LDTP's name convention, or a Unix glob. 76 | @type window_name: string 77 | @param object_name: Object name to look for, either full name, 78 | LDTP's name convention, or a Unix glob. Or menu heirarchy 79 | @type object_name: string 80 | 81 | @return: 1 on success. 82 | @rtype: integer 83 | """ 84 | object_handle = self._get_object_handle(window_name, object_name) 85 | if not object_handle.AXEnabled: 86 | raise LdtpServerException(u"Object %s state disabled" % object_name) 87 | self._grabfocus(object_handle) 88 | x, y, width, height = self._getobjectsize(object_handle) 89 | # Mouse left click on the object 90 | object_handle.clickMouseButtonLeft((x + width / 2, y + height / 2)) 91 | return 1 92 | 93 | def mouserightclick(self, window_name, object_name): 94 | """ 95 | Mouse right click on an object. 96 | 97 | @param window_name: Window name to look for, either full name, 98 | LDTP's name convention, or a Unix glob. 99 | @type window_name: string 100 | @param object_name: Object name to look for, either full name, 101 | LDTP's name convention, or a Unix glob. Or menu heirarchy 102 | @type object_name: string 103 | 104 | @return: 1 on success. 105 | @rtype: integer 106 | """ 107 | object_handle = self._get_object_handle(window_name, object_name) 108 | if not object_handle.AXEnabled: 109 | raise LdtpServerException(u"Object %s state disabled" % object_name) 110 | self._grabfocus(object_handle) 111 | x, y, width, height = self._getobjectsize(object_handle) 112 | # Mouse right click on the object 113 | object_handle.clickMouseButtonRight((x + width / 2, y + height / 2)) 114 | return 1 115 | 116 | def generatemouseevent(self, x, y, eventType="b1c", 117 | drag_button_override='drag_default_button'): 118 | """ 119 | Generate mouse event on x, y co-ordinates. 120 | 121 | @param x: X co-ordinate 122 | @type x: int 123 | @param y: Y co-ordinate 124 | @type y: int 125 | @param eventType: Mouse click type 126 | @type eventType: str 127 | @param drag_button_override: Any drag_xxx value 128 | Only relevant for movements, i.e. |type| = "abs" or "rel" 129 | Quartz is not fully compatible with windows, so for drags 130 | the drag button must be explicitly defined. generatemouseevent 131 | will remember the last button pressed by default, and drag 132 | that button, use this argument to override that. 133 | @type drag_button_override: str 134 | 135 | @return: 1 on success. 136 | @rtype: integer 137 | """ 138 | if drag_button_override not in mouse_click_override: 139 | raise ValueError('Unsupported drag_button_override type: %s' % \ 140 | drag_button_override) 141 | global drag_button_remembered 142 | point = (x, y) 143 | button = centre # Only matters for "other" buttons 144 | click_type = None 145 | if eventType == "abs" or eventType == "rel": 146 | if drag_button_override is not 'drag_default_button': 147 | events = [mouse_click_override[drag_button_override]] 148 | elif drag_button_remembered: 149 | events = [drag_button_remembered] 150 | else: 151 | events = [move] 152 | if eventType == "rel": 153 | point = CGEventGetLocation(CGEventCreate(None)) 154 | point.x += x 155 | point.y += y 156 | elif eventType == "b1p": 157 | events = [press_left] 158 | drag_button_remembered = drag_left 159 | elif eventType == "b1r": 160 | events = [release_left] 161 | drag_button_remembered = None 162 | elif eventType == "b1c": 163 | events = [press_left, release_left] 164 | elif eventType == "b1d": 165 | events = [press_left, release_left] 166 | click_type = double_click 167 | elif eventType == "b2p": 168 | events = [press_other] 169 | drag_button_remembered = drag_other 170 | elif eventType == "b2r": 171 | events = [release_other] 172 | drag_button_remembered = None 173 | elif eventType == "b2c": 174 | events = [press_other, release_other] 175 | elif eventType == "b2d": 176 | events = [press_other, release_other] 177 | click_type = double_click 178 | elif eventType == "b3p": 179 | events = [press_right] 180 | drag_button_remembered = drag_right 181 | elif eventType == "b3r": 182 | events = [release_right] 183 | drag_button_remembered = None 184 | elif eventType == "b3c": 185 | events = [press_right, release_right] 186 | elif eventType == "b3d": 187 | events = [press_right, release_right] 188 | click_type = double_click 189 | else: 190 | raise LdtpServerException(u"Mouse event '%s' not implemented" % eventType) 191 | 192 | for event in events: 193 | CG_event = CGEventCreateMouseEvent(None, event, point, button) 194 | if click_type: 195 | CGEventSetIntegerValueField( 196 | CG_event, kCGMouseEventClickState, click_type) 197 | CGEventPost(kCGHIDEventTap, CG_event) 198 | # Give the event time to happen 199 | time.sleep(0.01) 200 | return 1 201 | 202 | def mousemove(self, window_name, object_name): 203 | """ 204 | Mouse move on an object. 205 | 206 | @param window_name: Window name to look for, either full name, 207 | LDTP's name convention, or a Unix glob. 208 | @type window_name: string 209 | @param object_name: Object name to look for, either full name, 210 | LDTP's name convention, or a Unix glob. Or menu heirarchy 211 | @type object_name: string 212 | 213 | @return: 1 on success. 214 | @rtype: integer 215 | """ 216 | raise LdtpServerException("Not implemented") 217 | 218 | def doubleclick(self, window_name, object_name): 219 | """ 220 | Double click on the object 221 | 222 | @param window_name: Window name to look for, either full name, 223 | LDTP's name convention, or a Unix glob. 224 | @type window_name: string 225 | @param object_name: Object name to look for, either full name, 226 | LDTP's name convention, or a Unix glob. Or menu heirarchy 227 | @type object_name: string 228 | 229 | @return: 1 on success. 230 | @rtype: integer 231 | """ 232 | object_handle = self._get_object_handle(window_name, object_name) 233 | if not object_handle.AXEnabled: 234 | raise LdtpServerException(u"Object %s state disabled" % object_name) 235 | self._grabfocus(object_handle) 236 | x, y, width, height = self._getobjectsize(object_handle) 237 | window=self._get_front_most_window() 238 | # Mouse double click on the object 239 | #object_handle.doubleClick() 240 | window.doubleClickMouse((x + width / 2, y + height / 2)) 241 | return 1 242 | 243 | def simulatemousemove(self, source_x, source_y, dest_x, dest_y, delay = 0.0): 244 | """ 245 | @param source_x: Source X 246 | @type source_x: integer 247 | @param source_y: Source Y 248 | @type source_y: integer 249 | @param dest_x: Dest X 250 | @type dest_x: integer 251 | @param dest_y: Dest Y 252 | @type dest_y: integer 253 | @param delay: Sleep time between the mouse move 254 | @type delay: double 255 | 256 | @return: 1 if simulation was successful, 0 if not. 257 | @rtype: integer 258 | """ 259 | raise LdtpServerException("Not implemented") 260 | -------------------------------------------------------------------------------- /atomac/ldtpd/page_tab_list.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012 VMware, Inc. All Rights Reserved. 2 | 3 | # This file is part of ATOMac. 4 | 5 | #@author: Nagappan Alagappan 6 | #@copyright: Copyright (c) 2009-12 Nagappan Alagappan 7 | #http://ldtp.freedesktop.org 8 | 9 | # ATOMac is free software; you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by the Free 11 | # Software Foundation version 2 and no later version. 12 | 13 | # ATOMac is distributed in the hope that it will be useful, but 14 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 15 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License version 2 16 | # for more details. 17 | 18 | # You should have received a copy of the GNU General Public License along with 19 | # this program; if not, write to the Free Software Foundation, Inc., 51 Franklin 20 | # St, Fifth Floor, Boston, MA 02110-1301 USA. 21 | """PageTabList class.""" 22 | 23 | import re 24 | import fnmatch 25 | 26 | from utils import Utils 27 | from server_exception import LdtpServerException 28 | 29 | class PageTabList(Utils): 30 | def _get_tab_children(self, window_name, object_name): 31 | object_handle = self._get_object_handle(window_name, object_name) 32 | if not object_handle: 33 | raise LdtpServerException(u"Unable to find object %s" % object_name) 34 | return object_handle.AXChildren 35 | 36 | def _get_tab_handle(self, window_name, object_name, tab_name): 37 | children = self._get_tab_children(window_name, object_name) 38 | tab_handle = None 39 | for current_tab in children: 40 | role, label = self._ldtpize_accessible(current_tab) 41 | tmp_tab_name = fnmatch.translate(tab_name) 42 | if re.match(tmp_tab_name, label) or \ 43 | re.match(tmp_tab_name, u"%s%s" % (role, label)): 44 | tab_handle = current_tab 45 | break 46 | if not tab_handle: 47 | raise LdtpServerException(u"Unable to find tab %s" % tab_name) 48 | if not tab_handle.AXEnabled: 49 | raise LdtpServerException(u"Object %s state disabled" % object_name) 50 | return tab_handle 51 | 52 | def selecttab(self, window_name, object_name, tab_name): 53 | """ 54 | Select tab based on name. 55 | 56 | @param window_name: Window name to type in, either full name, 57 | LDTP's name convention, or a Unix glob. 58 | @type window_name: string 59 | @param object_name: Object name to type in, either full name, 60 | LDTP's name convention, or a Unix glob. 61 | @type object_name: string 62 | @param tab_name: tab to select 63 | @type data: string 64 | 65 | @return: 1 on success. 66 | @rtype: integer 67 | """ 68 | tab_handle = self._get_tab_handle(window_name, object_name, tab_name) 69 | tab_handle.Press() 70 | return 1 71 | 72 | def selecttabindex(self, window_name, object_name, tab_index): 73 | """ 74 | Select tab based on index. 75 | 76 | @param window_name: Window name to type in, either full name, 77 | LDTP's name convention, or a Unix glob. 78 | @type window_name: string 79 | @param object_name: Object name to type in, either full name, 80 | LDTP's name convention, or a Unix glob. 81 | @type object_name: string 82 | @param tab_index: tab to select 83 | @type data: integer 84 | 85 | @return: 1 on success. 86 | @rtype: integer 87 | """ 88 | children = self._get_tab_children(window_name, object_name) 89 | length = len(children) 90 | if tab_index < 0 or tab_index > length: 91 | raise LdtpServerException(u"Invalid tab index %s" % tab_index) 92 | tab_handle = children[tab_index] 93 | if not tab_handle.AXEnabled: 94 | raise LdtpServerException(u"Object %s state disabled" % object_name) 95 | tab_handle.Press() 96 | return 1 97 | 98 | def verifytabname(self, window_name, object_name, tab_name): 99 | """ 100 | Verify tab name. 101 | 102 | @param window_name: Window name to type in, either full name, 103 | LDTP's name convention, or a Unix glob. 104 | @type window_name: string 105 | @param object_name: Object name to type in, either full name, 106 | LDTP's name convention, or a Unix glob. 107 | @type object_name: string 108 | @param tab_name: tab to select 109 | @type data: string 110 | 111 | @return: 1 on success 0 on failure 112 | @rtype: integer 113 | """ 114 | try: 115 | tab_handle = self._get_tab_handle(window_name, object_name, tab_name) 116 | if tab_handle.AXValue: 117 | return 1 118 | except LdtpServerException: 119 | pass 120 | return 0 121 | 122 | def gettabcount(self, window_name, object_name): 123 | """ 124 | Get tab count. 125 | 126 | @param window_name: Window name to type in, either full name, 127 | LDTP's name convention, or a Unix glob. 128 | @type window_name: string 129 | @param object_name: Object name to type in, either full name, 130 | LDTP's name convention, or a Unix glob. 131 | @type object_name: string 132 | 133 | @return: tab count on success. 134 | @rtype: integer 135 | """ 136 | children = self._get_tab_children(window_name, object_name) 137 | return len(children) 138 | 139 | def gettabname(self, window_name, object_name, tab_index): 140 | """ 141 | Get tab name 142 | 143 | @param window_name: Window name to type in, either full name, 144 | LDTP's name convention, or a Unix glob. 145 | @type window_name: string 146 | @param object_name: Object name to type in, either full name, 147 | LDTP's name convention, or a Unix glob. 148 | @type object_name: string 149 | @param tab_index: Index of tab (zero based index) 150 | @type object_name: int 151 | 152 | @return: text on success. 153 | @rtype: string 154 | """ 155 | children = self._get_tab_children(window_name, object_name) 156 | length = len(children) 157 | if tab_index < 0 or tab_index > length: 158 | raise LdtpServerException(u"Invalid tab index %s" % tab_index) 159 | tab_handle = children[tab_index] 160 | if not tab_handle.AXEnabled: 161 | raise LdtpServerException(u"Object %s state disabled" % object_name) 162 | return tab_handle.AXTitle 163 | -------------------------------------------------------------------------------- /atomac/ldtpd/server_exception.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012 VMware, Inc. All Rights Reserved. 2 | 3 | # This file is part of ATOMac. 4 | 5 | #@author: Nagappan Alagappan 6 | #@copyright: Copyright (c) 2009-12 Nagappan Alagappan 7 | #http://ldtp.freedesktop.org 8 | 9 | # ATOMac is free software; you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by the Free 11 | # Software Foundation version 2 and no later version. 12 | 13 | # ATOMac is distributed in the hope that it will be useful, but 14 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 15 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License version 2 16 | # for more details. 17 | 18 | # You should have received a copy of the GNU General Public License along with 19 | # this program; if not, write to the Free Software Foundation, Inc., 51 Franklin 20 | # St, Fifth Floor, Boston, MA 02110-1301 USA. 21 | """Exception class.""" 22 | 23 | import xmlrpclib 24 | 25 | ERROR_CODE = 123 26 | 27 | class LdtpServerException(xmlrpclib.Fault): 28 | def __init__(self, message): 29 | xmlrpclib.Fault.__init__(self, ERROR_CODE, message) 30 | -------------------------------------------------------------------------------- /atomac/ldtpd/test.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012 VMware, Inc. All Rights Reserved. 2 | 3 | # This file is part of ATOMac. 4 | 5 | #@author: Nagappan Alagappan 6 | #@copyright: Copyright (c) 2009-12 Nagappan Alagappan 7 | #http://ldtp.freedesktop.org 8 | 9 | # ATOMac is free software; you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by the Free 11 | # Software Foundation version 2 and no later version. 12 | 13 | # ATOMac is distributed in the hope that it will be useful, but 14 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 15 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License version 2 16 | # for more details. 17 | 18 | # You should have received a copy of the GNU General Public License along with 19 | # this program; if not, write to the Free Software Foundation, Inc., 51 Franklin 20 | # St, Fifth Floor, Boston, MA 02110-1301 USA. 21 | """Core class to be exposed via XMLRPC in LDTP daemon.""" 22 | 23 | import core 24 | 25 | test=core.Core() 26 | #test.launchapp('Calculator') 27 | #print test.launchapp('Chicken of the VNC') 28 | #print test.launchapp('app does not exist') 29 | #test.wait(5) 30 | #test.generatekeyevent('') 31 | #test.generatekeyevent('d') 32 | #test.generatekeyevent('') 33 | #test.generatekeyevent('t') 34 | #test.generatekeyevent('n') 35 | #test.generatekeyevent('') # Not working 36 | #test.keypress('') # Not working 37 | #test.generatekeyevent('abc') 38 | #test.generatekeyevent('xyz') 39 | #test.generatekeyevent('abc') # Caps lock not working 40 | #test.keyrelease('') # Not working 41 | #test.imagecapture('Untitled') 42 | #apps=test.getapplist() 43 | #windows=test.getwindowlist() 44 | #test.generatemouseevent(10, 10) 45 | #test.wait(3) 46 | #size=test.getobjectsize("Jay") 47 | #test.generatemouseevent(size[0] + 100, size[1] + 5, "b3c") 48 | #test.generatemouseevent(size[0]-100, size[1], "b1d") 49 | #print test.guiexist("gedit") 50 | #print test.guiexist("gedit", "txt0") 51 | #print test.guiexist("Open") 52 | #print test.guiexist("Open", "btnCancel") 53 | #print test.guiexist("Open", "C0ncel") 54 | #print "waittillguiexist" 55 | #print test.waittillguiexist("Open") 56 | #print test.waittillguiexist("Open", "btnCancel") 57 | #print test.waittillguiexist("Open", "C0ncel", 10) 58 | #print "waittillguinotexist" 59 | #print test.waittillguinotexist("Open", guiTimeOut=5) 60 | #print test.waittillguinotexist("Open", "btnCancel", 5) 61 | #print test.waittillguinotexist("Open", "C0ncel") 62 | #print windows 63 | #objList = test.getobjectlist("frmTryitEditorv1.5") 64 | #for obj in objList: 65 | #if re.search("^tbl\d", obj): 66 | #print obj, test.getrowcount("frmTryitEditorv1.5", obj) 67 | #print test.selectrow("Accounts", "tbl0", "VMware") 68 | #print test.selectrowpartialmatch("Accounts", "tbl0", "Zim") 69 | #print test.selectrowindex("Accounts", "tbl0", 0) 70 | #print test.selectlastrow("Accounts", "tbl0") 71 | #print test.getcellvalue("Accounts", "tbl0", 1) 72 | #print test.scrollup("Downloads", "scbr0") 73 | #print test.oneright("Downloads", "scbr1", 3) 74 | #print len(apps), len(windows) 75 | #print apps, windows 76 | #print test.getobjectlist("Contacts") 77 | #print test.click("Open", "Cancel") 78 | #print test.comboselect("frmInstruments", "cboAdd", "UiAutomation.js") 79 | #print test.comboselect("frmInstruments", "Choose Target", "Choose Target;Octopus") 80 | #print test.getobjectlist("frmInstruments") 81 | #print test.check("frmInstruments", "chkRecordOnce") 82 | #print test.wait(1) 83 | #print test.uncheck("frmInstruments", "chkRepeatRecording") 84 | #print test.uncheck("frmInstruments", "chkPause") 85 | #print test.verifyuncheck("frmInstruments", "chkPause") 86 | #print test.verifycheck("frmInstruments", "chkRepeatRecording") 87 | #print test.doesmenuitemexist("Instru*", "File;Open...") 88 | #print test.doesmenuitemexist("Instruments*", "File;Open...") 89 | #print test.doesmenuitemexist("Instruments*", "File;Open*") 90 | #print test.selectmenuitem("Instruments*", "File;Open*") 91 | #print test.checkmenu("Instruments*", "View;Instruments") 92 | #test.wait(1) 93 | #print test.checkmenu("Instruments*", "View;Instruments") 94 | #print test.uncheckmenu("Instruments*", "View;Instruments") 95 | #test.wait(1) 96 | #print test.verifymenucheck("Instruments*", "View;Instruments") 97 | #print test.verifymenuuncheck("Instruments*", "View;Instruments") 98 | #print test.checkmenu("Instruments*", "View;Instruments") 99 | #test.wait(1) 100 | #print test.verifymenucheck("Instruments*", "View;Instruments") 101 | #print test.verifymenuuncheck("Instruments*", "View;Instruments") 102 | # Instruments Open dialog 103 | #print test.mouseleftclick("Open", "Cancel") 104 | #a=test.getobjectlist("Open") 105 | #for i in a: 106 | # if i.find("txt") != -1: 107 | # print i 108 | #print test.settextvalue("Open", "txttextfield", "pyatom ldtp") 109 | #print test.gettextvalue("Open", "txttextfield") 110 | #print test.getobjectinfo('Open', 'txttextfield') 111 | #print test.getobjectproperty('Open', 'txttextfield', 'class') 112 | #print test.inserttext("Open", "txttextfield", 0, "pyatom ldtp") 113 | #print test.getcharcount("Open", "txttextfield") 114 | #print test.menuitemenabled("Instruments*", "File;Record Trace") 115 | #print test.menuitemenabled("Instruments*", "File;Pause Trace") 116 | #print test.listsubmenus("Instruments*", "Fi*") 117 | #print test.listsubmenus("Instruments*", "File;OpenRecent") 118 | #print test.listsubmenus("Instruments*", "File;mnuOpenRecent") 119 | #print test.listsubmenus("Instruments*", "File;GetInfo") 120 | #try: 121 | # print test.listsubmenus("Instruments*", "File;ding") 122 | #except LdtpServerException: 123 | # pass 124 | #try: 125 | # print test.listsubmenus("Instruments*", "ding") 126 | #except LdtpServerException: 127 | # pass 128 | #try: 129 | # print test.listsubmenus("ding", "dong") 130 | #except LdtpServerException: 131 | # pass 132 | #print test.getcursorposition("Open", "txttextfield") 133 | #print test.setcursorposition("Open", "txttextfield", 10) 134 | #print test.cuttext("Open", "txttextfield", 2) 135 | #print test.cuttext("Open", "txttextfield", 2, 20) 136 | #print test.pastetext("Open", "txttextfield", 2) 137 | #print test.gettabname("*ldtpd*python*", "ptl0", 2) 138 | #print test.gettabcount("*ldtpd*python*", "ptl0") 139 | #print test.selecttabindex("*ldtpd*python*", "ptl0", 2) 140 | #print test.selecttab("*ldtpd*python*", "ptl0", "*bash*") 141 | #print test.verifytabname("*ldtpd*python*", "ptl0", "*gabe*") 142 | #print test.selectindex("frmInstruments", "cboAdd", 1) 143 | #print test.getallitem("frmInstruments", "cboAdd") 144 | #print test.selectindex("frmInstruments", "cboAdd", 10) 145 | #print test.showlist("frmInstruments", "cboAdd") 146 | #test.wait(1) 147 | #print test.verifydropdown("frmInstruments", "cboAdd") 148 | #print test.hidelist("frmInstruments", "cboAdd") 149 | #test.wait(1) 150 | #print test.verifydropdown("frmInstruments", "cboAdd") 151 | #print test.showlist("frmInstruments", "cboAdd") 152 | #test.wait(1) 153 | #print test.verifyshowlist("frmInstruments", "cboAdd") 154 | #print test.hidelist("frmInstruments", "cboAdd") 155 | #test.wait(1) 156 | #print test.verifyhidelist("frmInstruments", "cboAdd") 157 | # Terminal settings window 158 | #print test.comboselect("frmInstruments", "lst0", "Trace Log") 159 | #print test.getallstates("Settings", "chkUseboldfonts") 160 | #print test.getallstates("Settings", "chkAntialiastext") 161 | #print test.getallstates("Settings", "rbtn*Block") 162 | #print test.getallstates("Settings", "rbtn*Underline") 163 | #print test.getaccesskey("test*Python*", "Window;Zoom") # Will raise exception 164 | #print test.getaccesskey("test*Python*", "View;Scroll to Bottom") 165 | # Based on preview open dialog 166 | #print test.singleclickrow('Open', 'otloutline1', '31_09.jpeg') 167 | #print test.doubleclickrow('Open', 'otloutline1', '31_09.jpeg') 168 | #print test.doubleclickrowindex('Open', 'otloutline1', 0) 169 | #print test.rightclick('Open', 'otloutline1', '31_09.jpeg') 170 | # Based on Adium 171 | #print test.ldtp.multiselect('Contacts', 'otloutline', ['Nagappan A', 'nagappanal']) 172 | #test.wait(1) 173 | #print test.ldtp.multiremove('Contacts', 'otloutline', ['Nagappan A', 'nagappanal']) 174 | -------------------------------------------------------------------------------- /atomac/ldtpd/text.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012 VMware, Inc. All Rights Reserved. 2 | 3 | # This file is part of ATOMac. 4 | 5 | #@author: Nagappan Alagappan 6 | #@copyright: Copyright (c) 2009-12 Nagappan Alagappan 7 | #http://ldtp.freedesktop.org 8 | 9 | # ATOMac is free software; you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by the Free 11 | # Software Foundation version 2 and no later version. 12 | 13 | # ATOMac is distributed in the hope that it will be useful, but 14 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 15 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License version 2 16 | # for more details. 17 | 18 | # You should have received a copy of the GNU General Public License along with 19 | # this program; if not, write to the Free Software Foundation, Inc., 51 Franklin 20 | # St, Fifth Floor, Boston, MA 02110-1301 USA. 21 | """Text class.""" 22 | 23 | import re 24 | import fnmatch 25 | import atomac.Clipboard as Clipboard 26 | 27 | from utils import Utils 28 | from keypress_actions import KeyComboAction, KeyPressAction, KeyReleaseAction 29 | from server_exception import LdtpServerException 30 | 31 | class Text(Utils): 32 | def generatekeyevent(self, data): 33 | """ 34 | Generates key event to the system, this simulates the best user like 35 | interaction via keyboard. 36 | 37 | @param data: data to type. 38 | @type data: string 39 | 40 | @return: 1 on success. 41 | @rtype: integer 42 | """ 43 | KeyComboAction(data) 44 | return 1 45 | 46 | def keypress(self, data): 47 | """ 48 | Press key. NOTE: keyrelease should be called 49 | 50 | @param data: data to type. 51 | @type data: string 52 | 53 | @return: 1 on success. 54 | @rtype: integer 55 | """ 56 | try: 57 | window=self._get_front_most_window() 58 | except (IndexError, ): 59 | window=self._get_any_window() 60 | key_press_action = KeyPressAction(window, data) 61 | return 1 62 | 63 | def keyrelease(self, data): 64 | """ 65 | Release key. NOTE: keypress should be called before this 66 | 67 | @param data: data to type. 68 | @type data: string 69 | 70 | @return: 1 on success. 71 | @rtype: integer 72 | """ 73 | try: 74 | window=self._get_front_most_window() 75 | except (IndexError, ): 76 | window=self._get_any_window() 77 | key_release_action = KeyReleaseAction(window, data) 78 | return 1 79 | 80 | def enterstring(self, window_name, object_name='', data=''): 81 | """ 82 | Type string sequence. 83 | 84 | @param window_name: Window name to focus on, either full name, 85 | LDTP's name convention, or a Unix glob. 86 | @type window_name: string 87 | @param object_name: Object name to focus on, either full name, 88 | LDTP's name convention, or a Unix glob. 89 | @type object_name: string 90 | @param data: data to type. 91 | @type data: string 92 | 93 | @return: 1 on success. 94 | @rtype: integer 95 | """ 96 | if not object_name and not data: 97 | return self.generatekeyevent(window_name) 98 | else: 99 | object_handle=self._get_object_handle(window_name, object_name) 100 | if not object_handle.AXEnabled: 101 | raise LdtpServerException(u"Object %s state disabled" % object_name) 102 | self._grabfocus(object_handle) 103 | object_handle.sendKeys(data) 104 | return 1 105 | 106 | def settextvalue(self, window_name, object_name, data): 107 | """ 108 | Type string sequence. 109 | 110 | @param window_name: Window name to type in, either full name, 111 | LDTP's name convention, or a Unix glob. 112 | @type window_name: string 113 | @param object_name: Object name to type in, either full name, 114 | LDTP's name convention, or a Unix glob. 115 | @type object_name: string 116 | @param data: data to type. 117 | @type data: string 118 | 119 | @return: 1 on success. 120 | @rtype: integer 121 | """ 122 | object_handle=self._get_object_handle(window_name, object_name) 123 | if not object_handle.AXEnabled: 124 | raise LdtpServerException(u"Object %s state disabled" % object_name) 125 | object_handle.AXValue=data 126 | return 1 127 | 128 | def gettextvalue(self, window_name, object_name, startPosition=0, endPosition=0): 129 | """ 130 | Get text value 131 | 132 | @param window_name: Window name to type in, either full name, 133 | LDTP's name convention, or a Unix glob. 134 | @type window_name: string 135 | @param object_name: Object name to type in, either full name, 136 | LDTP's name convention, or a Unix glob. 137 | @type object_name: string 138 | @param startPosition: Starting position of text to fetch 139 | @type: startPosition: int 140 | @param endPosition: Ending position of text to fetch 141 | @type: endPosition: int 142 | 143 | @return: text on success. 144 | @rtype: string 145 | """ 146 | object_handle=self._get_object_handle(window_name, object_name) 147 | if not object_handle.AXEnabled: 148 | raise LdtpServerException(u"Object %s state disabled" % object_name) 149 | return object_handle.AXValue 150 | 151 | def inserttext(self, window_name, object_name, position, data): 152 | """ 153 | Insert string sequence in given position. 154 | 155 | @param window_name: Window name to type in, either full name, 156 | LDTP's name convention, or a Unix glob. 157 | @type window_name: string 158 | @param object_name: Object name to type in, either full name, 159 | LDTP's name convention, or a Unix glob. 160 | @type object_name: string 161 | @param position: position where text has to be entered. 162 | @type data: int 163 | @param data: data to type. 164 | @type data: string 165 | 166 | @return: 1 on success. 167 | @rtype: integer 168 | """ 169 | object_handle=self._get_object_handle(window_name, object_name) 170 | if not object_handle.AXEnabled: 171 | raise LdtpServerException(u"Object %s state disabled" % object_name) 172 | existing_data=object_handle.AXValue 173 | size=len(existing_data) 174 | if position < 0: 175 | position=0 176 | if position > size: 177 | position=size 178 | object_handle.AXValue=existing_data[:position] + data + \ 179 | existing_data[position:] 180 | return 1 181 | 182 | def verifypartialmatch(self, window_name, object_name, partial_text): 183 | """ 184 | Verify partial text 185 | 186 | @param window_name: Window name to type in, either full name, 187 | LDTP's name convention, or a Unix glob. 188 | @type window_name: string 189 | @param object_name: Object name to type in, either full name, 190 | LDTP's name convention, or a Unix glob. 191 | @type object_name: string 192 | @param partial_text: Partial text to match 193 | @type object_name: string 194 | 195 | @return: 1 on success. 196 | @rtype: integer 197 | """ 198 | try: 199 | if re.search(fnmatch.translate(partial_text), 200 | self.gettextvalue(window_name, 201 | object_name)): 202 | return 1 203 | except: 204 | pass 205 | return 0 206 | 207 | def verifysettext(self, window_name, object_name, text): 208 | """ 209 | Verify text is set correctly 210 | 211 | @param window_name: Window name to type in, either full name, 212 | LDTP's name convention, or a Unix glob. 213 | @type window_name: string 214 | @param object_name: Object name to type in, either full name, 215 | LDTP's name convention, or a Unix glob. 216 | @type object_name: string 217 | @param text: text to match 218 | @type object_name: string 219 | 220 | @return: 1 on success. 221 | @rtype: integer 222 | """ 223 | try: 224 | return int(re.match(fnmatch.translate(text), 225 | self.gettextvalue(window_name, 226 | object_name))) 227 | except: 228 | return 0 229 | 230 | def istextstateenabled(self, window_name, object_name): 231 | """ 232 | Verifies text state enabled or not 233 | 234 | @param window_name: Window name to type in, either full name, 235 | LDTP's name convention, or a Unix glob. 236 | @type window_name: string 237 | @param object_name: Object name to type in, either full name, 238 | LDTP's name convention, or a Unix glob. 239 | @type object_name: string 240 | 241 | @return: 1 on success 0 on failure. 242 | @rtype: integer 243 | """ 244 | try: 245 | object_handle=self._get_object_handle(window_name, object_name) 246 | if object_handle.AXEnabled: 247 | return 1 248 | except LdtpServerException: 249 | pass 250 | return 0 251 | 252 | def getcharcount(self, window_name, object_name): 253 | """ 254 | Get character count 255 | 256 | @param window_name: Window name to type in, either full name, 257 | LDTP's name convention, or a Unix glob. 258 | @type window_name: string 259 | @param object_name: Object name to type in, either full name, 260 | LDTP's name convention, or a Unix glob. 261 | @type object_name: string 262 | 263 | @return: 1 on success. 264 | @rtype: integer 265 | """ 266 | object_handle=self._get_object_handle(window_name, object_name) 267 | if not object_handle.AXEnabled: 268 | raise LdtpServerException(u"Object %s state disabled" % object_name) 269 | return object_handle.AXNumberOfCharacters 270 | 271 | def appendtext(self, window_name, object_name, data): 272 | """ 273 | Append string sequence. 274 | 275 | @param window_name: Window name to type in, either full name, 276 | LDTP's name convention, or a Unix glob. 277 | @type window_name: string 278 | @param object_name: Object name to type in, either full name, 279 | LDTP's name convention, or a Unix glob. 280 | @type object_name: string 281 | @param data: data to type. 282 | @type data: string 283 | 284 | @return: 1 on success. 285 | @rtype: integer 286 | """ 287 | object_handle=self._get_object_handle(window_name, object_name) 288 | if not object_handle.AXEnabled: 289 | raise LdtpServerException(u"Object %s state disabled" % object_name) 290 | object_handle.AXValue += data 291 | return 1 292 | 293 | def getcursorposition(self, window_name, object_name): 294 | """ 295 | Get cursor position 296 | 297 | @param window_name: Window name to type in, either full name, 298 | LDTP's name convention, or a Unix glob. 299 | @type window_name: string 300 | @param object_name: Object name to type in, either full name, 301 | LDTP's name convention, or a Unix glob. 302 | @type object_name: string 303 | 304 | @return: Cursor position on success. 305 | @rtype: integer 306 | """ 307 | object_handle=self._get_object_handle(window_name, object_name) 308 | if not object_handle.AXEnabled: 309 | raise LdtpServerException(u"Object %s state disabled" % object_name) 310 | return object_handle.AXSelectedTextRange.loc 311 | 312 | def setcursorposition(self, window_name, object_name, cursor_position): 313 | """ 314 | Set cursor position 315 | 316 | @param window_name: Window name to type in, either full name, 317 | LDTP's name convention, or a Unix glob. 318 | @type window_name: string 319 | @param object_name: Object name to type in, either full name, 320 | LDTP's name convention, or a Unix glob. 321 | @type object_name: string 322 | @param cursor_position: Cursor position to be set 323 | @type object_name: string 324 | 325 | @return: 1 on success. 326 | @rtype: integer 327 | """ 328 | object_handle=self._get_object_handle(window_name, object_name) 329 | if not object_handle.AXEnabled: 330 | raise LdtpServerException(u"Object %s state disabled" % object_name) 331 | object_handle.AXSelectedTextRange.loc=cursor_position 332 | return 1 333 | 334 | def cuttext(self, window_name, object_name, start_position, end_position=-1): 335 | """ 336 | cut text from start position to end position 337 | 338 | @param window_name: Window name to type in, either full name, 339 | LDTP's name convention, or a Unix glob. 340 | @type window_name: string 341 | @param object_name: Object name to type in, either full name, 342 | LDTP's name convention, or a Unix glob. 343 | @type object_name: string 344 | @param start_position: Start position 345 | @type object_name: integer 346 | @param end_position: End position, default -1 347 | Cut all the text from start position till end 348 | @type object_name: integer 349 | 350 | @return: 1 on success. 351 | @rtype: integer 352 | """ 353 | object_handle=self._get_object_handle(window_name, object_name) 354 | if not object_handle.AXEnabled: 355 | raise LdtpServerException(u"Object %s state disabled" % object_name) 356 | size=object_handle.AXNumberOfCharacters 357 | if end_position == -1 or end_position > size: 358 | end_position=size 359 | if start_position < 0: 360 | start_position=0 361 | data=object_handle.AXValue 362 | Clipboard.copy(data[start_position:end_position]) 363 | object_handle.AXValue=data[:start_position] + data[end_position:] 364 | return 1 365 | 366 | def copytext(self, window_name, object_name, start_position, end_position=-1): 367 | """ 368 | copy text from start position to end position 369 | 370 | @param window_name: Window name to type in, either full name, 371 | LDTP's name convention, or a Unix glob. 372 | @type window_name: string 373 | @param object_name: Object name to type in, either full name, 374 | LDTP's name convention, or a Unix glob. 375 | @type object_name: string 376 | @param start_position: Start position 377 | @type object_name: integer 378 | @param end_position: End position, default -1 379 | Copy all the text from start position till end 380 | @type object_name: integer 381 | 382 | @return: 1 on success. 383 | @rtype: integer 384 | """ 385 | object_handle=self._get_object_handle(window_name, object_name) 386 | if not object_handle.AXEnabled: 387 | raise LdtpServerException(u"Object %s state disabled" % object_name) 388 | size=object_handle.AXNumberOfCharacters 389 | if end_position == -1 or end_position > size: 390 | end_position=size 391 | if start_position < 0: 392 | start_position=0 393 | data=object_handle.AXValue 394 | Clipboard.copy(data[start_position:end_position]) 395 | return 1 396 | 397 | 398 | def deletetext(self, window_name, object_name, start_position, end_position=-1): 399 | """ 400 | delete text from start position to end position 401 | 402 | @param window_name: Window name to type in, either full name, 403 | LDTP's name convention, or a Unix glob. 404 | @type window_name: string 405 | @param object_name: Object name to type in, either full name, 406 | LDTP's name convention, or a Unix glob. 407 | @type object_name: string 408 | @param start_position: Start position 409 | @type object_name: integer 410 | @param end_position: End position, default -1 411 | Delete all the text from start position till end 412 | @type object_name: integer 413 | 414 | @return: 1 on success. 415 | @rtype: integer 416 | """ 417 | object_handle=self._get_object_handle(window_name, object_name) 418 | if not object_handle.AXEnabled: 419 | raise LdtpServerException(u"Object %s state disabled" % object_name) 420 | size=object_handle.AXNumberOfCharacters 421 | if end_position == -1 or end_position > size: 422 | end_position=size 423 | if start_position < 0: 424 | start_position=0 425 | data=object_handle.AXValue 426 | object_handle.AXValue=data[:start_position] + data[end_position:] 427 | return 1 428 | 429 | def pastetext(self, window_name, object_name, position=0): 430 | """ 431 | paste text from start position to end position 432 | 433 | @param window_name: Window name to type in, either full name, 434 | LDTP's name convention, or a Unix glob. 435 | @type window_name: string 436 | @param object_name: Object name to type in, either full name, 437 | LDTP's name convention, or a Unix glob. 438 | @type object_name: string 439 | @param position: Position to paste the text, default 0 440 | @type object_name: integer 441 | 442 | @return: 1 on success. 443 | @rtype: integer 444 | """ 445 | object_handle=self._get_object_handle(window_name, object_name) 446 | if not object_handle.AXEnabled: 447 | raise LdtpServerException(u"Object %s state disabled" % object_name) 448 | size=object_handle.AXNumberOfCharacters 449 | if position > size: 450 | position=size 451 | if position < 0: 452 | position=0 453 | clipboard=Clipboard.paste() 454 | data=object_handle.AXValue 455 | object_handle.AXValue=data[:position] + clipboard + data[position:] 456 | return 1 457 | -------------------------------------------------------------------------------- /atomac/ldtpd/value.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012 VMware, Inc. All Rights Reserved. 2 | 3 | # This file is part of ATOMac. 4 | 5 | #@author: Yingjun Li 6 | #@copyright: Copyright (c) 2009-12 Yingjun Li 7 | #http://ldtp.freedesktop.org 8 | 9 | # ATOMac is free software; you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by the Free 11 | # Software Foundation version 2 and no later version. 12 | 13 | # ATOMac is distributed in the hope that it will be useful, but 14 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 15 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License version 2 16 | # for more details. 17 | 18 | # You should have received a copy of the GNU General Public License along with 19 | # this program; if not, write to the Free Software Foundation, Inc., 51 Franklin 20 | # St, Fifth Floor, Boston, MA 02110-1301 USA. 21 | """Value class.""" 22 | 23 | import time 24 | 25 | from utils import Utils 26 | from server_exception import LdtpServerException 27 | 28 | class Value(Utils): 29 | def verifyscrollbarvertical(self, window_name, object_name): 30 | """ 31 | Verify scrollbar is vertical 32 | 33 | @param window_name: Window name to type in, either full name, 34 | LDTP's name convention, or a Unix glob. 35 | @type window_name: string 36 | @param object_name: Object name to type in, either full name, 37 | LDTP's name convention, or a Unix glob. 38 | @type object_name: string 39 | 40 | @return: 1 on success. 41 | @rtype: integer 42 | """ 43 | try: 44 | object_handle = self._get_object_handle(window_name, object_name) 45 | if object_handle.AXOrientation == "AXVerticalOrientation": 46 | return 1 47 | except: 48 | pass 49 | return 0 50 | 51 | def verifyscrollbarhorizontal(self, window_name, object_name): 52 | """ 53 | Verify scrollbar is horizontal 54 | 55 | @param window_name: Window name to type in, either full name, 56 | LDTP's name convention, or a Unix glob. 57 | @type window_name: string 58 | @param object_name: Object name to type in, either full name, 59 | LDTP's name convention, or a Unix glob. 60 | @type object_name: string 61 | 62 | @return: 1 on success. 63 | @rtype: integer 64 | """ 65 | try: 66 | object_handle = self._get_object_handle(window_name, object_name) 67 | if object_handle.AXOrientation == "AXHorizontalOrientation": 68 | return 1 69 | except: 70 | pass 71 | return 0 72 | 73 | def setmax(self, window_name, object_name): 74 | """ 75 | Set max value 76 | 77 | @param window_name: Window name to type in, either full name, 78 | LDTP's name convention, or a Unix glob. 79 | @type window_name: string 80 | @param object_name: Object name to type in, either full name, 81 | LDTP's name convention, or a Unix glob. 82 | @type object_name: string 83 | 84 | @return: 1 on success. 85 | @rtype: integer 86 | """ 87 | object_handle = self._get_object_handle(window_name, object_name) 88 | object_handle.AXValue = 1 89 | return 1 90 | 91 | def setmin(self, window_name, object_name): 92 | """ 93 | Set min value 94 | 95 | @param window_name: Window name to type in, either full name, 96 | LDTP's name convention, or a Unix glob. 97 | @type window_name: string 98 | @param object_name: Object name to type in, either full name, 99 | LDTP's name convention, or a Unix glob. 100 | @type object_name: string 101 | 102 | @return: 1 on success. 103 | @rtype: integer 104 | """ 105 | object_handle = self._get_object_handle(window_name, object_name) 106 | object_handle.AXValue = 0 107 | return 1 108 | 109 | def scrollup(self, window_name, object_name): 110 | """ 111 | Scroll up 112 | 113 | @param window_name: Window name to type in, either full name, 114 | LDTP's name convention, or a Unix glob. 115 | @type window_name: string 116 | @param object_name: Object name to type in, either full name, 117 | LDTP's name convention, or a Unix glob. 118 | @type object_name: string 119 | 120 | @return: 1 on success. 121 | @rtype: integer 122 | """ 123 | if not self.verifyscrollbarvertical(window_name, object_name): 124 | raise LdtpServerException('Object not vertical scrollbar') 125 | return self.setmin(window_name, object_name) 126 | 127 | def scrolldown(self, window_name, object_name): 128 | """ 129 | Scroll down 130 | 131 | @param window_name: Window name to type in, either full name, 132 | LDTP's name convention, or a Unix glob. 133 | @type window_name: string 134 | @param object_name: Object name to type in, either full name, 135 | LDTP's name convention, or a Unix glob. 136 | @type object_name: string 137 | 138 | @return: 1 on success. 139 | @rtype: integer 140 | """ 141 | if not self.verifyscrollbarvertical(window_name, object_name): 142 | raise LdtpServerException('Object not vertical scrollbar') 143 | return self.setmax(window_name, object_name) 144 | 145 | def scrollleft(self, window_name, object_name): 146 | """ 147 | Scroll left 148 | 149 | @param window_name: Window name to type in, either full name, 150 | LDTP's name convention, or a Unix glob. 151 | @type window_name: string 152 | @param object_name: Object name to type in, either full name, 153 | LDTP's name convention, or a Unix glob. 154 | @type object_name: string 155 | 156 | @return: 1 on success. 157 | @rtype: integer 158 | """ 159 | if not self.verifyscrollbarhorizontal(window_name, object_name): 160 | raise LdtpServerException('Object not horizontal scrollbar') 161 | return self.setmin(window_name, object_name) 162 | 163 | def scrollright(self, window_name, object_name): 164 | """ 165 | Scroll right 166 | 167 | @param window_name: Window name to type in, either full name, 168 | LDTP's name convention, or a Unix glob. 169 | @type window_name: string 170 | @param object_name: Object name to type in, either full name, 171 | LDTP's name convention, or a Unix glob. 172 | @type object_name: string 173 | 174 | @return: 1 on success. 175 | @rtype: integer 176 | """ 177 | if not self.verifyscrollbarhorizontal(window_name, object_name): 178 | raise LdtpServerException('Object not horizontal scrollbar') 179 | return self.setmax(window_name, object_name) 180 | 181 | def onedown(self, window_name, object_name, iterations): 182 | """ 183 | Press scrollbar down with number of iterations 184 | 185 | @param window_name: Window name to type in, either full name, 186 | LDTP's name convention, or a Unix glob. 187 | @type window_name: string 188 | @param object_name: Object name to type in, either full name, 189 | LDTP's name convention, or a Unix glob. 190 | @type object_name: string 191 | @param interations: iterations to perform on slider increase 192 | @type iterations: integer 193 | 194 | @return: 1 on success. 195 | @rtype: integer 196 | """ 197 | if not self.verifyscrollbarvertical(window_name, object_name): 198 | raise LdtpServerException('Object not vertical scrollbar') 199 | object_handle = self._get_object_handle(window_name, object_name) 200 | i = 0 201 | maxValue = 1.0 / 8 202 | flag = False 203 | while i < iterations: 204 | if object_handle.AXValue >= 1: 205 | raise LdtpServerException('Maximum limit reached') 206 | object_handle.AXValue += maxValue 207 | time.sleep(1.0 / 100) 208 | flag = True 209 | i += 1 210 | if flag: 211 | return 1 212 | else: 213 | raise LdtpServerException('Unable to increase scrollbar') 214 | 215 | def oneup(self, window_name, object_name, iterations): 216 | """ 217 | Press scrollbar up with number of iterations 218 | 219 | @param window_name: Window name to type in, either full name, 220 | LDTP's name convention, or a Unix glob. 221 | @type window_name: string 222 | @param object_name: Object name to type in, either full name, 223 | LDTP's name convention, or a Unix glob. 224 | @type object_name: string 225 | @param interations: iterations to perform on slider increase 226 | @type iterations: integer 227 | 228 | @return: 1 on success. 229 | @rtype: integer 230 | """ 231 | if not self.verifyscrollbarvertical(window_name, object_name): 232 | raise LdtpServerException('Object not vertical scrollbar') 233 | object_handle = self._get_object_handle(window_name, object_name) 234 | i = 0 235 | minValue = 1.0 / 8 236 | flag = False 237 | while i < iterations: 238 | if object_handle.AXValue <= 0: 239 | raise LdtpServerException('Minimum limit reached') 240 | object_handle.AXValue -= minValue 241 | time.sleep(1.0 / 100) 242 | flag = True 243 | i += 1 244 | if flag: 245 | return 1 246 | else: 247 | raise LdtpServerException('Unable to decrease scrollbar') 248 | 249 | def oneright(self, window_name, object_name, iterations): 250 | """ 251 | Press scrollbar right with number of iterations 252 | 253 | @param window_name: Window name to type in, either full name, 254 | LDTP's name convention, or a Unix glob. 255 | @type window_name: string 256 | @param object_name: Object name to type in, either full name, 257 | LDTP's name convention, or a Unix glob. 258 | @type object_name: string 259 | @param interations: iterations to perform on slider increase 260 | @type iterations: integer 261 | 262 | @return: 1 on success. 263 | @rtype: integer 264 | """ 265 | if not self.verifyscrollbarhorizontal(window_name, object_name): 266 | raise LdtpServerException('Object not horizontal scrollbar') 267 | object_handle = self._get_object_handle(window_name, object_name) 268 | i = 0 269 | maxValue = 1.0 / 8 270 | flag = False 271 | while i < iterations: 272 | if object_handle.AXValue >= 1: 273 | raise LdtpServerException('Maximum limit reached') 274 | object_handle.AXValue += maxValue 275 | time.sleep(1.0 / 100) 276 | flag = True 277 | i += 1 278 | if flag: 279 | return 1 280 | else: 281 | raise LdtpServerException('Unable to increase scrollbar') 282 | 283 | def oneleft(self, window_name, object_name, iterations): 284 | """ 285 | Press scrollbar left with number of iterations 286 | 287 | @param window_name: Window name to type in, either full name, 288 | LDTP's name convention, or a Unix glob. 289 | @type window_name: string 290 | @param object_name: Object name to type in, either full name, 291 | LDTP's name convention, or a Unix glob. 292 | @type object_name: string 293 | @param interations: iterations to perform on slider increase 294 | @type iterations: integer 295 | 296 | @return: 1 on success. 297 | @rtype: integer 298 | """ 299 | if not self.verifyscrollbarhorizontal(window_name, object_name): 300 | raise LdtpServerException('Object not horizontal scrollbar') 301 | object_handle = self._get_object_handle(window_name, object_name) 302 | i = 0 303 | minValue = 1.0 / 8 304 | flag = False 305 | while i < iterations: 306 | if object_handle.AXValue <= 0: 307 | raise LdtpServerException('Minimum limit reached') 308 | object_handle.AXValue -= minValue 309 | time.sleep(1.0 / 100) 310 | flag = True 311 | i += 1 312 | if flag: 313 | return 1 314 | else: 315 | raise LdtpServerException('Unable to decrease scrollbar') 316 | -------------------------------------------------------------------------------- /atomac/ooldtp/client_exception.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012 VMware, Inc. All Rights Reserved. 2 | 3 | # This file is part of ATOMac. 4 | 5 | #@author: Eitan Isaacson 6 | #@author: Nagappan Alagappan 7 | #@copyright: Copyright (c) 2009 Eitan Isaacson 8 | #@copyright: Copyright (c) 2009-12 Nagappan Alagappan 9 | 10 | #http://ldtp.freedesktop.org 11 | 12 | # ATOMac is free software; you can redistribute it and/or modify 13 | # it under the terms of the GNU General Public License as published by the Free 14 | # Software Foundation version 2 and no later version. 15 | 16 | # ATOMac is distributed in the hope that it will be useful, but 17 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 18 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License version 2 19 | # for more details. 20 | 21 | # You should have received a copy of the GNU General Public License along with 22 | # this program; if not, write to the Free Software Foundation, Inc., 51 Franklin 23 | # St, Fifth Floor, Boston, MA 02110-1301 USA. 24 | """Python LDTP exception""" 25 | 26 | ERROR_CODE = 123 27 | 28 | class LdtpExecutionError(Exception): 29 | pass 30 | -------------------------------------------------------------------------------- /atomac/ooldtp/log.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Nagappan Alagappan All Rights Reserved. 2 | 3 | # This file is part of ATOMac. 4 | 5 | #@author: Eitan Isaacson 6 | #@author: Nagappan Alagappan 7 | #@copyright: Copyright (c) 2009 Eitan Isaacson 8 | #@copyright: Copyright (c) 2009-13 Nagappan Alagappan 9 | 10 | #http://ldtp.freedesktop.org 11 | 12 | # ATOMac is free software; you can redistribute it and/or modify 13 | # it under the terms of the GNU General Public License as published by the Free 14 | # Software Foundation version 2 and no later version. 15 | 16 | # ATOMac is distributed in the hope that it will be useful, but 17 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 18 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License version 2 19 | # for more details. 20 | 21 | # You should have received a copy of the GNU General Public License along with 22 | # this program; if not, write to the Free Software Foundation, Inc., 51 Franklin 23 | # St, Fifth Floor, Boston, MA 02110-1301 USA. 24 | """Log routines for LDTP""" 25 | 26 | from os import environ as env 27 | import logging 28 | 29 | AREA = 'ldtp.client' 30 | ENV_LOG_LEVEL = 'LDTP_LOG_LEVEL' 31 | ENV_LOG_OUT = 'LDTP_LOG_OUT' 32 | 33 | log_level = getattr(logging, env.get(ENV_LOG_LEVEL, 'NOTSET'), logging.NOTSET) 34 | 35 | logger = logging.getLogger(AREA) 36 | 37 | if ENV_LOG_OUT not in env: 38 | handler = logging.StreamHandler() 39 | handler.setFormatter( 40 | logging.Formatter('%(name)-11s %(levelname)-8s %(message)s')) 41 | else: 42 | handler = logging.FileHandler(env[ENV_LOG_OUT]) 43 | handler.setFormatter( 44 | logging.Formatter('%(asctime)s %(levelname)-8s %(message)s')) 45 | 46 | logger.addHandler(handler) 47 | 48 | logger.setLevel(log_level) 49 | -------------------------------------------------------------------------------- /atomac/ooldtp/state.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Nagappan Alagappan All Rights Reserved. 2 | 3 | # This file is part of ATOMac. 4 | 5 | #@author: Nagappan Alagappan 6 | #@copyright: Copyright (c) 2009-13 Nagappan Alagappan 7 | 8 | #http://ldtp.freedesktop.org 9 | 10 | # ATOMac is free software; you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by the Free 12 | # Software Foundation version 2 and no later version. 13 | 14 | # ATOMac is distributed in the hope that it will be useful, but 15 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 16 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License version 2 17 | # for more details. 18 | 19 | # You should have received a copy of the GNU General Public License along with 20 | # this program; if not, write to the Free Software Foundation, Inc., 51 Franklin 21 | # St, Fifth Floor, Boston, MA 02110-1301 USA. 22 | """Python routines for LDTP""" 23 | 24 | ICONIFIED = "iconified" 25 | INVALID = "invalid" 26 | PRESSED = "pressed" 27 | EXPANDABLE = "expandable" 28 | VISIBLE = "visible" 29 | LAST_DEFINED = "last_defined" 30 | BUSY = "busy" 31 | EXPANDED = "expanded" 32 | MANAGES_DESCENDANTS = "manages_descendants" 33 | IS_DEFAULT = "is_default" 34 | INDETERMINATE = "indeterminate" 35 | REQUIRED = "required" 36 | TRANSIENT = "transient" 37 | CHECKED = "checked" 38 | SENSITIVE = "sensitive" 39 | COLLAPSED = "collapsed" 40 | STALE = "stale" 41 | OPAQUE = "opaque" 42 | ENABLED = "enabled" 43 | HAS_TOOLTIP = "has_tooltip" 44 | SUPPORTS_AUTOCOMPLETION = "supports_autocompletion" 45 | FOCUSABLE = "focusable" 46 | SELECTABLE = "selectable" 47 | ACTIVE = "active" 48 | HORIZONTAL = "horizontal" 49 | VISITED = "visited" 50 | INVALID_ENTRY = "invalid_entry" 51 | FOCUSED = "focused" 52 | MODAL = "modal" 53 | VERTICAL = "vertical" 54 | SELECTED = "selected" 55 | SHOWING = "showing" 56 | ANIMATED = "animated" 57 | EDITABLE = "editable" 58 | MULTI_LINE = "multi_line" 59 | SINGLE_LINE = "single_line" 60 | SELECTABLE_TEXT = "selectable_text" 61 | ARMED = "armed" 62 | DEFUNCT = "defunct" 63 | MULTISELECTABLE = "multiselectable" 64 | RESIZABLE = "resizable" 65 | TRUNCATED = "truncated" 66 | -------------------------------------------------------------------------------- /atomac/version.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2010 VMware, Inc. All Rights Reserved. 2 | 3 | # This file is part of ATOMac. 4 | 5 | # ATOMac is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by the Free 7 | # Software Foundation version 2 and no later version. 8 | 9 | # ATOMac is distributed in the hope that it will be useful, but 10 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License version 2 12 | # for more details. 13 | 14 | # You should have received a copy of the GNU General Public License along with 15 | # this program; if not, write to the Free Software Foundation, Inc., 51 Franklin 16 | # St, Fifth Floor, Boston, MA 02110-1301 USA. 17 | 18 | 19 | __version__ = '1.1.1' 20 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/ATOMac.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/ATOMac.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/ATOMac" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/ATOMac" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /doc/apireference.rst: -------------------------------------------------------------------------------- 1 | ATOMac API Reference 2 | ================================== 3 | 4 | .. automodule:: atomac 5 | 6 | .. autoexception:: Error 7 | 8 | .. autoexception:: ErrorAPIDisabled 9 | 10 | .. autoexception:: ErrorCannotComplete 11 | 12 | .. autoexception:: ErrorInvalidUIElement 13 | 14 | .. autoclass:: NativeUIElement 15 | :members: 16 | :undoc-members: 17 | 18 | .. autoclass:: Clipboard 19 | :members: 20 | :undoc-members: 21 | 22 | .. autoclass:: Prefs 23 | :members: 24 | :undoc-members: 25 | 26 | .. autofunction:: getAppRefByBundleId 27 | 28 | .. autofunction:: getAppRefByLocalizedName 29 | 30 | .. autofunction:: getAppRefByPid 31 | 32 | .. autofunction:: launchAppByBundleId 33 | 34 | .. autofunction:: launchAppByBundlePath 35 | 36 | .. autofunction:: terminateAppByBundleId 37 | 38 | .. autofunction:: setSystemWideTimeout 39 | 40 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # ATOMac documentation build configuration file, created by 4 | # sphinx-quickstart on Wed Jun 1 18:08:59 2011. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.insert(0, os.path.abspath('.')) 20 | # This should locate the parent directory for atomac 21 | sys.path.insert(0, os.path.abspath('..')) 22 | 23 | # Get info about the _a11y extension first 24 | import distutils.command.build 25 | from distutils.dist import Distribution 26 | 27 | b = distutils.command.build.build(Distribution()) 28 | b.initialize_options() 29 | b.finalize_options() 30 | 31 | # Add to sys.path the path to the library build directory 32 | # This will work only if the library has been built from commandline via 33 | # python setup.py build 34 | # TODO: Integrate building the _a11y module before building the docs 35 | sys.path.insert(0, os.path.join(os.path.abspath('..'), b.build_platlib)) 36 | 37 | # -- General configuration ----------------------------------------------------- 38 | 39 | # If your documentation needs a minimal Sphinx version, state it here. 40 | #needs_sphinx = '1.0' 41 | 42 | # Add any Sphinx extension module names here, as strings. They can be extensions 43 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 44 | extensions = [ 45 | 'sphinx.ext.autodoc', 46 | 'sphinx.ext.doctest', 47 | 'sphinx.ext.viewcode', 48 | ] 49 | 50 | # Add any paths that contain templates here, relative to this directory. 51 | templates_path = ['_templates'] 52 | 53 | # The suffix of source filenames. 54 | source_suffix = '.rst' 55 | 56 | # The encoding of source files. 57 | #source_encoding = 'utf-8-sig' 58 | 59 | # The master toctree document. 60 | master_doc = 'index' 61 | 62 | # General information about the project. 63 | project = u'ATOMac' 64 | copyright = u'2012, Jesse Mendonca, Ken Song, James Tatum, Andrew Wu' 65 | 66 | # The version info for the project you're documenting, acts as replacement for 67 | # |version| and |release|, also used in various other places throughout the 68 | # built documents. 69 | # 70 | # The short X.Y version. 71 | # Set the __version__ variable 72 | execfile('../atomac/version.py') 73 | version = __version__ 74 | # The full version, including alpha/beta/rc tags. 75 | release = __version__ 76 | 77 | # The language for content autogenerated by Sphinx. Refer to documentation 78 | # for a list of supported languages. 79 | #language = None 80 | 81 | # There are two options for replacing |today|: either, you set today to some 82 | # non-false value, then it is used: 83 | #today = '' 84 | # Else, today_fmt is used as the format for a strftime call. 85 | #today_fmt = '%B %d, %Y' 86 | 87 | # List of patterns, relative to source directory, that match files and 88 | # directories to ignore when looking for source files. 89 | exclude_patterns = ['_build'] 90 | 91 | # The reST default role (used for this markup: `text`) to use for all documents. 92 | #default_role = None 93 | 94 | # If true, '()' will be appended to :func: etc. cross-reference text. 95 | #add_function_parentheses = True 96 | 97 | # If true, the current module name will be prepended to all description 98 | # unit titles (such as .. function::). 99 | add_module_names = False 100 | 101 | # If true, sectionauthor and moduleauthor directives will be shown in the 102 | # output. They are ignored by default. 103 | #show_authors = False 104 | 105 | # The name of the Pygments (syntax highlighting) style to use. 106 | pygments_style = 'sphinx' 107 | 108 | # A list of ignored prefixes for module index sorting. 109 | #modindex_common_prefix = [] 110 | 111 | 112 | # -- Options for HTML output --------------------------------------------------- 113 | 114 | # The theme to use for HTML and HTML Help pages. See the documentation for 115 | # a list of builtin themes. 116 | html_theme = 'default' 117 | 118 | # Theme options are theme-specific and customize the look and feel of a theme 119 | # further. For a list of options available for each theme, see the 120 | # documentation. 121 | #html_theme_options = {} 122 | 123 | # Add any paths that contain custom themes here, relative to this directory. 124 | #html_theme_path = [] 125 | 126 | # The name for this set of Sphinx documents. If None, it defaults to 127 | # " v documentation". 128 | #html_title = None 129 | 130 | # A shorter title for the navigation bar. Default is the same as html_title. 131 | #html_short_title = None 132 | 133 | # The name of an image file (relative to this directory) to place at the top 134 | # of the sidebar. 135 | #html_logo = None 136 | 137 | # The name of an image file (within the static path) to use as favicon of the 138 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 139 | # pixels large. 140 | #html_favicon = None 141 | 142 | # Add any paths that contain custom static files (such as style sheets) here, 143 | # relative to this directory. They are copied after the builtin static files, 144 | # so a file named "default.css" will overwrite the builtin "default.css". 145 | #html_static_path = ['_static'] 146 | 147 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 148 | # using the given strftime format. 149 | #html_last_updated_fmt = '%b %d, %Y' 150 | 151 | # If true, SmartyPants will be used to convert quotes and dashes to 152 | # typographically correct entities. 153 | #html_use_smartypants = True 154 | 155 | # Custom sidebar templates, maps document names to template names. 156 | #html_sidebars = {} 157 | 158 | # Additional templates that should be rendered to pages, maps page names to 159 | # template names. 160 | #html_additional_pages = {} 161 | 162 | # If false, no module index is generated. 163 | #html_domain_indices = True 164 | 165 | # If false, no index is generated. 166 | #html_use_index = True 167 | 168 | # If true, the index is split into individual pages for each letter. 169 | #html_split_index = False 170 | 171 | # If true, links to the reST sources are added to the pages. 172 | #html_show_sourcelink = True 173 | 174 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 175 | #html_show_sphinx = True 176 | 177 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 178 | #html_show_copyright = True 179 | 180 | # If true, an OpenSearch description file will be output, and all pages will 181 | # contain a tag referring to it. The value of this option must be the 182 | # base URL from which the finished HTML is served. 183 | #html_use_opensearch = '' 184 | 185 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 186 | #html_file_suffix = None 187 | 188 | # Output file base name for HTML help builder. 189 | htmlhelp_basename = 'ATOMacdoc' 190 | 191 | 192 | # -- Options for LaTeX output -------------------------------------------------- 193 | 194 | # The paper size ('letter' or 'a4'). 195 | #latex_paper_size = 'letter' 196 | 197 | # The font size ('10pt', '11pt' or '12pt'). 198 | #latex_font_size = '10pt' 199 | 200 | # Grouping the document tree into LaTeX files. List of tuples 201 | # (source start file, target name, title, author, documentclass [howto/manual]). 202 | latex_documents = [ 203 | ('index', 'ATOMac.tex', u'ATOMac Documentation', 204 | u'Jesse Mendonca, Ken Song, James Tatum, Andrew Wu', 'manual'), 205 | ] 206 | 207 | # The name of an image file (relative to this directory) to place at the top of 208 | # the title page. 209 | #latex_logo = None 210 | 211 | # For "manual" documents, if this is true, then toplevel headings are parts, 212 | # not chapters. 213 | #latex_use_parts = False 214 | 215 | # If true, show page references after internal links. 216 | #latex_show_pagerefs = False 217 | 218 | # If true, show URL addresses after external links. 219 | #latex_show_urls = False 220 | 221 | # Additional stuff for the LaTeX preamble. 222 | #latex_preamble = '' 223 | 224 | # Documents to append as an appendix to all manuals. 225 | #latex_appendices = [] 226 | 227 | # If false, no module index is generated. 228 | #latex_domain_indices = True 229 | 230 | 231 | # -- Options for manual page output -------------------------------------------- 232 | 233 | # One entry per manual page. List of tuples 234 | # (source start file, name, description, authors, manual section). 235 | man_pages = [ 236 | ('index', 'atomac', u'ATOMac Documentation', 237 | [u'Jesse Mendonca, Ken Song, James Tatum, Andrew Wu'], 1) 238 | ] 239 | -------------------------------------------------------------------------------- /doc/gettingstarted.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | Getting Started 3 | =============== 4 | 5 | ------------------ 6 | Basic Requirements 7 | ------------------ 8 | 9 | * *Host* 10 | 11 | ATOMac requires an Intel-based system running OS X and Xcode installed. 12 | 13 | Tested OS versions: 14 | 15 | * 10.6 16 | * 10.7 17 | 18 | It may work on 10.5. 19 | If you experience issues with ATOMac on a particular version of OS X, 20 | please open a ticket in the issue tracker. 21 | 22 | * *System Preferences* 23 | 24 | System-wide accessibility must be enabled. 25 | 26 | To enable access for assistive devices, 27 | check the checkbox: 28 | 29 | | *System Preferences >* 30 | | *Universal Access >* 31 | | *Enable access for assistive devices.* 32 | 33 | Failure to enable this will result in ErrorAPIDisabled exceptions or invalid references. 34 | 35 | To facilitate programmatic control via keyboard shortcuts, 36 | select the radio button for *All Controls* under 37 | 38 | | *System Preferences >* 39 | | *Keyboard >* 40 | | *Keyboard Shortcuts >* 41 | | *Full Keyboard Access* 42 | 43 | * *Installation* 44 | 45 | Installation should be as simple as running the following command line, 46 | which will download, build and install ATOMac:: 47 | 48 | $ sudo easy_install atomac 49 | 50 | Alternatively, you can use `pip `_ 51 | for easier module management. 52 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. ATOMac documentation master file, created by 2 | sphinx-quickstart on Wed Jun 1 18:08:59 2011. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to ATOMac's documentation! 7 | ================================== 8 | 9 | ===================================== 10 | ATOMac - Automated Testing on the Mac 11 | ===================================== 12 | 13 | Welcome to ATOMac: 14 | the first Python library to enable programmatic control 15 | of GUI applications via the Mac OS X accessibility API. 16 | It is fast and easy to use to write tests 17 | or otherwise control your GUI application. 18 | 19 | .. Contents: 20 | 21 | .. toctree:: 22 | :maxdepth: 2 23 | 24 | gettingstarted 25 | usage 26 | apireference 27 | 28 | Indices and tables 29 | ================== 30 | 31 | * :ref:`genindex` 32 | * :ref:`modindex` 33 | * :ref:`search` 34 | 35 | -------------------------------------------------------------------------------- /doc/usage.rst: -------------------------------------------------------------------------------- 1 | Usage 2 | ===== 3 | 4 | 5 | Working with Applications 6 | ------------------------- 7 | 8 | Once the atomac module is installed, 9 | we should be able to use it to launch an application:: 10 | 11 | >>> import atomac 12 | >>> atomac.launchAppByBundleId('com.apple.Automator') 13 | 14 | This should launch Automator. 15 | Next, we can get a reference to the UI element 16 | for the application itself:: 17 | 18 | >>> automator = atomac.getAppRefByBundleId('com.apple.Automator') 19 | >>> automator 20 | 21 | 22 | Other means of obtaining references to a running application include:: 23 | 24 | >>> automator = atomac.getAppRefByLocalizedName('Automator') 25 | >>> automator = atomac.getAppRefByPid(12345) 26 | 27 | Finally, to terminate a running application, 28 | we can use the method ``terminateAppByBundleId``:: 29 | 30 | >>> atomac.terminateAppByBundleId('com.apple.Automator') 31 | 32 | 33 | Finding Objects in the Hierarchy 34 | -------------------------------- 35 | 36 | Assuming we have an atomac reference 37 | to the running Automator application, 38 | we can find objects in the accessibility hierarchy:: 39 | 40 | >>> window = automator.windows()[0] 41 | >>> window.AXTitle 42 | u'Untitled' 43 | >>> sheet = window.sheets()[0] 44 | 45 | Note that we retrieved an accessibility attribute 46 | from the Window object - AXTitle. 47 | ATOMac supports reading and writing of most attributes. 48 | Using Xcode's included *Accessibility Inspector* utility can provide 49 | a quick way to find these attributes. 50 | 51 | There is a shortcut for getting the sheet object 52 | which bypasses accessing it through the Window object. 53 | ATOMac can search all objects in the hierarchy 54 | by appending 'R' to the end of the method call:: 55 | 56 | >>> sheet = automator.sheetsR()[0] 57 | 58 | There are search methods for most types of accessibility objects. 59 | Each search method, such as ``windows``, 60 | has a corresponding recursive search function, such as ``windowsR``. 61 | The recursive search finds items 62 | that aren't just direct children of the associated object, 63 | but children of its children. 64 | These search methods can be given terms to identify specific elements. 65 | Note that "*" and "?" can be used as wildcard match characters 66 | in all ATOMac search methods:: 67 | 68 | >>> close = sheet.buttons('Close*')[0] 69 | 70 | ATOMac has a method to search for UI elements 71 | that match any number of criteria. 72 | The criteria are accessibility attributes 73 | (as outlined in Accessibility Inspector):: 74 | 75 | >>> close = sheet.findFirst(AXRole='AXButton', AXTitle='Close') 76 | 77 | ``FindFirst`` and ``FindFirstR`` return 78 | the first item found to match the criteria or ``None``. 79 | ``FindAll`` and ``FindAllR`` return a list of all items 80 | that match the criteria or an empty list. 81 | 82 | 83 | Attributes and Actions 84 | ---------------------- 85 | 86 | Objects are fairly versatile. 87 | You can get a list of supported attributes and actions on an object:: 88 | 89 | >>> close.getAttributes() 90 | [u'AXRole', u'AXRoleDescription', u'AXHelp', u'AXEnabled', u'AXFocused', 91 | u'AXParent', u'AXWindow', u'AXTopLevelUIElement', u'AXPosition', u'AXSize', 92 | u'AXTitle'] 93 | >>> close.AXTitle 94 | u'Close' 95 | >>> close.getActions() 96 | [u'Press'] 97 | 98 | Performing an action is as natural as:: 99 | 100 | >>> close.Press() 101 | 102 | Note that in Accessibility Inspector the action name begins with "AX-". 103 | To distinguish actions more easily from attributes, 104 | atomac drops the initial "AX" for names of actions. 105 | As a result, "AXPress" in Accessibility Inspector simply becomes 106 | "Press" in atomac and can be invoked on the object as ``Press()``. 107 | Any appropriate action can be triggered this way. 108 | 109 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2010 VMware, Inc. All Rights Reserved. 2 | 3 | # This file is part of ATOMac. 4 | 5 | # ATOMac is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by the Free 7 | # Software Foundation version 2 and no later version. 8 | 9 | # ATOMac is distributed in the hope that it will be useful, but 10 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License version 2 12 | # for more details. 13 | 14 | # You should have received a copy of the GNU General Public License along with 15 | # this program; if not, write to the Free Software Foundation, Inc., 51 Franklin 16 | # St, Fifth Floor, Boston, MA 02110-1301 USA. 17 | 18 | from setuptools import setup, Extension 19 | import os 20 | execfile('atomac/version.py') # set __version__ variable 21 | 22 | def read(fname): 23 | '''Returns the contents of the specified file located in the same dir as 24 | the script 25 | ''' 26 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 27 | 28 | source_files = [ 29 | 'atomac/_a11y/a11ymodule.c', 30 | 'atomac/_a11y/conversion.c', 31 | 'atomac/_a11y/axlib.c', 32 | ] 33 | 34 | _a11y = Extension( 35 | 'atomac._a11y', 36 | sources = source_files, 37 | extra_link_args = [ 38 | '-framework', 'ApplicationServices', 39 | '-framework', 'Carbon', 40 | ], 41 | ) 42 | 43 | setup ( 44 | name = 'atomac', 45 | version = __version__, 46 | author = 'The ATOMac Team', 47 | author_email = 'pyatom-dev@lists.pyatom.com', 48 | url = 'http://pyatom.com', 49 | description = ("Automated Testing on Mac - test GUI applications " 50 | "written in Cocoa by using Apple's Accessibility API"), 51 | license = 'GPLv2', 52 | long_description = read('README.rst'), 53 | ext_modules = [_a11y], 54 | packages = ['atomac', 'atomac.ldtpd', 'atomac.ldtp', 'atomac.ooldtp'], 55 | classifiers = [ 56 | 'Development Status :: 5 - Production/Stable', 57 | 'Environment :: MacOS X :: Cocoa', 58 | 'License :: OSI Approved :: GNU General Public License (GPL)', 59 | 'Topic :: Software Development :: Quality Assurance', 60 | 'Topic :: Software Development :: Testing', 61 | ], 62 | entry_points={ 63 | 'console_scripts' : ['ldtp = atomac.ldtpd:main'], 64 | }, 65 | ) 66 | -------------------------------------------------------------------------------- /upload.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # If you did a sudo python setup.py install, you may need to remove some 4 | # directories first - dist, build, and atomac.egg-info 5 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 6 | sudo rm -rf $DIR/dist 7 | sudo rm -rf $DIR/build 8 | sudo rm -rf $DIR/atomac.egg-info 9 | 10 | python setup.py register 11 | python setup.py sdist upload --sign 12 | python setup.py bdist_egg upload --sign 13 | 14 | --------------------------------------------------------------------------------