├── .developer └── dev.sh ├── .github └── workflows │ └── release.yml ├── .gitignore ├── ABOUT.rst ├── AUTHORS ├── CHANGELOG ├── DESIGN.md ├── DEVELOP.md ├── LICENSE ├── README.md ├── e2e_benchmarks ├── README.md ├── __init__.py ├── app │ └── facebookwdae2e │ │ ├── facebookwdae2e.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ ├── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ │ └── xcuserdata │ │ │ │ └── youngfreefjs.xcuserdatad │ │ │ │ └── UserInterfaceState.xcuserstate │ │ └── xcuserdata │ │ │ └── youngfreefjs.xcuserdatad │ │ │ └── xcschemes │ │ │ └── xcschememanagement.plist │ │ ├── facebookwdae2e │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── Contents.json │ │ │ │ └── e2e (2).png │ │ │ ├── Contents.json │ │ │ └── applogo.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── applogo 1.png │ │ │ │ ├── applogo 2.png │ │ │ │ └── applogo.png │ │ ├── ContentView.swift │ │ ├── DragView.swift │ │ ├── ListView.swift │ │ ├── Preview Content │ │ │ └── Preview Assets.xcassets │ │ │ │ └── Contents.json │ │ └── facebookwdae2eApp.swift │ │ ├── facebookwdae2eTests │ │ └── facebookwdae2eTests.swift │ │ └── facebookwdae2eUITests │ │ ├── facebookwdae2eUITests.swift │ │ └── facebookwdae2eUITestsLaunchTests.swift ├── constant.py ├── reports │ ├── WDA_V611.md │ └── WDA_V711.md ├── requirement.txt ├── test_alert_commands.py ├── test_custom_commands.py ├── test_debug_commands.py ├── test_element_commands.py ├── test_find_element_commands.py ├── test_orientation_commands.py ├── test_screenshot_commands.py └── test_session_commands.py ├── examples ├── com.netease.cloudmusic-pytest │ ├── README.txt │ ├── requirements.txt │ └── test_discover_music.py └── full_example.py ├── images └── ios-display.png ├── requirements.txt ├── runtest.sh ├── setup.cfg ├── setup.py ├── tests ├── AlertTest.zip ├── conftest.py ├── requirements.txt ├── test_callback.py ├── test_client.py ├── test_common.py ├── test_element.py ├── test_session.py └── test_xpath.py └── wda ├── __init__.py ├── _proto.py ├── exceptions.py ├── usbmux ├── __init__.py ├── exceptions.py └── pyusbmux.py ├── utils.py └── xcui_element_types.py /.developer/dev.sh: -------------------------------------------------------------------------------- 1 | ### Initialize Developer Environment 2 | 3 | # Set up the environment 4 | python3 -m venv ./venv 5 | 6 | # Use venv 7 | source ./venv/bin/activate 8 | 9 | # Download Python packages 10 | pip install -r requirements.txt 11 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Set up Python 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: 3.8 19 | 20 | - name: Install pypa/build and Build targz and wheel 21 | run: | 22 | python3 -m pip install wheel 23 | python3 setup.py sdist bdist_wheel 24 | 25 | - name: Publish distribution 📦 to PyPI 26 | uses: pypa/gh-action-pypi-publish@release/v1 27 | with: 28 | skip-existing: true 29 | password: ${{ secrets.PYPI_API_TOKEN }} 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | *.ipynb 91 | .vscode 92 | .DS_Store 93 | xcuserdata/ 94 | 95 | # IDE 96 | .idea/ 97 | -------------------------------------------------------------------------------- /ABOUT.rst: -------------------------------------------------------------------------------- 1 | Documentation in 2 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Antoine Reversat 2 | Antoine Reversat 3 | Aris Senosoft 4 | Davis Hoo 5 | DoubleH 6 | JUNXIAN DIAO 7 | Jerry Zhao 8 | Lukasz Zeglinski 9 | Zeglinski, Lukasz 10 | codeskyblue 11 | lpe234 12 | shengxiang 13 | wwjackli 14 | youngfreefjs <471011042@qq.com> 15 | yuzy 16 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | CHANGES 2 | ======= 3 | 4 | 1.4.7 5 | ----- 6 | 7 | * Update release.yml 8 | * update github release (#146) 9 | * fix: fix PR comment 10 | * fix: fix PR comment 11 | * fix: remove ds store file 12 | * doc: add report link to readme 13 | * feat: support tap api older version and add 611version test report 14 | * feat: add some unsupport func 15 | * feat: add some unsupport func 16 | * doc: add v771 e2e test report 17 | * doc: add v771 e2e test report 18 | * feat: add all e2e benchmarks 19 | * feat: add find element e2e testing 20 | * feat: add find element e2e testing 21 | * feat: add benchmark e2e testing (element and before etc.) 22 | 23 | 1.4.6 24 | ----- 25 | 26 | * add github workflow 27 | 28 | 1.4.5 29 | ----- 30 | 31 | * set trust\_env to False 32 | 33 | 1.4.4 34 | ----- 35 | 36 | * set no\_proxy since some pc socket.gethostbyname is very slow 37 | * callback example fix 38 | 39 | 1.4.3 40 | ----- 41 | 42 | * add cahced\_property to alibaba 43 | 44 | 1.4.2 45 | ----- 46 | 47 | * hotfix for screenshot().save(s.jpg) error 48 | 49 | 1.4.1 50 | ----- 51 | 52 | * fix #102 53 | 54 | 1.4.0 55 | ----- 56 | 57 | * add new api for press\_duration 58 | * use self write AttrDict class to replace attrdict lib 59 | * update readme 60 | 61 | 1.3.4 62 | ----- 63 | 64 | * remove noisy log 65 | 66 | 1.3.3 67 | ----- 68 | 69 | * support tidevice 70 | 71 | 1.3.2 72 | ----- 73 | 74 | * limit debug log length 75 | 76 | 1.3.1 77 | ----- 78 | 79 | * add more debug info 80 | 81 | 1.3.0 82 | ----- 83 | 84 | * change wait() default return None when element not found, previous raise Error 85 | 86 | 1.2.7 87 | ----- 88 | 89 | * fix again 90 | 91 | 1.2.6 92 | ----- 93 | 94 | * fix loop call 95 | 96 | 1.2.5 97 | ----- 98 | 99 | * wait wda ready before tins xctest 100 | 101 | 1.2.4 102 | ----- 103 | 104 | * update get scale logic 105 | 106 | 1.2.3 107 | ----- 108 | 109 | * add try\_first in register\_callback, optimize usage of USBClient 110 | * python2 is no logger support, remove from traivs.yml 111 | 112 | 1.2.2 113 | ----- 114 | 115 | * add python 3.7 and 3.8 to travis.yml 116 | * retry for stale element error 117 | * add d.alert.click\_exists 118 | 119 | 1.2.1 120 | ----- 121 | 122 | * Add en button name for alert monitor 123 | * add timeout in http request 124 | 125 | 1.2.0 126 | ----- 127 | 128 | * fix first call windows\_size has to start com.apple.Preferences bug, add wda\_bundle\_id argument 129 | 130 | 1.1.10 131 | ------ 132 | 133 | * fix nameMatches, update tests 134 | * fix info get error 135 | * fix window size request twice error 136 | 137 | 1.1.9 138 | ----- 139 | 140 | * merge WDAUnknownError 141 | * support handle possibly crashed error 142 | 143 | 1.1.8 144 | ----- 145 | 146 | * add retry to app\_current 147 | 148 | 1.1.7 149 | ----- 150 | 151 | * add depth to 4 152 | * support tins when wda is not ready 153 | 154 | 1.1.6 155 | ----- 156 | 157 | * add \_fast\_swipe 158 | * add 无线局域网与蜂窝网络 to watch\_and\_click 159 | 160 | 1.1.5 161 | ----- 162 | 163 | * hotfix window\_size return (0,0) error 164 | 165 | 1.1.4 166 | ----- 167 | 168 | * fix wait\_device and fix-session-id both occur error 169 | * add open watch button 170 | 171 | 1.1.3 172 | ----- 173 | 174 | * change http timeout to 180s and wait device to 180s 175 | * add watch\_and\_click usage 176 | * hotfix for inside platform 177 | 178 | 1.1.2 179 | ----- 180 | 181 | * fix spell 182 | * fix check isatty bug 183 | 184 | 1.1.1 185 | ----- 186 | 187 | * only tmq and not local device use /mds/touchAndHold 188 | 189 | 1.1.0 190 | ----- 191 | 192 | * add d.alert.watch\_and\_click support 193 | * add watch and click support 194 | 195 | 1.0.12 196 | ------ 197 | 198 | * change touch to mds touch for test 199 | 200 | 1.0.11 201 | ------ 202 | 203 | * wait for wda recover when 502 bad gateway 204 | 205 | 1.0.10 206 | ------ 207 | 208 | * add Selector.click 209 | 210 | 1.0.9 211 | ----- 212 | 213 | * change wait\_debug timeout from 30s to 120s, add element.info 214 | 215 | 1.0.8 216 | ----- 217 | 218 | * hot fix, i do not know why 219 | 220 | 1.0.7 221 | ----- 222 | 223 | * change tmq check time 224 | 225 | 1.0.6 226 | ----- 227 | 228 | * add appium\_settings, fix invalid\_session\_id not handle old wda 229 | * slow find elements 230 | 231 | 1.0.5 232 | ----- 233 | 234 | * fix missing delete bug 235 | 236 | 1.0.4 237 | ----- 238 | 239 | * fix selector getattr recursive error 240 | 241 | 1.0.3 242 | ----- 243 | 244 | * show error message 245 | 246 | 1.0.2 247 | ----- 248 | 249 | * add alert check before send\_keys, for tmq platform 250 | 251 | 1.0.1 252 | ----- 253 | 254 | * fix element operation error 255 | 256 | 1.0.0 257 | ----- 258 | 259 | * finish register\_callback 260 | * update doc 261 | * add callback 262 | 263 | 0.9.7 264 | ----- 265 | 266 | * update session-id when session-id matched application crashed. for old wda 267 | 268 | 0.9.6 269 | ----- 270 | 271 | * simplefy error\_callback logic, fix infinite callback error 272 | * remove useless api 273 | * fix ipython complete, add more tests 274 | 275 | 0.9.5 276 | ----- 277 | 278 | * bug fix 279 | 280 | 0.9.4 281 | ----- 282 | 283 | * reduce /status request count 284 | 285 | 0.9.3 286 | ----- 287 | 288 | * change el.click logic to get position first and send tap request 289 | 290 | 0.9.2 291 | ----- 292 | 293 | * fix compability with old WDA session 294 | 295 | 0.9.1 296 | ----- 297 | 298 | * fix bug when device re-plug 299 | 300 | 0.9.0 301 | ----- 302 | 303 | * fix urljoin in python 3.7 304 | * add class USBClient, which support usb connection without iproxy 305 | 306 | 0.8.1 307 | ----- 308 | 309 | * fix since attrdict doc not readed 310 | * fix get sessionId 311 | 312 | 0.8.0 313 | ----- 314 | 315 | * add press method, support args: home, volumeUp, volumeDown 316 | 317 | 0.7.7 318 | ----- 319 | 320 | * compatible with old WDA 321 | 322 | 0.7.6 323 | ----- 324 | 325 | * merge master 326 | * fix conflict 327 | * fix c.windows\_size() when session not created 328 | * add golang wda client link 329 | * update readme 330 | 331 | 0.7.5 332 | ----- 333 | 334 | * fix alert callback missing error 335 | 336 | 0.7.4 337 | ----- 338 | 339 | * fix compatibility for old version of wda 340 | 341 | 0.7.3 342 | ----- 343 | 344 | * when error:invalid-session-id occurs, retry again 345 | 346 | 0.7.2 347 | ----- 348 | 349 | * raise error when return value contains error key 350 | 351 | 0.7.1 352 | ----- 353 | 354 | * fix session() with no argument error 355 | 356 | 0.7.0 357 | ----- 358 | 359 | * update doc 360 | * move all Session methods to Client 361 | 362 | 0.6.2 363 | ----- 364 | 365 | * rename wda\_alibaba to wda\_taobao 366 | 367 | 0.6.1 368 | ----- 369 | 370 | * add taobao property 371 | 372 | 0.6.0 373 | ----- 374 | 375 | * add alibaba property 376 | 377 | 0.5.0 378 | ----- 379 | 380 | * add home function to Session 381 | * add app\_list, even useless 382 | 383 | 0.4.2 384 | ----- 385 | 386 | * fix swipe\_up-down-left-right, close #89 387 | 388 | 0.4.1 389 | ----- 390 | 391 | * fix python2 support, close #76 392 | * update doc 393 | 394 | 0.4.0 395 | ----- 396 | 397 | * update readme 398 | * add more functions 399 | * add method s.xpath(...) 400 | * make httpdo thread safe 401 | 402 | 0.3.9 403 | ----- 404 | 405 | * fix app launch stuck, set Quiescence to False 406 | 407 | 0.3.8 408 | ----- 409 | 410 | * check url format when create Client 411 | 412 | 0.3.7 413 | ----- 414 | 415 | * fix compatible for the latest modified WDA by appium 416 | 417 | 0.3.6 418 | ----- 419 | 420 | * fix WDARequestError, format code with yapf 421 | 422 | 0.3.5 423 | ----- 424 | 425 | * add pillow depencency 426 | 427 | 0.3.4 428 | ----- 429 | 430 | * fix: #65 431 | 432 | 0.3.3 433 | ----- 434 | 435 | * fix all the tests 436 | 437 | 0.3.2 438 | ----- 439 | 440 | * compatiable with py2 441 | 442 | 0.3.1 443 | ----- 444 | 445 | * add retry for status, since status sometime get EmptyReponse 446 | 447 | 0.3.0 448 | ----- 449 | 450 | * make session launch more robust 451 | * change json.decoder.JSONDecodeError to WDARequestError 452 | 453 | 0.2.4 454 | ----- 455 | 456 | * fix session, can not launch error 457 | 458 | 0.2.3 459 | ----- 460 | 461 | * add content-type application/json when send http post 462 | * recommend using appium/WebDriverAgent 463 | * fix set\_callback check error 464 | 465 | 0.2.2 466 | ----- 467 | 468 | * fix travis 469 | * try catch response.json() 470 | * add wait\_ready function 471 | * add screenshot in session, add click(support percent), add scale, update readme 472 | * [issue #33]Add escape character for quotes 473 | 474 | 0.2.1 475 | ----- 476 | 477 | * fix in py3.6 error 478 | * fix name matches, use string escape fix it 479 | * add test example 480 | * fix nameMatches 481 | * add netease cloudmusic test example 482 | 483 | 0.2.0 484 | ----- 485 | 486 | * finish first version of sync new code 487 | * fix some tests 488 | * add nameMatches 489 | * sync to lastest wda 490 | * sync to new wda code 491 | * Allow extra capabilities to be passed to the session (#25) 492 | * [Screenshot] Open file in binary mode 493 | 494 | 0.1.2 495 | ----- 496 | 497 | * freeze webdriveragent version 498 | * change example tab to 4space 499 | * support handle alert automaticly 500 | * Sessions with arguments (#23) 501 | * change class\_name to className and text\_contains to textContains 502 | 503 | 0.1.1 504 | ----- 505 | 506 | * Lukzeg change orientation functionality (#21) 507 | * add Element change lot of code 508 | * add some beta apis 509 | * follow format 510 | * Change time\_out to timeout and add example usage in readme 511 | * add alert.wait and Selector partial text 512 | * support get current session id and bundle id 513 | 514 | 0.1.0 515 | ----- 516 | 517 | * add pinch api 518 | * update doc 519 | * update accessible,pinch,keyboardDismiss,doubleTap,deactivateApp 520 | * close #15 521 | * update api sync with WDA (#12) 522 | * add healthcheck api 523 | * fix broken swipe() API, due to wda specific endpoint complied with WD spec naming convention https://github.com/facebook/WebDriverAgent/commit/eeaa1a3bbc9559d41ff3779c73c6678c7715a3d0 524 | * fix unicode convert to unicode error 525 | * fix missing e args 526 | * support python3 527 | 528 | 0.0.3 529 | ----- 530 | 531 | * retry request when first failed 532 | * Update \_\_init\_\_.py 533 | * Update \_\_init\_\_.py 534 | * update README.md file, adding example for element swipe api 535 | * Add swipe support for wda element 536 | * add comment and fix some code structure according to the pr 537 | * expand ‘link text’ query of Selector class with ‘value’ or ‘label’ property, in case no ‘name’ property exists and only ‘value’ property valid 538 | * fix the right api 539 | * add send\_keys, but it not works actually 540 | * sync to the latest wda 541 | * status func add sessionId 542 | * just add some comment 543 | * add alert support, close #6 544 | 545 | 0.0.2 546 | ----- 547 | 548 | * raise exceptions when connect went wrong 549 | * add more about it 550 | * add more .. 551 | * change rect to bounds, add DEBUG mode 552 | * remove extra code 553 | * finish scroll part 554 | * add scroll function 555 | * limit max find element time into 90s 556 | * merge code 557 | * add bounds 558 | * Add attribute 559 | * Add getAttribut() 560 | * Update \_\_init\_\_.py 561 | * Fix find element when element's isvisible=NO 562 | * add tap\_hold api 563 | * enable swipe function 564 | * Fix xpath定位 565 | * Add xpath 定位 566 | * Update README.md 567 | * Update README.md 568 | * Update README.md 569 | * Update README.md 570 | * Update README.md 571 | * fix documentation, add some props 572 | * do some fix 573 | * update desc 574 | 575 | 0.0.1 576 | ----- 577 | 578 | * wow, update again, this is the last today 579 | * update document 580 | * add the rest api 581 | * fix json 582 | * make doc looks beautiful 583 | * add with operation 584 | * fix travis setting 585 | * update password 586 | * add travis badge 587 | * add travis 588 | * add screenshot and selector 589 | * add selector 590 | * add tap and session close 591 | * add session, and home function, wrap python requests functions 592 | * update but not implement except status 593 | * initial first release 594 | * init project 595 | * Initial commit 596 | -------------------------------------------------------------------------------- /DESIGN.md: -------------------------------------------------------------------------------- 1 | ## DESIGN 2 | Create client object 3 | 4 | ``` 5 | import wda 6 | 7 | 8 | client = wda.Client('http://10.0.0.1:8100') 9 | client.status() 10 | 11 | se = client.session('com.example.demo') 12 | se.tap(150, 150) # alias 13 | se.click(150, 150) # alias of tap 14 | se.long_tap(150, 150) # also long_click 15 | se.swipe(50, 60, 70, 80, step=10) 16 | 17 | se.screenshot(filename=None) # return PIL object 18 | se(className='Button', text='update').click() 19 | se(text='update').exists # return bool 20 | se(className='Button').count # return number 21 | se(className='EditText').set_text("hello") # input something 22 | se.close() 23 | ``` 24 | 25 | -------------------------------------------------------------------------------- /DEVELOP.md: -------------------------------------------------------------------------------- 1 | # How to run tests 2 | the test is based on py.test, and you need to install python3 3 | 4 | ```bash 5 | export DEVICE_URL="http://localhost:8100" 6 | cd tests 7 | py.test -vv 8 | ``` 9 | 10 | # 已知问题 11 | - iOS 13.5 12 | 顶部有通知栏时,alert操作不了 13 | 14 | # HTTP Request 15 | ``` 16 | $ http GET $DEVICE_URL/HEALTH 17 | I-AM-ALIVE 18 | 19 | $ curl -X POST -d '{"name": "home"}' 'http://localhost:8100/session/024A4577-2105-4E0C-9623-D683CDF9707E/wda/pressButton' 20 | Return (47ms): { 21 | "status" : 0, 22 | "sessionId" : "024A4577-2105-4E0C-9623-D683CDF9707E", 23 | "value" : null 24 | } 25 | 26 | $ curl -X POST -d '{"value": ["h", "e", "l", "l", "o"]}' 'http://localhost:8100/session/024A4577-2105-4E0C-9623-D683CDF9707E/wda/keys' 27 | { 28 | "status" : 0, 29 | "sessionId" : "024A4577-2105-4E0C-9623-D683CDF9707E", 30 | "value" : null 31 | } 32 | ``` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 shengxiang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-wda 2 | [![Build Status](https://travis-ci.org/openatx/facebook-wda.svg?branch=master)](https://travis-ci.org/openatx/facebook-wda) 3 | [![PyPI](https://img.shields.io/pypi/v/facebook-wda.svg)](https://pypi.python.org/pypi/facebook-wda) 4 | [![PyPI](https://img.shields.io/pypi/l/facebook-wda.svg)]() 5 | 6 | Facebook WebDriverAgent Python Client Library (not official) 7 | Implemented apis describe in 8 | 9 | Most functions finished. 10 | 11 | Since facebook/WebDriverAgent has been archived. Recommend use the forked WDA: https://github.com/appium/WebDriverAgent 12 | 13 | Tested with: 14 | 15 | ## Alternatives 16 | - gwda (Golang): https://github.com/ElectricBubble/gwda 17 | 18 | ## Installation 19 | 1. You need to start WebDriverAgent by yourself 20 | 21 | **New** There is a new tool, which can start WDA without xcodebuild, even you can run in Linux and Windows. 22 | See: 23 | 24 | Or 25 | 26 | Follow the instructions in 27 | 28 | It is better to start with Xcode to prevent CodeSign issues. 29 | 30 | But it is also ok to start WDA with command line. 31 | 32 | ``` 33 | xcodebuild -project WebDriverAgent.xcodeproj -scheme WebDriverAgentRunner -destination 'platform=iOS Simulator,name=iPhone 6' test 34 | ``` 35 | 36 | WDA在真机上运行需要一些配置,可以参考这篇文章 [ATX 文档 - iOS 真机如何安装 WebDriverAgent](https://testerhome.com/topics/7220) 37 | 38 | 配置完之后运行下面的命令即可(需要用到Mac的密码,以及设备的UDID) 39 | 40 | ```bash 41 | # 解锁keychain,以便可以正常的签名应用 42 | security unlock-keychain -p $your-mac-password-here ~/Library/Keychains/login.keychain 43 | 44 | # 获取设备的UDID 45 | UDID=$(idevice_id -l | head -n1) 46 | 47 | # 运行测试 48 | xcodebuild -project WebDriverAgent.xcodeproj -scheme WebDriverAgentRunner -destination "id=$UDID" test 49 | ``` 50 | 51 | 2. Install python wda client 52 | 53 | ``` 54 | pip3 install -U facebook-wda 55 | ``` 56 | 57 | ## TCP connection over USB (optional) 58 | You can use wifi network, it is very convinient, but not very stable enough. 59 | 60 | I found a tools named `iproxy` which can forward device port to localhost, it\'s source code is here 61 | 62 | The usage is very simple `iproxy [udid]` 63 | 64 | For more information see [SSH Over USB](https://iphonedevwiki.net/index.php/SSH_Over_USB) 65 | 66 | ## Something you need to know 67 | function `window_size()` return UIKit size, While `screenshot()` image size is Native Resolution 68 | 69 | [![IOS Display](images/ios-display.png)](https://developer.apple.com/library/archive/documentation/DeviceInformation/Reference/iOSDeviceCompatibility/Displays/Displays.html) 70 | 71 | when use `screenshot`, the image size is pixels size. eg(`1080 x 1920`) 72 | But this size is different with `c.window_size()` 73 | 74 | use `session.scale` to get UIKit scale factor 75 | 76 | ## Configuration 77 | ```python 78 | import wda 79 | 80 | wda.DEBUG = False # default False 81 | wda.HTTP_TIMEOUT = 60.0 # default 60.0 seconds 82 | ``` 83 | 84 | ## How to use 85 | ### Create a client 86 | 87 | ```py 88 | import wda 89 | 90 | # Enable debug will see http Request and Response 91 | # wda.DEBUG = True 92 | c = wda.Client('http://localhost:8100') 93 | 94 | # get env from $DEVICE_URL if no arguments pass to wda.Client 95 | # http://localhost:8100 is the default value if $DEVICE_URL is empty 96 | c = wda.Client() 97 | ``` 98 | 99 | A `wda.WDAError` will be raised if communite with WDA went wrong. 100 | 101 | **Experiment feature**: create through usbmuxd without `iproxy` 102 | 103 | > Added in version: 0.9.0 104 | 105 | class `USBClient` inherit from `Client` 106 | 107 | USBClient connect to wda-server through `unix:/var/run/usbmuxd` 108 | 109 | ```python 110 | import wda 111 | 112 | # 如果只有一个设备也可以简写为 113 | # If there is only one iPhone connecttd 114 | c = wda.USBClient() 115 | 116 | # 支持指定设备的udid,和WDA的端口号 117 | # Specify udid and WDA port 118 | c = wda.USBClient("539c5fffb18f2be0bf7f771d68f7c327fb68d2d9", port=8100) 119 | 120 | # 也支持通过DEVICE_URL访问 121 | c = wda.Client("usbmux://{udid}:8100".format(udid="539c5fffb18f2be0bf7f771d68f7c327fb68d2d9")) 122 | print(c.window_size()) 123 | 124 | # 注: 125 | # 仅在安装了tins的电脑上可以使用(目前并不对外开放) 126 | # 1.2.0 引入 wda_bundle_id 参数 127 | c = wda.USBClient("539c5fffb18f2be0bf7f771d68f7c327fb68d2d9", port=8100, wda_bundle_id="com.facebook.custom.xctest") 128 | ``` 129 | 130 | 看到这里,可以看 [examples](examples) 目录下的一些代码了 131 | 132 | ### Client 133 | 134 | ```py 135 | # Show status 136 | print c.status() 137 | 138 | # Wait WDA ready 139 | c.wait_ready(timeout=300) # 等待300s,默认120s 140 | c.wait_ready(timeout=300, noprint=True) # 安静的等待,无进度输出 141 | 142 | # Press home button 143 | c.home() 144 | 145 | # Hit healthcheck 146 | c.healthcheck() 147 | 148 | # Get page source 149 | c.source() # format XML 150 | c.source(accessible=True) # default false, format JSON 151 | 152 | c.locked() # true of false 153 | c.lock() # lock screen 154 | c.unlock() # unlock 155 | c.app_current() # {"pid": 1281, "bundleId": "com.netease.cloudmusic"} 156 | 157 | # OpenURL not working very well 158 | c.open_url("taobao://m.taobao.com/index.htm") 159 | ``` 160 | 161 | Take screenshot save as png 162 | 163 | ```py 164 | c.screenshot('screen.png') # Good 165 | c.screenshot("screen.jpg") # Bad 166 | 167 | # convert to PIL.Image and then save as jpg 168 | c.screenshot().save("screen.jpg") # Good 169 | 170 | c.appium_settings() # 获取appium的配置 171 | c.appium_settings({"mjpegServerFramerate": 20}) # 修改配置 172 | ``` 173 | 174 | ### Session 175 | > From version 0.7.0, All Session methods moved to Client class. now Session is alias of Client 176 | 177 | Open app 178 | 179 | ```py 180 | with c.session('com.apple.Health') as s: 181 | print(s.orientation) 182 | ``` 183 | 184 | Same as 185 | 186 | ```py 187 | s = c.session('com.apple.Health') 188 | print(s.orientation) 189 | s.close() 190 | ``` 191 | 192 | For web browser like Safari you can define page whit which will be opened: 193 | ```python 194 | s = c.session('com.apple.mobilesafari', ['-u', 'https://www.google.com/ncr']) 195 | print(s.orientation) 196 | s.close() 197 | ``` 198 | 199 | Other app operation (Works in [appium/WebDriverAgent](https://github.com/appium/WebDriverAgent)) 200 | 201 | ```bash 202 | c.app_current() # show current app info 203 | # Output example -- 204 | # {'processArguments': {'env': {}, 'args': []}, 205 | # 'name': '', 206 | # 'pid': 2978, 207 | # 'bundleId': 'com.apple.Preferences'} 208 | 209 | # Handle alert automatically in WDA (never tested before) 210 | # alert_action should be one of ["accept", "dismiss"] 211 | s = c.session("com.apple.Health", alert_action="accept") 212 | 213 | # launch without terminate app (WDAEmptyResponseError might raise) 214 | c.session().app_activate("com.apple.Health") # same as app_launch 215 | 216 | # terminate app 217 | c.session().app_terminate("com.apple.Health") 218 | 219 | # get app state 220 | c.session().app_state("com.apple.Health") 221 | # output {"value": 4, "sessionId": "xxxxxx"} 222 | # different value means 1: die, 2: background, 4: running 223 | ``` 224 | 225 | ### Session operations 226 | ```python 227 | # set default element search timeout 30 seconds 228 | s.implicitly_wait(30.0) 229 | 230 | # Current bundleId and sessionId 231 | print(s.bundle_id, s.id) 232 | 233 | s.home() # same as c.home(), use the same API 234 | 235 | s.lock() # lock screen 236 | s.unlock() # unlock screen 237 | s.locked() # locked status, true or false 238 | 239 | s.battery_info() # return like {"level": 1, "state": 2} 240 | s.device_info() # return like {"currentLocale": "zh_CN", "timeZone": "Asia/Shanghai"} 241 | 242 | s.set_clipboard("Hello world") # update clipboard 243 | # s.get_clipboard() # Not working now 244 | 245 | # Screenshot return PIL.Image 246 | # Requires pillow, installed by "pip install pillow" 247 | s.screenshot().save("s.png") 248 | 249 | # Sometimes screenshot rotation is wrong, but we can rotate it to the right direction 250 | # Refs: https://pillow.readthedocs.io/en/3.1.x/reference/Image.html#PIL.Image.Image.transpose 251 | from PIL import Image 252 | s.screenshot().transpose(Image.ROTATE_90).save("correct.png") 253 | 254 | # One of 255 | print(s.orientation) # expect PORTRAIT or LANDSCAPE 256 | 257 | # Change orientation 258 | s.orientation = wda.LANDSCAPE # there are many other directions 259 | 260 | # Deactivate App for some time 261 | s.deactivate(5.0) # 5s 262 | 263 | # Get width and height 264 | print(s.window_size()) 265 | # Expect tuple output (width, height) 266 | # For example: (414, 736) 267 | 268 | # Get UIKit scale factor, the first time will take about 1s, next time use cached value 269 | print(s.scale) 270 | # Example output: 3 271 | 272 | # Simulate touch 273 | s.tap(200, 200) 274 | 275 | # Very like tap, but support float and int argument 276 | # float indicate percent. eg 0.5 means 50% 277 | s.click(200, 200) 278 | s.click(0.5, 0.5) # click center of screen 279 | s.click(0.5, 200) # click center of x, and y(200) 280 | 281 | # Double touch 282 | s.double_tap(200, 200) 283 | 284 | # Simulate swipe, utilizing drag api 285 | s.swipe(x1, y1, x2, y2, 0.5) # 0.5s 286 | s.swipe(0.5, 0.5, 0.5, 1.0) # swipe middle to bottom 287 | 288 | s.swipe_left() 289 | s.swipe_right() 290 | s.swipe_up() 291 | s.swipe_down() 292 | 293 | # tap hold for 1 seconds 294 | s.tap_hold(x, y, 1.0) 295 | 296 | # Hide keyboard (not working in simulator), did not success using latest WDA 297 | # s.keyboard_dismiss() 298 | 299 | # press home, volumeUp, volumeDown 300 | s.press("home") # fater then s.home() 301 | s.press("volumeUp") 302 | s.press("volumeDown") 303 | 304 | # New in WebDriverAgent(3.8.0) 305 | # long press home, volumeUp, volumeDown, power, snapshot(power+home) 306 | s.press_duration("volumeUp", 1) # long press for 1 second 307 | s.press_duration("snapshot", 0.1) 308 | ``` 309 | 310 | ### Find element 311 | > Note: if element not found, `WDAElementNotFoundError` will be raised 312 | 313 | ```python 314 | # For example, expect: True or False 315 | # using id to find element and check if exists 316 | s(id="URL").exists # return True or False 317 | 318 | # using id or other query conditions 319 | s(id='URL') 320 | 321 | # using className 322 | s(className="Button") 323 | 324 | # using name 325 | s(name='URL') 326 | s(nameContains='UR') 327 | s(nameMatches=".RL") 328 | 329 | # using label 330 | s(label="label") 331 | s(labelContains="URL") 332 | 333 | # using value 334 | s(value="Enter") 335 | s(valueContains="RL") 336 | 337 | # using visible, enabled 338 | s(visible=True, enabled=True) 339 | 340 | # using index, index must combined with at least on label,value, etc... 341 | s(name='URL', index=1) # find the second element. index of founded elements, min is 0 342 | 343 | # combines search conditions 344 | # attributes bellow can combines 345 | # :"className", "name", "label", "visible", "enabled" 346 | s(className='Button', name='URL', visible=True, labelContains="Addr") 347 | ``` 348 | 349 | More powerful finding method 350 | 351 | ```python 352 | s(xpath='//Button[@name="URL"]') 353 | 354 | # another code style 355 | s.xpath('//Button[@name="URL"]') 356 | 357 | s(predicate='name LIKE "UR*"') 358 | s('name LIKE "U*L"') # predicate is the first argument, without predicate= is ok 359 | s(classChain='**/Button[`name == "URL"`]') 360 | ``` 361 | 362 | To see more `Class Chain Queries` examples, view 363 | 364 | [Predicate Format String Syntax](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Predicates/Articles/pSyntax.html) 365 | 366 | ### Get Element info 367 | ```python 368 | # if not found, raise WDAElementNotFoundError 369 | e = s(text='Dashboard').get(timeout=10.0) 370 | 371 | # e could be None if not exists 372 | e = s(text='Dashboard').wait(timeout=10.0) 373 | 374 | # get element attributes 375 | e.className # XCUIElementTypeStaticText 376 | e.name # XCUIElementTypeStaticText /name 377 | e.visible # True /attribute/visible 378 | e.value # Dashboard /attribute/value 379 | e.label # Dashboard /attribute/label 380 | e.text # Dashboard /text 381 | e.enabled # True /enabled 382 | e.displayed # True /displayed 383 | 384 | e.bounds # Rect(x=161, y=32, width=53, height=21) /rect 385 | x, y, w, h = e.bounds 386 | 387 | 388 | ``` 389 | 390 | ### Element operations (eg: `tap`, `scroll`, `set_text` etc...) 391 | Exmaple search element and tap 392 | 393 | ```python 394 | # Get first match Element object 395 | # The function get() is very important. 396 | # when elements founded in 10 seconds(:default:), Element object returns 397 | # or WDAElementNotFoundError raises 398 | e = s(text='Dashboard').get(timeout=10.0) 399 | # s(text='Dashboard') is Selector 400 | # e is Element object 401 | e.tap() # tap element 402 | ``` 403 | 404 | >Some times, I just hate to type `.get()` 405 | 406 | Using python magic tricks to do it again. 407 | 408 | ```python 409 | # using python magic function "__getattr__", it is ok with out type "get()" 410 | s(text='Dashboard').tap() 411 | # same as 412 | s(text='Dashboard').get().tap() 413 | ``` 414 | 415 | Note: Python magic tricks can not used on get attributes 416 | 417 | ```python 418 | # Accessing attrbutes, you have to use get() 419 | s(text='Dashboard').get().value 420 | 421 | # Not right 422 | # s(text='Dashboard').value # Bad, always return None 423 | ``` 424 | 425 | Click element if exists 426 | 427 | ```python 428 | s(text='Dashboard').click_exists() # return immediately if not found 429 | s(text='Dashboard').click_exists(timeout=5.0) # wait for 5s 430 | ``` 431 | 432 | Other Element operations 433 | 434 | ```python 435 | # Check if elements exists 436 | print(s(text="Dashboard").exists) 437 | 438 | # Find all matches elements, return Array of Element object 439 | s(text='Dashboard').find_elements() 440 | 441 | # Use index to find second element 442 | s(text='Dashboard')[1].exists 443 | 444 | # Use child to search sub elements 445 | s(text='Dashboard').child(className='Cell').exists 446 | 447 | # Default timeout is 10 seconds 448 | # But you can change by 449 | s.set_timeout(10.0) 450 | 451 | # do element operations 452 | e.tap() 453 | e.click() # alias of tap 454 | e.clear_text() 455 | e.set_text("Hello world") 456 | e.tap_hold(2.0) # tapAndHold for 2.0s 457 | 458 | e.scroll() # scroll to make element visiable 459 | 460 | # directions can be "up", "down", "left", "right" 461 | # swipe distance default to its height or width according to the direction 462 | e.scroll('up') 463 | 464 | # Set text 465 | e.set_text("Hello WDA") # normal usage 466 | e.set_text("Hello WDA\n") # send text with enter 467 | e.set_text("\b\b\b") # delete 3 chars 468 | 469 | # Wait element gone 470 | s(text='Dashboard').wait_gone(timeout=10.0) 471 | 472 | # Swipe 473 | s(className="Image").swipe("left") 474 | 475 | # Pinch 476 | s(className="Map").pinch(2, 1) # scale=2, speed=1 477 | s(className="Map").pinch(0.1, -1) # scale=0.1, speed=-1 (I donot very understand too) 478 | 479 | # properties (bool) 480 | e.accessible 481 | e.displayed 482 | e.enabled 483 | 484 | # properties (str) 485 | e.text # ex: Dashboard 486 | e.className # ex: XCUIElementTypeStaticText 487 | e.value # ex: github.com 488 | 489 | # Bounds return namedtuple 490 | rect = e.bounds # ex: Rect(x=144, y=28, width=88.0, height=27.0) 491 | rect.x # expect 144 492 | ``` 493 | 494 | Alert 495 | 496 | ```python 497 | print(s.alert.exists) 498 | print(s.alert.text) 499 | s.alert.accept() # Actually do click first alert button 500 | s.alert.dismiss() # Actually do click second alert button 501 | s.alert.wait(5) # if alert apper in 5 second it will return True,else return False (default 20.0) 502 | s.alert.wait() # wait alert apper in 2 second 503 | 504 | s.alert.buttons() 505 | # example return: ["设置", "好"] 506 | 507 | s.alert.click("设置") 508 | s.alert.click(["设置", "信任", "安装"]) # when Arg type is list, click the first match, raise ValueError if no match 509 | ``` 510 | 511 | Alert monitor 512 | 513 | ```python 514 | with c.alert.watch_and_click(['好', '确定']): 515 | s(label="Settings").click() # 516 | # ... other operations 517 | 518 | # default watch buttons are 519 | # ["使用App时允许", "好", "稍后", "稍后提醒", "确定", "允许", "以后"] 520 | with c.alert.watch_and_click(interval=2.0): # default check every 2.0s 521 | # ... operations 522 | ``` 523 | 524 | ### Callback 525 | 回调操作: `register_callback` 526 | 527 | ```python 528 | c = wda.Client() 529 | 530 | # 使用Example 531 | def device_offline_callback(client, err): 532 | if isinstance(err, wda.WDABadGateway): 533 | print("Handle device offline") 534 | ok = client.wait_ready(60) # 等待60s恢复 535 | if not ok: 536 | return wda.Callback.RET_ABORT 537 | return wda.Callback.RET_RETRY 538 | 539 | c.register_callback(wda.Callback.ERROR, device_offline_callback, try_first=True) 540 | # try_first 优先使用device_offline_callback函数处理ERROR 541 | 542 | 543 | # the argument name in callback function can be one of 544 | # - client: wda.Client 545 | # - url: str, eg: http://localhost:8100/session/024A4577-2105-4E0C-9623-D683CDF9707E/wda/keys 546 | # - urlpath: str, eg: /wda/keys (without session id) 547 | # - with_session: bool # if url contains session id 548 | # - method: str, eg: GET 549 | # - response: dict # Callback.HTTP_REQUEST_AFTER only 550 | # - err: WDAError # Callback.ERROR only 551 | # 552 | def _cb(client: wda.Client, url: str): 553 | if url.endswith("/wda/keys"): 554 | print("send_keys called") 555 | 556 | c.register_callback(wda.Callback.HTTP_REQUEST_BEFORE, _cb) 557 | c.register_callback(wda.Callback.HTTP_REQUEST_BEFORE, lambda url: print(url), try_first=True) # 回调会比_cb更先回调 558 | c.send_keys("Hello") 559 | 560 | # unregister 561 | c.unregister_callback(wda.Callback.HTTP_REQUEST_BEFORE, _cb) 562 | c.unregister_callback(wda.Callback.HTTP_REQUEST_BEFORE) # ungister all 563 | c.unregister_callback() # unregister all callbacks 564 | ``` 565 | 566 | 支持的回调有 567 | 568 | ``` 569 | wda.Callback.HTTP_REQUEST_BEFORE 570 | wda.Callback.HTTP_REQUEST_AFTER 571 | wda.Callback.ERROR 572 | ``` 573 | 574 | 默认代码内置了两个回调函数 `wda.Callback.ERROR`,使用`c.unregister_callback(wda.Callback.ERROR)`可以去掉这两个回调 575 | 576 | - 当遇到`invalid session id`错误时,更新session id并重试 577 | - 当遇到设备掉线时,等待`wda.DEVICE_WAIT_TIMEOUT`时间 (当前是30s,以后可能会改的更长一些) 578 | 579 | ## TODO 580 | longTap not done pinch(not found in WDA) 581 | 582 | TouchID 583 | 584 | * Match Touch ID 585 | * Do not match Touch ID 586 | 587 | ## How to handle alert message automaticly (need more tests) 588 | For example 589 | 590 | ```python 591 | import wda 592 | 593 | s = wda.Client().session() 594 | 595 | def _alert_callback(session): 596 | session.alert.accept() 597 | 598 | s.set_alert_callback(_alert_callback) # deprecated,此方法不能用了 599 | 600 | # do operations, when alert popup, it will auto accept 601 | s(type="Button").click() 602 | ``` 603 | 604 | ## Special property 605 | ```python 606 | # s: wda.Session 607 | s.alibaba.xxx # only used in alibaba-company 608 | ``` 609 | 610 | ## DEVELOP 611 | See [DEVELOP.md](DEVELOP.md) for more details. 612 | 613 | ## iOS Build-in Apps 614 | **苹果自带应用** 615 | 616 | | Name | Bundle ID | 617 | |--------|--------------------| 618 | | iMovie | com.apple.iMovie | 619 | | Apple Store | com.apple.AppStore | 620 | | Weather | com.apple.weather | 621 | | 相机Camera | com.apple.camera | 622 | | iBooks | com.apple.iBooks | 623 | | Health | com.apple.Health | 624 | | Settings | com.apple.Preferences | 625 | | Watch | com.apple.Bridge | 626 | | Maps | com.apple.Maps | 627 | | Game Center | com.apple.gamecenter | 628 | | Wallet | com.apple.Passbook | 629 | | 电话 | com.apple.mobilephone | 630 | | 备忘录 | com.apple.mobilenotes | 631 | | 指南针 | com.apple.compass | 632 | | 浏览器 | com.apple.mobilesafari | 633 | | 日历 | com.apple.mobilecal | 634 | | 信息 | com.apple.MobileSMS | 635 | | 时钟 | com.apple.mobiletimer | 636 | | 照片 | com.apple.mobileslideshow | 637 | | 提醒事项 | com.apple.reminders | 638 | | Desktop | com.apple.springboard (Start this will cause your iPhone reboot) | 639 | 640 | **第三方应用 Thirdparty** 641 | 642 | | Name | Bundle ID | 643 | |--------|--------------------| 644 | | 腾讯QQ | com.tencent.mqq | 645 | | 微信 | com.tencent.xin | 646 | | 部落冲突 | com.supercell.magic | 647 | | 钉钉 | com.laiwang.DingTalk | 648 | | Skype | com.skype.tomskype | 649 | | Chrome | com.google.chrome.ios | 650 | 651 | 652 | Another way to list apps installed on you phone is use `ideviceinstaller` 653 | install with `brew install ideviceinstaller` 654 | 655 | List apps with command 656 | 657 | ```sh 658 | $ ideviceinstaller -l 659 | ``` 660 | 661 | ## Tests 662 | 测试的用例放在`tests/`目录下,使用iphone SE作为测试机型,系统语言应用。调度框架`pytest` 663 | 664 | ## WDA Benchmark E2E Tests 665 | [E2E Tests](e2e_benchmarks) 666 | Latest WDA Version Testing Report: 667 | - [WDA Version 7.1.1](e2e_benchmarks/reports/WDA_V711.md) 668 | - [WDA Version 6.1.1](e2e_benchmarks/reports/WDA_V611.md) 669 | 670 | ## Reference 671 | Source code 672 | 673 | - [Router](https://github.com/facebook/WebDriverAgent/blob/master/WebDriverAgentLib/Commands/FBElementCommands.m#L62) 674 | - [Alert](https://github.com/facebook/WebDriverAgent/blob/master/WebDriverAgentLib/Commands/FBAlertViewCommands.m#L25) 675 | 676 | ## Thanks 677 | - https://github.com/msabramo/requests-unixsocket 678 | - https://github.com/iOSForensics/pymobiledevice 679 | 680 | ## Articles 681 | * By [diaojunxiam](https://github.com/diaojunxian) 682 | 683 | ## Contributors 684 | * [diaojunxian](https://github.com/diaojunxian) 685 | * [iquicktest](https://github.com/iquicktest) 686 | 687 | ## DESIGN 688 | [DESIGN](DESIGN.md) 689 | 690 | ## LICENSE 691 | [MIT](LICENSE) 692 | 693 | -------------------------------------------------------------------------------- /e2e_benchmarks/README.md: -------------------------------------------------------------------------------- 1 | # E2E Testing 2 | 3 | ## Overview 4 | This directory contains end-to-end tests for the operator. 5 | This will involve benchmark testing across different [WDA service versions](https://github.com/appium/WebDriverAgent/releases). 6 | 7 | ## Running the tests 8 | The tests can be run using the `pytest -v -rsx '/Users/youngfreefjs/Desktop/code/github/facebook-wda/e2e_benchmarks/'` script. 9 | This will install the WDA service versions, ensuring a swift identification of whether the client remains compatible and functional after an upgrade in the WDA service. 10 | -------------------------------------------------------------------------------- /e2e_benchmarks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openatx/facebook-wda/85d944acb2371820c9c6542b6063948def94c120/e2e_benchmarks/__init__.py -------------------------------------------------------------------------------- /e2e_benchmarks/app/facebookwdae2e/facebookwdae2e.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 88FD625B2BAADAB7003459E2 /* facebookwdae2eApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88FD625A2BAADAB7003459E2 /* facebookwdae2eApp.swift */; }; 11 | 88FD625D2BAADAB7003459E2 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88FD625C2BAADAB7003459E2 /* ContentView.swift */; }; 12 | 88FD625F2BAADAB9003459E2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 88FD625E2BAADAB9003459E2 /* Assets.xcassets */; }; 13 | 88FD62622BAADAB9003459E2 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 88FD62612BAADAB9003459E2 /* Preview Assets.xcassets */; }; 14 | 88FD626C2BAADAB9003459E2 /* facebookwdae2eTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88FD626B2BAADAB9003459E2 /* facebookwdae2eTests.swift */; }; 15 | 88FD62762BAADAB9003459E2 /* facebookwdae2eUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88FD62752BAADAB9003459E2 /* facebookwdae2eUITests.swift */; }; 16 | 88FD62782BAADAB9003459E2 /* facebookwdae2eUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88FD62772BAADAB9003459E2 /* facebookwdae2eUITestsLaunchTests.swift */; }; 17 | 88FD62852BAC3F63003459E2 /* ListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88FD62842BAC3F63003459E2 /* ListView.swift */; }; 18 | 88FD62872BAC4510003459E2 /* DragView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88FD62862BAC4510003459E2 /* DragView.swift */; }; 19 | /* End PBXBuildFile section */ 20 | 21 | /* Begin PBXContainerItemProxy section */ 22 | 88FD62682BAADAB9003459E2 /* PBXContainerItemProxy */ = { 23 | isa = PBXContainerItemProxy; 24 | containerPortal = 88FD624F2BAADAB7003459E2 /* Project object */; 25 | proxyType = 1; 26 | remoteGlobalIDString = 88FD62562BAADAB7003459E2; 27 | remoteInfo = facebookwdae2e; 28 | }; 29 | 88FD62722BAADAB9003459E2 /* PBXContainerItemProxy */ = { 30 | isa = PBXContainerItemProxy; 31 | containerPortal = 88FD624F2BAADAB7003459E2 /* Project object */; 32 | proxyType = 1; 33 | remoteGlobalIDString = 88FD62562BAADAB7003459E2; 34 | remoteInfo = facebookwdae2e; 35 | }; 36 | /* End PBXContainerItemProxy section */ 37 | 38 | /* Begin PBXFileReference section */ 39 | 88FD62572BAADAB7003459E2 /* facebookwdae2e.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = facebookwdae2e.app; sourceTree = BUILT_PRODUCTS_DIR; }; 40 | 88FD625A2BAADAB7003459E2 /* facebookwdae2eApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = facebookwdae2eApp.swift; sourceTree = ""; }; 41 | 88FD625C2BAADAB7003459E2 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 42 | 88FD625E2BAADAB9003459E2 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 43 | 88FD62612BAADAB9003459E2 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 44 | 88FD62672BAADAB9003459E2 /* facebookwdae2eTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = facebookwdae2eTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 45 | 88FD626B2BAADAB9003459E2 /* facebookwdae2eTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = facebookwdae2eTests.swift; sourceTree = ""; }; 46 | 88FD62712BAADAB9003459E2 /* facebookwdae2eUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = facebookwdae2eUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 47 | 88FD62752BAADAB9003459E2 /* facebookwdae2eUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = facebookwdae2eUITests.swift; sourceTree = ""; }; 48 | 88FD62772BAADAB9003459E2 /* facebookwdae2eUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = facebookwdae2eUITestsLaunchTests.swift; sourceTree = ""; }; 49 | 88FD62842BAC3F63003459E2 /* ListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListView.swift; sourceTree = ""; }; 50 | 88FD62862BAC4510003459E2 /* DragView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DragView.swift; sourceTree = ""; }; 51 | /* End PBXFileReference section */ 52 | 53 | /* Begin PBXFrameworksBuildPhase section */ 54 | 88FD62542BAADAB7003459E2 /* Frameworks */ = { 55 | isa = PBXFrameworksBuildPhase; 56 | buildActionMask = 2147483647; 57 | files = ( 58 | ); 59 | runOnlyForDeploymentPostprocessing = 0; 60 | }; 61 | 88FD62642BAADAB9003459E2 /* Frameworks */ = { 62 | isa = PBXFrameworksBuildPhase; 63 | buildActionMask = 2147483647; 64 | files = ( 65 | ); 66 | runOnlyForDeploymentPostprocessing = 0; 67 | }; 68 | 88FD626E2BAADAB9003459E2 /* Frameworks */ = { 69 | isa = PBXFrameworksBuildPhase; 70 | buildActionMask = 2147483647; 71 | files = ( 72 | ); 73 | runOnlyForDeploymentPostprocessing = 0; 74 | }; 75 | /* End PBXFrameworksBuildPhase section */ 76 | 77 | /* Begin PBXGroup section */ 78 | 88FD624E2BAADAB7003459E2 = { 79 | isa = PBXGroup; 80 | children = ( 81 | 88FD62592BAADAB7003459E2 /* facebookwdae2e */, 82 | 88FD626A2BAADAB9003459E2 /* facebookwdae2eTests */, 83 | 88FD62742BAADAB9003459E2 /* facebookwdae2eUITests */, 84 | 88FD62582BAADAB7003459E2 /* Products */, 85 | ); 86 | sourceTree = ""; 87 | }; 88 | 88FD62582BAADAB7003459E2 /* Products */ = { 89 | isa = PBXGroup; 90 | children = ( 91 | 88FD62572BAADAB7003459E2 /* facebookwdae2e.app */, 92 | 88FD62672BAADAB9003459E2 /* facebookwdae2eTests.xctest */, 93 | 88FD62712BAADAB9003459E2 /* facebookwdae2eUITests.xctest */, 94 | ); 95 | name = Products; 96 | sourceTree = ""; 97 | }; 98 | 88FD62592BAADAB7003459E2 /* facebookwdae2e */ = { 99 | isa = PBXGroup; 100 | children = ( 101 | 88FD625A2BAADAB7003459E2 /* facebookwdae2eApp.swift */, 102 | 88FD625C2BAADAB7003459E2 /* ContentView.swift */, 103 | 88FD625E2BAADAB9003459E2 /* Assets.xcassets */, 104 | 88FD62602BAADAB9003459E2 /* Preview Content */, 105 | 88FD62842BAC3F63003459E2 /* ListView.swift */, 106 | 88FD62862BAC4510003459E2 /* DragView.swift */, 107 | ); 108 | path = facebookwdae2e; 109 | sourceTree = ""; 110 | }; 111 | 88FD62602BAADAB9003459E2 /* Preview Content */ = { 112 | isa = PBXGroup; 113 | children = ( 114 | 88FD62612BAADAB9003459E2 /* Preview Assets.xcassets */, 115 | ); 116 | path = "Preview Content"; 117 | sourceTree = ""; 118 | }; 119 | 88FD626A2BAADAB9003459E2 /* facebookwdae2eTests */ = { 120 | isa = PBXGroup; 121 | children = ( 122 | 88FD626B2BAADAB9003459E2 /* facebookwdae2eTests.swift */, 123 | ); 124 | path = facebookwdae2eTests; 125 | sourceTree = ""; 126 | }; 127 | 88FD62742BAADAB9003459E2 /* facebookwdae2eUITests */ = { 128 | isa = PBXGroup; 129 | children = ( 130 | 88FD62752BAADAB9003459E2 /* facebookwdae2eUITests.swift */, 131 | 88FD62772BAADAB9003459E2 /* facebookwdae2eUITestsLaunchTests.swift */, 132 | ); 133 | path = facebookwdae2eUITests; 134 | sourceTree = ""; 135 | }; 136 | /* End PBXGroup section */ 137 | 138 | /* Begin PBXNativeTarget section */ 139 | 88FD62562BAADAB7003459E2 /* facebookwdae2e */ = { 140 | isa = PBXNativeTarget; 141 | buildConfigurationList = 88FD627B2BAADAB9003459E2 /* Build configuration list for PBXNativeTarget "facebookwdae2e" */; 142 | buildPhases = ( 143 | 88FD62532BAADAB7003459E2 /* Sources */, 144 | 88FD62542BAADAB7003459E2 /* Frameworks */, 145 | 88FD62552BAADAB7003459E2 /* Resources */, 146 | ); 147 | buildRules = ( 148 | ); 149 | dependencies = ( 150 | ); 151 | name = facebookwdae2e; 152 | productName = facebookwdae2e; 153 | productReference = 88FD62572BAADAB7003459E2 /* facebookwdae2e.app */; 154 | productType = "com.apple.product-type.application"; 155 | }; 156 | 88FD62662BAADAB9003459E2 /* facebookwdae2eTests */ = { 157 | isa = PBXNativeTarget; 158 | buildConfigurationList = 88FD627E2BAADAB9003459E2 /* Build configuration list for PBXNativeTarget "facebookwdae2eTests" */; 159 | buildPhases = ( 160 | 88FD62632BAADAB9003459E2 /* Sources */, 161 | 88FD62642BAADAB9003459E2 /* Frameworks */, 162 | 88FD62652BAADAB9003459E2 /* Resources */, 163 | ); 164 | buildRules = ( 165 | ); 166 | dependencies = ( 167 | 88FD62692BAADAB9003459E2 /* PBXTargetDependency */, 168 | ); 169 | name = facebookwdae2eTests; 170 | productName = facebookwdae2eTests; 171 | productReference = 88FD62672BAADAB9003459E2 /* facebookwdae2eTests.xctest */; 172 | productType = "com.apple.product-type.bundle.unit-test"; 173 | }; 174 | 88FD62702BAADAB9003459E2 /* facebookwdae2eUITests */ = { 175 | isa = PBXNativeTarget; 176 | buildConfigurationList = 88FD62812BAADAB9003459E2 /* Build configuration list for PBXNativeTarget "facebookwdae2eUITests" */; 177 | buildPhases = ( 178 | 88FD626D2BAADAB9003459E2 /* Sources */, 179 | 88FD626E2BAADAB9003459E2 /* Frameworks */, 180 | 88FD626F2BAADAB9003459E2 /* Resources */, 181 | ); 182 | buildRules = ( 183 | ); 184 | dependencies = ( 185 | 88FD62732BAADAB9003459E2 /* PBXTargetDependency */, 186 | ); 187 | name = facebookwdae2eUITests; 188 | productName = facebookwdae2eUITests; 189 | productReference = 88FD62712BAADAB9003459E2 /* facebookwdae2eUITests.xctest */; 190 | productType = "com.apple.product-type.bundle.ui-testing"; 191 | }; 192 | /* End PBXNativeTarget section */ 193 | 194 | /* Begin PBXProject section */ 195 | 88FD624F2BAADAB7003459E2 /* Project object */ = { 196 | isa = PBXProject; 197 | attributes = { 198 | BuildIndependentTargetsInParallel = 1; 199 | LastSwiftUpdateCheck = 1430; 200 | LastUpgradeCheck = 1430; 201 | TargetAttributes = { 202 | 88FD62562BAADAB7003459E2 = { 203 | CreatedOnToolsVersion = 14.3; 204 | }; 205 | 88FD62662BAADAB9003459E2 = { 206 | CreatedOnToolsVersion = 14.3; 207 | TestTargetID = 88FD62562BAADAB7003459E2; 208 | }; 209 | 88FD62702BAADAB9003459E2 = { 210 | CreatedOnToolsVersion = 14.3; 211 | TestTargetID = 88FD62562BAADAB7003459E2; 212 | }; 213 | }; 214 | }; 215 | buildConfigurationList = 88FD62522BAADAB7003459E2 /* Build configuration list for PBXProject "facebookwdae2e" */; 216 | compatibilityVersion = "Xcode 14.0"; 217 | developmentRegion = en; 218 | hasScannedForEncodings = 0; 219 | knownRegions = ( 220 | en, 221 | Base, 222 | ); 223 | mainGroup = 88FD624E2BAADAB7003459E2; 224 | productRefGroup = 88FD62582BAADAB7003459E2 /* Products */; 225 | projectDirPath = ""; 226 | projectRoot = ""; 227 | targets = ( 228 | 88FD62562BAADAB7003459E2 /* facebookwdae2e */, 229 | 88FD62662BAADAB9003459E2 /* facebookwdae2eTests */, 230 | 88FD62702BAADAB9003459E2 /* facebookwdae2eUITests */, 231 | ); 232 | }; 233 | /* End PBXProject section */ 234 | 235 | /* Begin PBXResourcesBuildPhase section */ 236 | 88FD62552BAADAB7003459E2 /* Resources */ = { 237 | isa = PBXResourcesBuildPhase; 238 | buildActionMask = 2147483647; 239 | files = ( 240 | 88FD62622BAADAB9003459E2 /* Preview Assets.xcassets in Resources */, 241 | 88FD625F2BAADAB9003459E2 /* Assets.xcassets in Resources */, 242 | ); 243 | runOnlyForDeploymentPostprocessing = 0; 244 | }; 245 | 88FD62652BAADAB9003459E2 /* Resources */ = { 246 | isa = PBXResourcesBuildPhase; 247 | buildActionMask = 2147483647; 248 | files = ( 249 | ); 250 | runOnlyForDeploymentPostprocessing = 0; 251 | }; 252 | 88FD626F2BAADAB9003459E2 /* Resources */ = { 253 | isa = PBXResourcesBuildPhase; 254 | buildActionMask = 2147483647; 255 | files = ( 256 | ); 257 | runOnlyForDeploymentPostprocessing = 0; 258 | }; 259 | /* End PBXResourcesBuildPhase section */ 260 | 261 | /* Begin PBXSourcesBuildPhase section */ 262 | 88FD62532BAADAB7003459E2 /* Sources */ = { 263 | isa = PBXSourcesBuildPhase; 264 | buildActionMask = 2147483647; 265 | files = ( 266 | 88FD625D2BAADAB7003459E2 /* ContentView.swift in Sources */, 267 | 88FD625B2BAADAB7003459E2 /* facebookwdae2eApp.swift in Sources */, 268 | 88FD62872BAC4510003459E2 /* DragView.swift in Sources */, 269 | 88FD62852BAC3F63003459E2 /* ListView.swift in Sources */, 270 | ); 271 | runOnlyForDeploymentPostprocessing = 0; 272 | }; 273 | 88FD62632BAADAB9003459E2 /* Sources */ = { 274 | isa = PBXSourcesBuildPhase; 275 | buildActionMask = 2147483647; 276 | files = ( 277 | 88FD626C2BAADAB9003459E2 /* facebookwdae2eTests.swift in Sources */, 278 | ); 279 | runOnlyForDeploymentPostprocessing = 0; 280 | }; 281 | 88FD626D2BAADAB9003459E2 /* Sources */ = { 282 | isa = PBXSourcesBuildPhase; 283 | buildActionMask = 2147483647; 284 | files = ( 285 | 88FD62782BAADAB9003459E2 /* facebookwdae2eUITestsLaunchTests.swift in Sources */, 286 | 88FD62762BAADAB9003459E2 /* facebookwdae2eUITests.swift in Sources */, 287 | ); 288 | runOnlyForDeploymentPostprocessing = 0; 289 | }; 290 | /* End PBXSourcesBuildPhase section */ 291 | 292 | /* Begin PBXTargetDependency section */ 293 | 88FD62692BAADAB9003459E2 /* PBXTargetDependency */ = { 294 | isa = PBXTargetDependency; 295 | target = 88FD62562BAADAB7003459E2 /* facebookwdae2e */; 296 | targetProxy = 88FD62682BAADAB9003459E2 /* PBXContainerItemProxy */; 297 | }; 298 | 88FD62732BAADAB9003459E2 /* PBXTargetDependency */ = { 299 | isa = PBXTargetDependency; 300 | target = 88FD62562BAADAB7003459E2 /* facebookwdae2e */; 301 | targetProxy = 88FD62722BAADAB9003459E2 /* PBXContainerItemProxy */; 302 | }; 303 | /* End PBXTargetDependency section */ 304 | 305 | /* Begin XCBuildConfiguration section */ 306 | 88FD62792BAADAB9003459E2 /* Debug */ = { 307 | isa = XCBuildConfiguration; 308 | buildSettings = { 309 | ALWAYS_SEARCH_USER_PATHS = NO; 310 | CLANG_ANALYZER_NONNULL = YES; 311 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 312 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 313 | CLANG_ENABLE_MODULES = YES; 314 | CLANG_ENABLE_OBJC_ARC = YES; 315 | CLANG_ENABLE_OBJC_WEAK = YES; 316 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 317 | CLANG_WARN_BOOL_CONVERSION = YES; 318 | CLANG_WARN_COMMA = YES; 319 | CLANG_WARN_CONSTANT_CONVERSION = YES; 320 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 321 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 322 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 323 | CLANG_WARN_EMPTY_BODY = YES; 324 | CLANG_WARN_ENUM_CONVERSION = YES; 325 | CLANG_WARN_INFINITE_RECURSION = YES; 326 | CLANG_WARN_INT_CONVERSION = YES; 327 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 328 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 329 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 330 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 331 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 332 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 333 | CLANG_WARN_STRICT_PROTOTYPES = YES; 334 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 335 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 336 | CLANG_WARN_UNREACHABLE_CODE = YES; 337 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 338 | COPY_PHASE_STRIP = NO; 339 | DEBUG_INFORMATION_FORMAT = dwarf; 340 | ENABLE_STRICT_OBJC_MSGSEND = YES; 341 | ENABLE_TESTABILITY = YES; 342 | GCC_C_LANGUAGE_STANDARD = gnu11; 343 | GCC_DYNAMIC_NO_PIC = NO; 344 | GCC_NO_COMMON_BLOCKS = YES; 345 | GCC_OPTIMIZATION_LEVEL = 0; 346 | GCC_PREPROCESSOR_DEFINITIONS = ( 347 | "DEBUG=1", 348 | "$(inherited)", 349 | ); 350 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 351 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 352 | GCC_WARN_UNDECLARED_SELECTOR = YES; 353 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 354 | GCC_WARN_UNUSED_FUNCTION = YES; 355 | GCC_WARN_UNUSED_VARIABLE = YES; 356 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 357 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 358 | MTL_FAST_MATH = YES; 359 | ONLY_ACTIVE_ARCH = YES; 360 | SDKROOT = iphoneos; 361 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 362 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 363 | }; 364 | name = Debug; 365 | }; 366 | 88FD627A2BAADAB9003459E2 /* Release */ = { 367 | isa = XCBuildConfiguration; 368 | buildSettings = { 369 | ALWAYS_SEARCH_USER_PATHS = NO; 370 | CLANG_ANALYZER_NONNULL = YES; 371 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 372 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 373 | CLANG_ENABLE_MODULES = YES; 374 | CLANG_ENABLE_OBJC_ARC = YES; 375 | CLANG_ENABLE_OBJC_WEAK = YES; 376 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 377 | CLANG_WARN_BOOL_CONVERSION = YES; 378 | CLANG_WARN_COMMA = YES; 379 | CLANG_WARN_CONSTANT_CONVERSION = YES; 380 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 381 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 382 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 383 | CLANG_WARN_EMPTY_BODY = YES; 384 | CLANG_WARN_ENUM_CONVERSION = YES; 385 | CLANG_WARN_INFINITE_RECURSION = YES; 386 | CLANG_WARN_INT_CONVERSION = YES; 387 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 388 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 389 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 390 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 391 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 392 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 393 | CLANG_WARN_STRICT_PROTOTYPES = YES; 394 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 395 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 396 | CLANG_WARN_UNREACHABLE_CODE = YES; 397 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 398 | COPY_PHASE_STRIP = NO; 399 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 400 | ENABLE_NS_ASSERTIONS = NO; 401 | ENABLE_STRICT_OBJC_MSGSEND = YES; 402 | GCC_C_LANGUAGE_STANDARD = gnu11; 403 | GCC_NO_COMMON_BLOCKS = YES; 404 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 405 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 406 | GCC_WARN_UNDECLARED_SELECTOR = YES; 407 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 408 | GCC_WARN_UNUSED_FUNCTION = YES; 409 | GCC_WARN_UNUSED_VARIABLE = YES; 410 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 411 | MTL_ENABLE_DEBUG_INFO = NO; 412 | MTL_FAST_MATH = YES; 413 | SDKROOT = iphoneos; 414 | SWIFT_COMPILATION_MODE = wholemodule; 415 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 416 | VALIDATE_PRODUCT = YES; 417 | }; 418 | name = Release; 419 | }; 420 | 88FD627C2BAADAB9003459E2 /* Debug */ = { 421 | isa = XCBuildConfiguration; 422 | buildSettings = { 423 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 424 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 425 | CODE_SIGN_STYLE = Automatic; 426 | CURRENT_PROJECT_VERSION = 1; 427 | DEVELOPMENT_ASSET_PATHS = "\"facebookwdae2e/Preview Content\""; 428 | DEVELOPMENT_TEAM = WK7SZCY93A; 429 | ENABLE_PREVIEWS = YES; 430 | GENERATE_INFOPLIST_FILE = YES; 431 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 432 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 433 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 434 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 435 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 436 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 437 | LD_RUNPATH_SEARCH_PATHS = ( 438 | "$(inherited)", 439 | "@executable_path/Frameworks", 440 | ); 441 | MARKETING_VERSION = 1.0; 442 | PRODUCT_BUNDLE_IDENTIFIER = com.test.cert.TestCert; 443 | PRODUCT_NAME = "$(TARGET_NAME)"; 444 | SWIFT_EMIT_LOC_STRINGS = YES; 445 | SWIFT_VERSION = 5.0; 446 | TARGETED_DEVICE_FAMILY = "1,2"; 447 | }; 448 | name = Debug; 449 | }; 450 | 88FD627D2BAADAB9003459E2 /* Release */ = { 451 | isa = XCBuildConfiguration; 452 | buildSettings = { 453 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 454 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 455 | CODE_SIGN_STYLE = Automatic; 456 | CURRENT_PROJECT_VERSION = 1; 457 | DEVELOPMENT_ASSET_PATHS = "\"facebookwdae2e/Preview Content\""; 458 | DEVELOPMENT_TEAM = WK7SZCY93A; 459 | ENABLE_PREVIEWS = YES; 460 | GENERATE_INFOPLIST_FILE = YES; 461 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 462 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 463 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 464 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 465 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 466 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 467 | LD_RUNPATH_SEARCH_PATHS = ( 468 | "$(inherited)", 469 | "@executable_path/Frameworks", 470 | ); 471 | MARKETING_VERSION = 1.0; 472 | PRODUCT_BUNDLE_IDENTIFIER = com.test.cert.TestCert; 473 | PRODUCT_NAME = "$(TARGET_NAME)"; 474 | SWIFT_EMIT_LOC_STRINGS = YES; 475 | SWIFT_VERSION = 5.0; 476 | TARGETED_DEVICE_FAMILY = "1,2"; 477 | }; 478 | name = Release; 479 | }; 480 | 88FD627F2BAADAB9003459E2 /* Debug */ = { 481 | isa = XCBuildConfiguration; 482 | buildSettings = { 483 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 484 | BUNDLE_LOADER = "$(TEST_HOST)"; 485 | CODE_SIGN_STYLE = Automatic; 486 | CURRENT_PROJECT_VERSION = 1; 487 | DEVELOPMENT_TEAM = V82S4U9CM9; 488 | GENERATE_INFOPLIST_FILE = YES; 489 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 490 | MARKETING_VERSION = 1.0; 491 | PRODUCT_BUNDLE_IDENTIFIER = com.facebookwda.e2e.facebookwdae2eTests; 492 | PRODUCT_NAME = "$(TARGET_NAME)"; 493 | SWIFT_EMIT_LOC_STRINGS = NO; 494 | SWIFT_VERSION = 5.0; 495 | TARGETED_DEVICE_FAMILY = "1,2"; 496 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/facebookwdae2e.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/facebookwdae2e"; 497 | }; 498 | name = Debug; 499 | }; 500 | 88FD62802BAADAB9003459E2 /* Release */ = { 501 | isa = XCBuildConfiguration; 502 | buildSettings = { 503 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 504 | BUNDLE_LOADER = "$(TEST_HOST)"; 505 | CODE_SIGN_STYLE = Automatic; 506 | CURRENT_PROJECT_VERSION = 1; 507 | DEVELOPMENT_TEAM = V82S4U9CM9; 508 | GENERATE_INFOPLIST_FILE = YES; 509 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 510 | MARKETING_VERSION = 1.0; 511 | PRODUCT_BUNDLE_IDENTIFIER = com.facebookwda.e2e.facebookwdae2eTests; 512 | PRODUCT_NAME = "$(TARGET_NAME)"; 513 | SWIFT_EMIT_LOC_STRINGS = NO; 514 | SWIFT_VERSION = 5.0; 515 | TARGETED_DEVICE_FAMILY = "1,2"; 516 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/facebookwdae2e.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/facebookwdae2e"; 517 | }; 518 | name = Release; 519 | }; 520 | 88FD62822BAADAB9003459E2 /* Debug */ = { 521 | isa = XCBuildConfiguration; 522 | buildSettings = { 523 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 524 | CODE_SIGN_STYLE = Automatic; 525 | CURRENT_PROJECT_VERSION = 1; 526 | DEVELOPMENT_TEAM = WK7SZCY93A; 527 | GENERATE_INFOPLIST_FILE = YES; 528 | MARKETING_VERSION = 1.0; 529 | PRODUCT_BUNDLE_IDENTIFIER = com.facebookwda.e2e.facebookwdae2eUITests; 530 | PRODUCT_NAME = "$(TARGET_NAME)"; 531 | SWIFT_EMIT_LOC_STRINGS = NO; 532 | SWIFT_VERSION = 5.0; 533 | TARGETED_DEVICE_FAMILY = "1,2"; 534 | TEST_TARGET_NAME = facebookwdae2e; 535 | }; 536 | name = Debug; 537 | }; 538 | 88FD62832BAADAB9003459E2 /* Release */ = { 539 | isa = XCBuildConfiguration; 540 | buildSettings = { 541 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 542 | CODE_SIGN_STYLE = Automatic; 543 | CURRENT_PROJECT_VERSION = 1; 544 | DEVELOPMENT_TEAM = WK7SZCY93A; 545 | GENERATE_INFOPLIST_FILE = YES; 546 | MARKETING_VERSION = 1.0; 547 | PRODUCT_BUNDLE_IDENTIFIER = com.facebookwda.e2e.facebookwdae2eUITests; 548 | PRODUCT_NAME = "$(TARGET_NAME)"; 549 | SWIFT_EMIT_LOC_STRINGS = NO; 550 | SWIFT_VERSION = 5.0; 551 | TARGETED_DEVICE_FAMILY = "1,2"; 552 | TEST_TARGET_NAME = facebookwdae2e; 553 | }; 554 | name = Release; 555 | }; 556 | /* End XCBuildConfiguration section */ 557 | 558 | /* Begin XCConfigurationList section */ 559 | 88FD62522BAADAB7003459E2 /* Build configuration list for PBXProject "facebookwdae2e" */ = { 560 | isa = XCConfigurationList; 561 | buildConfigurations = ( 562 | 88FD62792BAADAB9003459E2 /* Debug */, 563 | 88FD627A2BAADAB9003459E2 /* Release */, 564 | ); 565 | defaultConfigurationIsVisible = 0; 566 | defaultConfigurationName = Release; 567 | }; 568 | 88FD627B2BAADAB9003459E2 /* Build configuration list for PBXNativeTarget "facebookwdae2e" */ = { 569 | isa = XCConfigurationList; 570 | buildConfigurations = ( 571 | 88FD627C2BAADAB9003459E2 /* Debug */, 572 | 88FD627D2BAADAB9003459E2 /* Release */, 573 | ); 574 | defaultConfigurationIsVisible = 0; 575 | defaultConfigurationName = Release; 576 | }; 577 | 88FD627E2BAADAB9003459E2 /* Build configuration list for PBXNativeTarget "facebookwdae2eTests" */ = { 578 | isa = XCConfigurationList; 579 | buildConfigurations = ( 580 | 88FD627F2BAADAB9003459E2 /* Debug */, 581 | 88FD62802BAADAB9003459E2 /* Release */, 582 | ); 583 | defaultConfigurationIsVisible = 0; 584 | defaultConfigurationName = Release; 585 | }; 586 | 88FD62812BAADAB9003459E2 /* Build configuration list for PBXNativeTarget "facebookwdae2eUITests" */ = { 587 | isa = XCConfigurationList; 588 | buildConfigurations = ( 589 | 88FD62822BAADAB9003459E2 /* Debug */, 590 | 88FD62832BAADAB9003459E2 /* Release */, 591 | ); 592 | defaultConfigurationIsVisible = 0; 593 | defaultConfigurationName = Release; 594 | }; 595 | /* End XCConfigurationList section */ 596 | }; 597 | rootObject = 88FD624F2BAADAB7003459E2 /* Project object */; 598 | } 599 | -------------------------------------------------------------------------------- /e2e_benchmarks/app/facebookwdae2e/facebookwdae2e.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /e2e_benchmarks/app/facebookwdae2e/facebookwdae2e.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /e2e_benchmarks/app/facebookwdae2e/facebookwdae2e.xcodeproj/project.xcworkspace/xcuserdata/youngfreefjs.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openatx/facebook-wda/85d944acb2371820c9c6542b6063948def94c120/e2e_benchmarks/app/facebookwdae2e/facebookwdae2e.xcodeproj/project.xcworkspace/xcuserdata/youngfreefjs.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /e2e_benchmarks/app/facebookwdae2e/facebookwdae2e.xcodeproj/xcuserdata/youngfreefjs.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | facebookwdae2e.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /e2e_benchmarks/app/facebookwdae2e/facebookwdae2e/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e_benchmarks/app/facebookwdae2e/facebookwdae2e/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "e2e (2).png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /e2e_benchmarks/app/facebookwdae2e/facebookwdae2e/Assets.xcassets/AppIcon.appiconset/e2e (2).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openatx/facebook-wda/85d944acb2371820c9c6542b6063948def94c120/e2e_benchmarks/app/facebookwdae2e/facebookwdae2e/Assets.xcassets/AppIcon.appiconset/e2e (2).png -------------------------------------------------------------------------------- /e2e_benchmarks/app/facebookwdae2e/facebookwdae2e/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /e2e_benchmarks/app/facebookwdae2e/facebookwdae2e/Assets.xcassets/applogo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "applogo.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "applogo 1.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "applogo 2.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /e2e_benchmarks/app/facebookwdae2e/facebookwdae2e/Assets.xcassets/applogo.imageset/applogo 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openatx/facebook-wda/85d944acb2371820c9c6542b6063948def94c120/e2e_benchmarks/app/facebookwdae2e/facebookwdae2e/Assets.xcassets/applogo.imageset/applogo 1.png -------------------------------------------------------------------------------- /e2e_benchmarks/app/facebookwdae2e/facebookwdae2e/Assets.xcassets/applogo.imageset/applogo 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openatx/facebook-wda/85d944acb2371820c9c6542b6063948def94c120/e2e_benchmarks/app/facebookwdae2e/facebookwdae2e/Assets.xcassets/applogo.imageset/applogo 2.png -------------------------------------------------------------------------------- /e2e_benchmarks/app/facebookwdae2e/facebookwdae2e/Assets.xcassets/applogo.imageset/applogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openatx/facebook-wda/85d944acb2371820c9c6542b6063948def94c120/e2e_benchmarks/app/facebookwdae2e/facebookwdae2e/Assets.xcassets/applogo.imageset/applogo.png -------------------------------------------------------------------------------- /e2e_benchmarks/app/facebookwdae2e/facebookwdae2e/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // facebookwdae2e 4 | // 5 | // Created by youngfreefjs on 2024/3/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // Custom struct to encapsulate the confirmation alert logic with a button 11 | struct ConfirmationAlertButton: View { 12 | @Binding var isPresented: Bool 13 | let id: String 14 | 15 | var body: some View { 16 | Button(id) { 17 | isPresented = true 18 | } 19 | .id(id) 20 | .accessibilityLabel(id) 21 | .accessibilityIdentifier(id) 22 | .alert(isPresented: $isPresented) { 23 | Alert( 24 | title: Text("Confirmation"), 25 | message: Text("Do you accept?"), 26 | primaryButton: .default(Text("Accept").accessibilityLabel("Accept")) { 27 | // Handle acceptance 28 | print("Accepted") 29 | }, 30 | secondaryButton: .cancel(Text("Reject").accessibilityLabel("Reject")) { 31 | // Handle rejection 32 | print("Rejected") 33 | } 34 | ) 35 | } 36 | .padding() 37 | } 38 | } 39 | 40 | // Custom struct to encapsulate the text input alert logic 41 | struct TextInputAlert: View { 42 | @Binding var isPresented: Bool 43 | @Binding var text: String 44 | let onAccept: (String) -> Void 45 | 46 | var body: some View { 47 | if isPresented { 48 | VStack(spacing: 20) { 49 | Text("Enter your input") 50 | TextField("Enter text", text: $text) 51 | .textFieldStyle(RoundedBorderTextFieldStyle()) 52 | HStack(spacing: 20) { 53 | Button("Cancel") { 54 | isPresented = false 55 | } 56 | Button("Accept") { 57 | isPresented = false 58 | onAccept(text) 59 | } 60 | } 61 | } 62 | .padding() 63 | .background(Color.white) 64 | .cornerRadius(10) 65 | .shadow(radius: 5) 66 | .frame(maxWidth: 300) 67 | .transition(.scale) 68 | } 69 | } 70 | } 71 | 72 | // View extension to present the custom text input alert 73 | extension View { 74 | func textInputAlert(isPresented: Binding, text: Binding, onAccept: @escaping (String) -> Void) -> some View { 75 | ZStack { 76 | self 77 | TextInputAlert(isPresented: isPresented, text: text, onAccept: onAccept) 78 | .animation(.default, value: isPresented.wrappedValue) 79 | .opacity(isPresented.wrappedValue ? 1 : 0) 80 | } 81 | } 82 | } 83 | 84 | // ContentView using both ConfirmationAlertButton and TextInputAlert 85 | struct ContentView: View { 86 | @State private var showConfirmationAlert = false 87 | @State private var showTextInputAlert = false 88 | @State private var userInput = "this value" 89 | @State private var isButtonHidden = true 90 | @State private var isChecked: Bool = false 91 | @State private var inputText: String = "" 92 | @State private var longTapShowAlert = false 93 | @State private var DoubleTapShowAlert = false 94 | @State private var showAlert = false 95 | @State private var alertText = "" 96 | @State private var orientation: UIDeviceOrientation = UIDevice.current.orientation 97 | 98 | 99 | let acceptOrRejectAlertID = "ACCEPT_OR_REJECT_ALERT" 100 | let inputAlertID = "INPUT_ALERT" 101 | 102 | 103 | var orientationText: String { 104 | switch orientation { 105 | case .landscapeLeft, .landscapeRight: 106 | return "LANDSCAPE" 107 | case .portrait, .portraitUpsideDown: 108 | return "PORTRAIT" 109 | default: 110 | return "UNKNOW" 111 | } 112 | } 113 | 114 | var body: some View { 115 | NavigationView { 116 | 117 | VStack { 118 | 119 | HStack { 120 | ConfirmationAlertButton(isPresented: $showConfirmationAlert, id: acceptOrRejectAlertID) 121 | 122 | Button(inputAlertID) { 123 | showTextInputAlert = true 124 | }.id(inputAlertID) 125 | .accessibilityLabel(inputAlertID) 126 | .accessibilityIdentifier(inputAlertID) 127 | .padding() 128 | 129 | 130 | } 131 | 132 | // 新的并排按钮 133 | HStack { 134 | // 启用的按钮 135 | Button("ENABLED_BTN") { 136 | // 这里处理按钮点击事件 137 | print("Enabled button tapped") 138 | } 139 | .background(Color.black) // 背景颜色 140 | .accessibilityLabel("ENABLED_BTN") 141 | .accessibilityIdentifier("ENABLED_BTN") 142 | 143 | .padding() 144 | 145 | // 禁用的按钮 146 | Button("DISABLED_BTN") { 147 | // 由于按钮被禁用,这里的代码不会被执行 148 | } 149 | .accessibilityLabel("DISABLED_BTN") 150 | .accessibilityIdentifier("DISABLED_BTN") 151 | .disabled(true) // 禁用按钮 152 | .padding() 153 | 154 | // 添加本地图像 155 | Image("applogo") 156 | .resizable() 157 | .scaledToFit() 158 | .frame(width: 100, height: 50) 159 | .accessibilityIdentifier("IMG_BTN") 160 | 161 | // 隐藏按钮 162 | Button("HIDDEN_BTN") { 163 | // 按钮的动作 164 | } 165 | .opacity(isButtonHidden ? 0 : 1) // 根据条件设置透明度 166 | // 或者 167 | // .hidden(isButtonHidden) // 根据条件隐藏按钮,但这需要自定义扩展 168 | } 169 | 170 | // select 171 | HStack { 172 | // 没有被选中的勾选框 173 | Button(action: { 174 | isChecked = false 175 | }) { 176 | Image(systemName: isChecked ? "circle" : "checkmark.circle.fill") 177 | }.accessibilityIdentifier("CHECKED_BTN") 178 | 179 | // 被选中的勾选框 180 | Button(action: { 181 | isChecked = true 182 | }) { 183 | Image(systemName: isChecked ? "checkmark.circle.fill" : "circle") 184 | }.accessibilityIdentifier("UNCHECKED_BTN") 185 | } 186 | // input 187 | HStack { 188 | TextField("INPUT_FIELD", text: $inputText) 189 | .accessibilityLabel("INPUT_FIELD") 190 | .textFieldStyle(RoundedBorderTextFieldStyle()) 191 | .padding() 192 | 193 | Button(action: { 194 | self.inputText = "" // 清空输入框 195 | }) { 196 | Text("CLEAR INPUT") 197 | }.accessibilityLabel("CLEAR_INPUT_BTN") 198 | .padding() 199 | } 200 | 201 | // accessibilityContainer COMBINED_TEXT_CONTAINER 202 | VStack { 203 | Text("First line of text") 204 | Text("Second line of text") 205 | Text("Third line of text") 206 | } 207 | .accessibilityElement(children: .combine) 208 | .accessibilityIdentifier("COMBINED_TEXT_CONTAINER") 209 | 210 | HStack { 211 | Text("LONG_TAP_ALERT") 212 | .padding() 213 | .background(Color.blue) 214 | .foregroundColor(Color.white) 215 | .cornerRadius(5) 216 | .onLongPressGesture(minimumDuration: 1.0) { 217 | self.longTapShowAlert = true 218 | } 219 | .alert(isPresented: $longTapShowAlert) { 220 | Alert( 221 | title: Text("Long Tap Alert"), 222 | message: Text("Long Tap Alert"), 223 | dismissButton: .default(Text("LONG_TAP_ALERT_OK")) 224 | ) 225 | } 226 | 227 | Text("DOUBLE_TAP_ALERT") 228 | .padding() 229 | .background(Color.blue) 230 | .foregroundColor(Color.white) 231 | .cornerRadius(5) 232 | .onTapGesture(count: 2) { 233 | self.DoubleTapShowAlert = true 234 | } 235 | .alert(isPresented: $DoubleTapShowAlert) { 236 | Alert( 237 | title: Text("DOUBLE Tap Alert"), 238 | message: Text("DOUBLE Tap Alert"), 239 | dismissButton: .default(Text("DOUBLE_TAP_ALERT_OK")) 240 | ) 241 | } 242 | 243 | 244 | } 245 | 246 | // Return Origatation Text 247 | Text(orientationText) 248 | .accessibilityIdentifier("ORIGATATION_TEXT") 249 | .onAppear { 250 | // Listener 251 | NotificationCenter.default.addObserver( 252 | forName: UIDevice.orientationDidChangeNotification, 253 | object: nil, 254 | queue: .main 255 | ) { _ in 256 | // Update 257 | orientation = UIDevice.current.orientation 258 | } 259 | } 260 | 261 | 262 | 263 | // List View 264 | HStack { 265 | // GO OTHER 266 | NavigationLink(destination: ListView()) { 267 | Text("ListView") 268 | } 269 | .padding() 270 | .background(Color.blue) 271 | .foregroundColor(Color.white) 272 | .cornerRadius(8) 273 | 274 | // GO LIST 275 | NavigationLink(destination: DragView()) { 276 | Text("DragView") 277 | } 278 | .padding() 279 | .background(Color.blue) 280 | .foregroundColor(Color.white) 281 | .cornerRadius(8) 282 | 283 | // 其他内容 284 | } 285 | .navigationBarTitle("Home", displayMode: .inline) 286 | 287 | } 288 | .textInputAlert(isPresented: $showTextInputAlert, text: $userInput) { enteredText in 289 | // Handle the text input acceptance here 290 | print("The user entered: \(enteredText)") 291 | } 292 | } 293 | } 294 | } 295 | 296 | struct ContentView_Previews: PreviewProvider { 297 | static var previews: some View { 298 | ContentView() 299 | } 300 | } 301 | 302 | 303 | // SwiftUI 没有内置的带文本输入的弹出对话框,所以我们需要自定义一个 304 | extension View { 305 | func textFieldAlert(isPresented: Binding, text: Binding) -> some View { 306 | TextFieldAlert(isPresented: isPresented, text: text, presentingView: self) 307 | } 308 | } 309 | 310 | struct TextFieldAlert: UIViewControllerRepresentable { 311 | @Binding var isPresented: Bool 312 | @Binding var text: String 313 | let presentingView: PresentingView 314 | 315 | func makeUIViewController(context: Context) -> UIViewController { 316 | UIViewController() 317 | } 318 | 319 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) { 320 | guard context.coordinator.alert == nil else { return } 321 | 322 | if isPresented { 323 | let alert = UIAlertController(title: "Alert", message: "Type something:", preferredStyle: .alert) 324 | alert.addTextField { textField in 325 | textField.placeholder = "Enter text here" 326 | textField.text = text 327 | } 328 | 329 | alert.addAction(UIAlertAction(title: "Submit", style: .default) { _ in 330 | if let textField = alert.textFields?.first, let text = textField.text { 331 | self.text = text 332 | isPresented = false 333 | } 334 | }) 335 | 336 | alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in 337 | isPresented = false 338 | }) 339 | 340 | context.coordinator.alert = alert 341 | 342 | DispatchQueue.main.async { // Must be presented in the main thread 343 | uiViewController.present(alert, animated: true, completion: { 344 | self.isPresented = false 345 | context.coordinator.alert = nil 346 | }) 347 | } 348 | } 349 | } 350 | 351 | func makeCoordinator() -> Coordinator { 352 | Coordinator(self) 353 | } 354 | 355 | class Coordinator { 356 | var alert: UIAlertController? 357 | var textFieldAlert: TextFieldAlert 358 | 359 | init(_ textFieldAlert: TextFieldAlert) { 360 | self.textFieldAlert = textFieldAlert 361 | } 362 | } 363 | } 364 | -------------------------------------------------------------------------------- /e2e_benchmarks/app/facebookwdae2e/facebookwdae2e/DragView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DragView.swift 3 | // facebookwdae2e 4 | // 5 | // Created by youngfreefjs on 2024/3/21. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | struct DragView: View { 12 | @State private var showingAlert = false 13 | @State private var isLongPressing = false 14 | 15 | // 按钮B的布局边界 16 | @State private var buttonBFrame: CGRect = .zero 17 | 18 | var body: some View { 19 | VStack { 20 | // 按钮A 21 | Button("按钮 A (长按并拖到B)") { 22 | // 不在这里处理点击事件 23 | } 24 | .simultaneousGesture(LongPressGesture(minimumDuration: 0.5).onEnded { _ in 25 | self.isLongPressing = true 26 | }) 27 | .gesture(DragGesture(minimumDistance: 0) 28 | .onEnded { value in 29 | if self.isLongPressing && self.buttonBFrame.contains(value.location) { 30 | self.showingAlert = true 31 | } 32 | self.isLongPressing = false 33 | } 34 | ) 35 | 36 | Spacer().frame(height: 50) 37 | 38 | // 按钮B 39 | Button("按钮 B") { 40 | // 不在这里处理点击事件 41 | } 42 | .background(GeometryReader { geometry in 43 | Color.clear.onAppear { self.buttonBFrame = geometry.frame(in: .global) } 44 | }) 45 | 46 | Spacer() 47 | } 48 | .alert(isPresented: $showingAlert) { 49 | Alert(title: Text("成功"), message: Text("你已经成功长按并拖动到按钮 B!"), dismissButton: .default(Text("好的"))) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /e2e_benchmarks/app/facebookwdae2e/facebookwdae2e/ListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListView.swift 3 | // facebookwdae2e 4 | // 5 | // Created by youngfreefjs on 2024/3/21. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | // ListView.swift 12 | struct ListView: View { 13 | var body: some View { 14 | List(1...100, id: \.self) { number in 15 | Text("Row\(number)") 16 | }.accessibilityLabel("LIST_CONTAINER") 17 | .navigationBarTitle("Numbers List", displayMode: .inline) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /e2e_benchmarks/app/facebookwdae2e/facebookwdae2e/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /e2e_benchmarks/app/facebookwdae2e/facebookwdae2e/facebookwdae2eApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // facebookwdae2eApp.swift 3 | // facebookwdae2e 4 | // 5 | // Created by youngfreefjs on 2024/3/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct facebookwdae2eApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /e2e_benchmarks/app/facebookwdae2e/facebookwdae2eTests/facebookwdae2eTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // facebookwdae2eTests.swift 3 | // facebookwdae2eTests 4 | // 5 | // Created by youngfreefjs on 2024/3/20. 6 | // 7 | 8 | import XCTest 9 | @testable import facebookwdae2e 10 | 11 | final class facebookwdae2eTests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | // Any test you write for XCTest can be annotated as throws and async. 25 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. 26 | // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. 27 | } 28 | 29 | func testPerformanceExample() throws { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /e2e_benchmarks/app/facebookwdae2e/facebookwdae2eUITests/facebookwdae2eUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // facebookwdae2eUITests.swift 3 | // facebookwdae2eUITests 4 | // 5 | // Created by youngfreefjs on 2024/3/20. 6 | // 7 | 8 | import XCTest 9 | 10 | final class facebookwdae2eUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | func testExample() throws { 26 | // UI tests must launch the application that they test. 27 | let app = XCUIApplication() 28 | app.launch() 29 | 30 | // Use XCTAssert and related functions to verify your tests produce the correct results. 31 | } 32 | 33 | func testLaunchPerformance() throws { 34 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { 35 | // This measures how long it takes to launch your application. 36 | measure(metrics: [XCTApplicationLaunchMetric()]) { 37 | XCUIApplication().launch() 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /e2e_benchmarks/app/facebookwdae2e/facebookwdae2eUITests/facebookwdae2eUITestsLaunchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // facebookwdae2eUITestsLaunchTests.swift 3 | // facebookwdae2eUITests 4 | // 5 | // Created by youngfreefjs on 2024/3/20. 6 | // 7 | 8 | import XCTest 9 | 10 | final class facebookwdae2eUITestsLaunchTests: XCTestCase { 11 | 12 | override class var runsForEachTargetApplicationUIConfiguration: Bool { 13 | true 14 | } 15 | 16 | override func setUpWithError() throws { 17 | continueAfterFailure = false 18 | } 19 | 20 | func testLaunch() throws { 21 | let app = XCUIApplication() 22 | app.launch() 23 | 24 | // Insert steps here to perform after app launch but before taking a screenshot, 25 | // such as logging into a test account or navigating somewhere in the app 26 | 27 | let attachment = XCTAttachment(screenshot: app.screenshot()) 28 | attachment.name = "Launch Screen" 29 | attachment.lifetime = .keepAlways 30 | add(attachment) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /e2e_benchmarks/constant.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | UNDER_TEST_BUNDLE_ID = "com.test.cert.TestCert" 5 | 6 | UNKNOW_BUNDLE_ID = "com.apple.unknown" 7 | 8 | HOME_SCREEN_BUNDLE_ID = "com.apple.springboard" 9 | 10 | class ALERT_ENUMS(Enum): 11 | CONFIRM = "ACCEPT_OR_REJECT_ALERT" 12 | INPUT = "INPUT_ALERT" 13 | 14 | 15 | -------------------------------------------------------------------------------- /e2e_benchmarks/reports/WDA_V611.md: -------------------------------------------------------------------------------- 1 | # WDA Server And Client Benchmark E2E Testing Report (V7.1.1) 2 | 3 | ## Overview 4 | 61 passed, 17 skipped, 1 warning in 172.92s (0:02:52) 5 | 6 | ## 1. Test Environments 7 | - WDA Version: [version 6.1.1 release by Appium.](https://github.com/appium/WebDriverAgent/releases/tag/v6.1.1) 8 | - Platform: iPhone 13 Pro (iOS 16.3.1) 9 | 10 | ## 2. Test Results 11 | platform darwin -- Python 3.7.9, pytest-7.4.4, pluggy-1.2.0 12 | collected 78 items 13 | 14 | e2e_benchmarks/test_alert_commands.py::TestAlert::test_alert_text_accept_endpoint PASSED [ 1%] 15 | e2e_benchmarks/test_alert_commands.py::TestAlert::test_alert_text_button_endpoint_value PASSED [ 2%] 16 | e2e_benchmarks/test_alert_commands.py::TestAlert::test_alert_text_dimiss_endpoint PASSED [ 3%] 17 | e2e_benchmarks/test_alert_commands.py::TestAlert::test_alert_text_endpoint_confirmation PASSED [ 5%] 18 | e2e_benchmarks/test_alert_commands.py::TestAlert::test_alert_text_endpoint_when_no_alert PASSED [ 6%] 19 | e2e_benchmarks/test_alert_commands.py::TestAlert::test_alert_text_input PASSED [ 7%] 20 | e2e_benchmarks/test_custom_commands.py::TestDevice::test_active_app_info PASSED [ 8%] 21 | e2e_benchmarks/test_custom_commands.py::TestDevice::test_battery_info PASSED [ 10%] 22 | e2e_benchmarks/test_custom_commands.py::TestDevice::test_deactivate_app PASSED [ 11%] 23 | e2e_benchmarks/test_custom_commands.py::TestDevice::test_device_info PASSED [ 12%] 24 | e2e_benchmarks/test_custom_commands.py::TestDevice::test_get_paste_board SKIPPED (WDA API NOT USEFUL: {{baseURL}}/session/{{sessionId}}/wda/getPasteboard) [ 14%] 25 | e2e_benchmarks/test_custom_commands.py::TestDevice::test_homescreen PASSED [ 15%] 26 | e2e_benchmarks/test_custom_commands.py::TestDevice::test_keybord_dismiss PASSED [ 16%] 27 | e2e_benchmarks/test_custom_commands.py::TestDevice::test_keybord_lock_and_unlock PASSED [ 17%] 28 | e2e_benchmarks/test_custom_commands.py::TestDevice::test_locked PASSED [ 19%] 29 | e2e_benchmarks/test_custom_commands.py::TestDevice::test_scale PASSED [ 20%] 30 | e2e_benchmarks/test_custom_commands.py::TestDevice::test_set_paste_board PASSED [ 21%] 31 | e2e_benchmarks/test_custom_commands.py::TestDevice::test_timeouts PASSED [ 23%] 32 | e2e_benchmarks/test_debug_commands.py::TestDebug::test_accessible_source PASSED [ 24%] 33 | e2e_benchmarks/test_debug_commands.py::TestDebug::test_source PASSED [ 25%] 34 | e2e_benchmarks/test_element_commands.py::TestElement::test_app_drag_from_to_for_duration PASSED [ 26%] 35 | e2e_benchmarks/test_element_commands.py::TestElement::test_attribute_btn_displayed PASSED [ 28%] 36 | e2e_benchmarks/test_element_commands.py::TestElement::test_attribute_btn_not_displayed PASSED [ 29%] 37 | e2e_benchmarks/test_element_commands.py::TestElement::test_attribute_btn_text PASSED [ 30%] 38 | e2e_benchmarks/test_element_commands.py::TestElement::test_attribute_image_text PASSED [ 32%] 39 | e2e_benchmarks/test_element_commands.py::TestElement::test_attribute_invild PASSED [ 33%] 40 | e2e_benchmarks/test_element_commands.py::TestElement::test_attribute_label PASSED [ 34%] 41 | e2e_benchmarks/test_element_commands.py::TestElement::test_btn_enable PASSED [ 35%] 42 | e2e_benchmarks/test_element_commands.py::TestElement::test_btn_name PASSED [ 37%] 43 | e2e_benchmarks/test_element_commands.py::TestElement::test_btn_selected PASSED [ 38%] 44 | e2e_benchmarks/test_element_commands.py::TestElement::test_drag_from_to_for_duration SKIPPED (NOT IMPLEMENTED: [POST] {{baseURL}}/session/{{sessionId}}/wda/element/{{uuid}}/dragfromtoforduration) [ 39%] 45 | e2e_benchmarks/test_element_commands.py::TestElement::test_ele_accessibilityContainer_is_false PASSED [ 41%] 46 | e2e_benchmarks/test_element_commands.py::TestElement::test_ele_accessibilityContainer_is_true SKIPPED (WDA API NOT USEFUL: Not use in SwiftUI.) [ 42%] 47 | e2e_benchmarks/test_element_commands.py::TestElement::test_ele_accessible_is_false PASSED [ 43%] 48 | e2e_benchmarks/test_element_commands.py::TestElement::test_ele_accessible_is_true PASSED [ 44%] 49 | e2e_benchmarks/test_element_commands.py::TestElement::test_ele_force_touch SKIPPED (NOT IMPLEMENTED: [POST] {{baseURL}}/session/{{sessionId}}/wda/element/{{uuid}}/forceTouch) [ 46%] 50 | e2e_benchmarks/test_element_commands.py::TestElement::test_ele_select SKIPPED (NOT IMPLEMENTED: [POST] {{baseURL}}/session/{{sessionId}}/wda/pickerwheel/{{uuid}}/select) [ 47%] 51 | e2e_benchmarks/test_element_commands.py::TestElement::test_ele_swipe_top SKIPPED (NOT IMPLEMENTED) [ 48%] 52 | e2e_benchmarks/test_element_commands.py::TestElement::test_ele_touble_tap PASSED [ 50%] 53 | e2e_benchmarks/test_element_commands.py::TestElement::test_img_name PASSED [ 51%] 54 | e2e_benchmarks/test_element_commands.py::TestElement::test_input_clear PASSED [ 52%] 55 | e2e_benchmarks/test_element_commands.py::TestElement::test_input_click PASSED [ 53%] 56 | e2e_benchmarks/test_element_commands.py::TestElement::test_input_text PASSED [ 55%] 57 | e2e_benchmarks/test_element_commands.py::TestElement::test_pinch SKIPPED (NOT IMPLEMENTED: [POST] {{baseURL}}/session/{{sessionId}}/wda/element/{{uuid}}/pinch) [ 56%] 58 | e2e_benchmarks/test_element_commands.py::TestElement::test_rect PASSED [ 57%] 59 | e2e_benchmarks/test_element_commands.py::TestElement::test_screenshot PASSED [ 58%] 60 | e2e_benchmarks/test_element_commands.py::TestElement::test_screenshot_element SKIPPED (NOT IMPLEMENTED: [POST] {{baseURL}}/session/{{sessionId}}/screenshot/{{uuid}}) [ 60%] 61 | e2e_benchmarks/test_element_commands.py::TestElement::test_touch_and_hold PASSED [ 61%] 62 | e2e_benchmarks/test_element_commands.py::TestElement::test_wda_keys PASSED [ 62%] 63 | e2e_benchmarks/test_element_commands.py::TestElement::test_wda_touch_and_hold PASSED [ 64%] 64 | e2e_benchmarks/test_element_commands.py::TestElement::test_window_size PASSED [ 65%] 65 | e2e_benchmarks/test_find_element_commands.py::TestFindElement::test_ele_elements SKIPPED (NOT IMPLEMENTED: [GET] /session/{{sessionId}}/element/{{uuid}}/elements) [ 66%] 66 | e2e_benchmarks/test_find_element_commands.py::TestFindElement::test_element_ids PASSED [ 67%] 67 | e2e_benchmarks/test_find_element_commands.py::TestFindElement::test_elements PASSED [ 69%] 68 | e2e_benchmarks/test_find_element_commands.py::TestFindElement::test_wda_active SKIPPED (NOT IMPLEMENTED: [GET] {{baseURL}}/session/{{sessionId}}/element/active) [ 70%] 69 | e2e_benchmarks/test_find_element_commands.py::TestFindElement::test_wda_element SKIPPED (NOT IMPLEMENTED: [GET] {{baseURL}}/session/{{sessionId}}/wda/element/{{uuid}}/getVisibleCells) [ 71%] 70 | e2e_benchmarks/test_orientation_commands.py::TestOrientation::test_get_rotation SKIPPED (NOT IMPLEMENTED: [GET] {{baseURL}}/session/{{sessionId}}/rotation) [ 73%] 71 | e2e_benchmarks/test_orientation_commands.py::TestOrientation::test_orientation PASSED [ 74%] 72 | e2e_benchmarks/test_orientation_commands.py::TestOrientation::test_set_orientation PASSED [ 75%] 73 | e2e_benchmarks/test_orientation_commands.py::TestOrientation::test_set_rotation SKIPPED (NOT IMPLEMENTED: [POST] {{baseURL}}/session/{{sessionId}}/rotation) [ 76%] 74 | e2e_benchmarks/test_screenshot_commands.py::TestScreenshot::test_screenshot PASSED [ 78%] 75 | e2e_benchmarks/test_session_commands.py::TestSessionCommands::test_app_activate PASSED [ 79%] 76 | e2e_benchmarks/test_session_commands.py::TestSessionCommands::test_app_list PASSED [ 80%] 77 | e2e_benchmarks/test_session_commands.py::TestSessionCommands::test_app_state PASSED [ 82%] 78 | e2e_benchmarks/test_session_commands.py::TestSessionCommands::test_app_terminate PASSED [ 83%] 79 | e2e_benchmarks/test_session_commands.py::TestSessionCommands::test_appium_settings PASSED [ 84%] 80 | e2e_benchmarks/test_session_commands.py::TestSessionCommands::test_create_session_id PASSED [ 85%] 81 | e2e_benchmarks/test_session_commands.py::TestSessionCommands::test_delete_session PASSED [ 87%] 82 | e2e_benchmarks/test_session_commands.py::TestSessionCommands::test_health SKIPPED (NOT IMPLEMENTED: [GET] {{baseURL}}/health) [ 88%] 83 | e2e_benchmarks/test_session_commands.py::TestSessionCommands::test_health_check PASSED [ 89%] 84 | e2e_benchmarks/test_session_commands.py::TestSessionCommands::test_health_check_has_session_id PASSED [ 91%] 85 | e2e_benchmarks/test_session_commands.py::TestSessionCommands::test_launch_app PASSED [ 92%] 86 | e2e_benchmarks/test_session_commands.py::TestSessionCommands::test_session_info SKIPPED (NOT IMPLEMENTED: [GET] {{baseURL}}/session/{{sessionId}}) [ 93%] 87 | e2e_benchmarks/test_session_commands.py::TestSessionCommands::test_status_command_return_schema PASSED [ 94%] 88 | e2e_benchmarks/test_session_commands.py::TestSessionCommands::test_status_command_state_and_ready PASSED [ 96%] 89 | e2e_benchmarks/test_session_commands.py::TestSessionCommands::test_unknow_app_state PASSED [ 97%] 90 | e2e_benchmarks/test_session_commands.py::TestSessionCommands::test_url SKIPPED (UNKNOW HOW TO TEST: [POST] {{baseURL}}/session/{{sessionId}}/url) [ 98%] 91 | e2e_benchmarks/test_session_commands.py::TestSessionCommands::test_wda_shutdown SKIPPED (NOT IMPLEMENTED: [GET] {{baseURL}}/wda/shutdown) [100%] 92 | 93 | =========================================================================================================== warnings summary ============================================================================================================ 94 | e2e_benchmarks/test_debug_commands.py::TestDebug::test_source 95 | /Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/case.py:703: FutureWarning: The behavior of this method will change in future versions. Use specific 'len(elem)' or 'elem is not None' test instead. 96 | if not expr: 97 | 98 | -- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html 99 | ======================================================================================================== short test summary info ======================================================================================================== 100 | SKIPPED [1] e2e_benchmarks/test_custom_commands.py:127: WDA API NOT USEFUL: {{baseURL}}/session/{{sessionId}}/wda/getPasteboard 101 | SKIPPED [1] e2e_benchmarks/test_element_commands.py:256: NOT IMPLEMENTED: [POST] {{baseURL}}/session/{{sessionId}}/wda/element/{{uuid}}/dragfromtoforduration 102 | SKIPPED [1] e2e_benchmarks/test_element_commands.py:212: WDA API NOT USEFUL: Not use in SwiftUI. 103 | SKIPPED [1] e2e_benchmarks/test_element_commands.py:299: NOT IMPLEMENTED: [POST] {{baseURL}}/session/{{sessionId}}/wda/element/{{uuid}}/forceTouch 104 | SKIPPED [1] e2e_benchmarks/test_element_commands.py:280: NOT IMPLEMENTED: [POST] {{baseURL}}/session/{{sessionId}}/wda/pickerwheel/{{uuid}}/select 105 | SKIPPED [1] e2e_benchmarks/test_element_commands.py:224: NOT IMPLEMENTED 106 | SKIPPED [1] e2e_benchmarks/test_element_commands.py:234: NOT IMPLEMENTED: [POST] {{baseURL}}/session/{{sessionId}}/wda/element/{{uuid}}/pinch 107 | SKIPPED [1] e2e_benchmarks/test_element_commands.py:168: NOT IMPLEMENTED: [POST] {{baseURL}}/session/{{sessionId}}/screenshot/{{uuid}} 108 | SKIPPED [1] e2e_benchmarks/test_find_element_commands.py:51: NOT IMPLEMENTED: [GET] /session/{{sessionId}}/element/{{uuid}}/elements 109 | SKIPPED [1] e2e_benchmarks/test_find_element_commands.py:87: NOT IMPLEMENTED: [GET] {{baseURL}}/session/{{sessionId}}/element/active 110 | SKIPPED [1] e2e_benchmarks/test_find_element_commands.py:76: NOT IMPLEMENTED: [GET] {{baseURL}}/session/{{sessionId}}/wda/element/{{uuid}}/getVisibleCells 111 | SKIPPED [1] e2e_benchmarks/test_orientation_commands.py:54: NOT IMPLEMENTED: [GET] {{baseURL}}/session/{{sessionId}}/rotation 112 | SKIPPED [1] e2e_benchmarks/test_orientation_commands.py:63: NOT IMPLEMENTED: [POST] {{baseURL}}/session/{{sessionId}}/rotation 113 | SKIPPED [1] e2e_benchmarks/test_session_commands.py:256: NOT IMPLEMENTED: [GET] {{baseURL}}/health 114 | SKIPPED [1] e2e_benchmarks/test_session_commands.py:140: NOT IMPLEMENTED: [GET] {{baseURL}}/session/{{sessionId}} 115 | SKIPPED [1] e2e_benchmarks/test_session_commands.py:33: UNKNOW HOW TO TEST: [POST] {{baseURL}}/session/{{sessionId}}/url 116 | SKIPPED [1] e2e_benchmarks/test_session_commands.py:247: NOT IMPLEMENTED: [GET] {{baseURL}}/wda/shutdown 117 | ========================================================================================= 61 passed, 17 skipped, 1 warning in 172.92s (0:02:52) ========================================================================================= -------------------------------------------------------------------------------- /e2e_benchmarks/reports/WDA_V711.md: -------------------------------------------------------------------------------- 1 | # WDA Server And Client Benchmark E2E Testing Report (V7.1.1) 2 | 3 | ## Overview 4 | 61 passed, 17 skipped, 1 warning in 173.98s (0:02:53) 5 | 6 | ## 1. Test Environments 7 | - WDA Version: [version 7.1.1 release by Appium.](https://github.com/appium/WebDriverAgent/releases/tag/v7.1.1) 8 | - Platform: iPhone 13 Pro (iOS 16.3.1) 9 | 10 | ## 2. Test Results 11 | platform darwin -- Python 3.7.9, pytest-7.4.4, pluggy-1.2.0 12 | 13 | collected 78 items 14 | 15 | e2e_benchmarks/test_alert_commands.py::TestAlert::test_alert_text_accept_endpoint PASSED [ 1%] 16 | e2e_benchmarks/test_alert_commands.py::TestAlert::test_alert_text_button_endpoint_value PASSED [ 2%] 17 | e2e_benchmarks/test_alert_commands.py::TestAlert::test_alert_text_dimiss_endpoint PASSED [ 3%] 18 | e2e_benchmarks/test_alert_commands.py::TestAlert::test_alert_text_endpoint_confirmation PASSED [ 5%] 19 | e2e_benchmarks/test_alert_commands.py::TestAlert::test_alert_text_endpoint_when_no_alert PASSED [ 6%] 20 | e2e_benchmarks/test_alert_commands.py::TestAlert::test_alert_text_input PASSED [ 7%] 21 | e2e_benchmarks/test_custom_commands.py::TestDevice::test_active_app_info PASSED [ 8%] 22 | e2e_benchmarks/test_custom_commands.py::TestDevice::test_battery_info PASSED [ 10%] 23 | e2e_benchmarks/test_custom_commands.py::TestDevice::test_deactivate_app PASSED [ 11%] 24 | e2e_benchmarks/test_custom_commands.py::TestDevice::test_device_info PASSED [ 12%] 25 | e2e_benchmarks/test_custom_commands.py::TestDevice::test_get_paste_board SKIPPED (WDA API NOT USEFUL: {{baseURL}}/session/{{sessionId}}/wda/getPasteboard) [ 14%] 26 | e2e_benchmarks/test_custom_commands.py::TestDevice::test_homescreen PASSED [ 15%] 27 | e2e_benchmarks/test_custom_commands.py::TestDevice::test_keybord_dismiss PASSED [ 16%] 28 | e2e_benchmarks/test_custom_commands.py::TestDevice::test_keybord_lock_and_unlock PASSED [ 17%] 29 | e2e_benchmarks/test_custom_commands.py::TestDevice::test_locked PASSED [ 19%] 30 | e2e_benchmarks/test_custom_commands.py::TestDevice::test_scale PASSED [ 20%] 31 | e2e_benchmarks/test_custom_commands.py::TestDevice::test_set_paste_board PASSED [ 21%] 32 | e2e_benchmarks/test_custom_commands.py::TestDevice::test_timeouts PASSED [ 23%] 33 | e2e_benchmarks/test_debug_commands.py::TestDebug::test_accessible_source PASSED [ 24%] 34 | e2e_benchmarks/test_debug_commands.py::TestDebug::test_source PASSED [ 25%] 35 | e2e_benchmarks/test_element_commands.py::TestElement::test_app_drag_from_to_for_duration PASSED [ 26%] 36 | e2e_benchmarks/test_element_commands.py::TestElement::test_attribute_btn_displayed PASSED [ 28%] 37 | e2e_benchmarks/test_element_commands.py::TestElement::test_attribute_btn_not_displayed PASSED [ 29%] 38 | e2e_benchmarks/test_element_commands.py::TestElement::test_attribute_btn_text PASSED [ 30%] 39 | e2e_benchmarks/test_element_commands.py::TestElement::test_attribute_image_text PASSED [ 32%] 40 | e2e_benchmarks/test_element_commands.py::TestElement::test_attribute_invild PASSED [ 33%] 41 | e2e_benchmarks/test_element_commands.py::TestElement::test_attribute_label PASSED [ 34%] 42 | e2e_benchmarks/test_element_commands.py::TestElement::test_btn_enable PASSED [ 35%] 43 | e2e_benchmarks/test_element_commands.py::TestElement::test_btn_name PASSED [ 37%] 44 | e2e_benchmarks/test_element_commands.py::TestElement::test_btn_selected PASSED [ 38%] 45 | e2e_benchmarks/test_element_commands.py::TestElement::test_drag_from_to_for_duration SKIPPED (NOT IMPLEMENTED: [POST] {{baseURL}}/session/{{sessionId}}/wda/element/{{uuid}}...) [ 39%] 46 | e2e_benchmarks/test_element_commands.py::TestElement::test_ele_accessibilityContainer_is_false PASSED [ 41%] 47 | e2e_benchmarks/test_element_commands.py::TestElement::test_ele_accessibilityContainer_is_true SKIPPED (WDA API NOT USEFUL: Not use in SwiftUI.) [ 42%] 48 | e2e_benchmarks/test_element_commands.py::TestElement::test_ele_accessible_is_false PASSED [ 43%] 49 | e2e_benchmarks/test_element_commands.py::TestElement::test_ele_accessible_is_true PASSED [ 44%] 50 | e2e_benchmarks/test_element_commands.py::TestElement::test_ele_force_touch SKIPPED (NOT IMPLEMENTED: [POST] {{baseURL}}/session/{{sessionId}}/wda/element/{{uuid}}/forceTouch) [ 46%] 51 | e2e_benchmarks/test_element_commands.py::TestElement::test_ele_select SKIPPED (NOT IMPLEMENTED: [POST] {{baseURL}}/session/{{sessionId}}/wda/pickerwheel/{{uuid}}/select) [ 47%] 52 | e2e_benchmarks/test_element_commands.py::TestElement::test_ele_swipe_top SKIPPED (NOT IMPLEMENTED) [ 48%] 53 | e2e_benchmarks/test_element_commands.py::TestElement::test_ele_touble_tap PASSED [ 50%] 54 | e2e_benchmarks/test_element_commands.py::TestElement::test_img_name PASSED [ 51%] 55 | e2e_benchmarks/test_element_commands.py::TestElement::test_input_clear PASSED [ 52%] 56 | e2e_benchmarks/test_element_commands.py::TestElement::test_input_click PASSED [ 53%] 57 | e2e_benchmarks/test_element_commands.py::TestElement::test_input_text PASSED [ 55%] 58 | e2e_benchmarks/test_element_commands.py::TestElement::test_pinch SKIPPED (NOT IMPLEMENTED: [POST] {{baseURL}}/session/{{sessionId}}/wda/element/{{uuid}}/pinch) [ 56%] 59 | e2e_benchmarks/test_element_commands.py::TestElement::test_rect PASSED [ 57%] 60 | e2e_benchmarks/test_element_commands.py::TestElement::test_screenshot PASSED [ 58%] 61 | e2e_benchmarks/test_element_commands.py::TestElement::test_screenshot_element SKIPPED (NOT IMPLEMENTED: [POST] {{baseURL}}/session/{{sessionId}}/screenshot/{{uuid}}) [ 60%] 62 | e2e_benchmarks/test_element_commands.py::TestElement::test_touch_and_hold PASSED [ 61%] 63 | e2e_benchmarks/test_element_commands.py::TestElement::test_wda_keys PASSED [ 62%] 64 | e2e_benchmarks/test_element_commands.py::TestElement::test_wda_touch_and_hold PASSED [ 64%] 65 | e2e_benchmarks/test_element_commands.py::TestElement::test_window_size PASSED [ 65%] 66 | e2e_benchmarks/test_find_element_commands.py::TestFindElement::test_ele_elements SKIPPED (NOT IMPLEMENTED: [GET] /session/{{sessionId}}/element/{{uuid}}/elements) [ 66%] 67 | e2e_benchmarks/test_find_element_commands.py::TestFindElement::test_element_ids PASSED [ 67%] 68 | e2e_benchmarks/test_find_element_commands.py::TestFindElement::test_elements PASSED [ 69%] 69 | e2e_benchmarks/test_find_element_commands.py::TestFindElement::test_wda_active SKIPPED (NOT IMPLEMENTED: [GET] {{baseURL}}/session/{{sessionId}}/element/active) [ 70%] 70 | e2e_benchmarks/test_find_element_commands.py::TestFindElement::test_wda_element SKIPPED (NOT IMPLEMENTED: [GET] {{baseURL}}/session/{{sessionId}}/wda/element/{{uuid}}/getVi...) [ 71%] 71 | e2e_benchmarks/test_orientation_commands.py::TestOrientation::test_get_rotation SKIPPED (NOT IMPLEMENTED: [GET] {{baseURL}}/session/{{sessionId}}/rotation) [ 73%] 72 | e2e_benchmarks/test_orientation_commands.py::TestOrientation::test_orientation PASSED [ 74%] 73 | e2e_benchmarks/test_orientation_commands.py::TestOrientation::test_set_orientation PASSED [ 75%] 74 | e2e_benchmarks/test_orientation_commands.py::TestOrientation::test_set_rotation SKIPPED (NOT IMPLEMENTED: [POST] {{baseURL}}/session/{{sessionId}}/rotation) [ 76%] 75 | e2e_benchmarks/test_screenshot_commands.py::TestScreenshot::test_screenshot PASSED [ 78%] 76 | e2e_benchmarks/test_session_commands.py::TestSessionCommands::test_app_activate PASSED [ 79%] 77 | e2e_benchmarks/test_session_commands.py::TestSessionCommands::test_app_list PASSED [ 80%] 78 | e2e_benchmarks/test_session_commands.py::TestSessionCommands::test_app_state PASSED [ 82%] 79 | e2e_benchmarks/test_session_commands.py::TestSessionCommands::test_app_terminate PASSED [ 83%] 80 | e2e_benchmarks/test_session_commands.py::TestSessionCommands::test_appium_settings PASSED [ 84%] 81 | e2e_benchmarks/test_session_commands.py::TestSessionCommands::test_create_session_id PASSED [ 85%] 82 | e2e_benchmarks/test_session_commands.py::TestSessionCommands::test_delete_session PASSED [ 87%] 83 | e2e_benchmarks/test_session_commands.py::TestSessionCommands::test_health SKIPPED (NOT IMPLEMENTED: [GET] {{baseURL}}/health) [ 88%] 84 | e2e_benchmarks/test_session_commands.py::TestSessionCommands::test_health_check PASSED [ 89%] 85 | e2e_benchmarks/test_session_commands.py::TestSessionCommands::test_health_check_has_session_id PASSED [ 91%] 86 | e2e_benchmarks/test_session_commands.py::TestSessionCommands::test_launch_app PASSED [ 92%] 87 | e2e_benchmarks/test_session_commands.py::TestSessionCommands::test_session_info SKIPPED (NOT IMPLEMENTED: [GET] {{baseURL}}/session/{{sessionId}}) [ 93%] 88 | e2e_benchmarks/test_session_commands.py::TestSessionCommands::test_status_command_return_schema PASSED [ 94%] 89 | e2e_benchmarks/test_session_commands.py::TestSessionCommands::test_status_command_state_and_ready PASSED [ 96%] 90 | e2e_benchmarks/test_session_commands.py::TestSessionCommands::test_unknow_app_state PASSED [ 97%] 91 | e2e_benchmarks/test_session_commands.py::TestSessionCommands::test_url SKIPPED (UNKNOW HOW TO TEST: [POST] {{baseURL}}/session/{{sessionId}}/url) [ 98%] 92 | e2e_benchmarks/test_session_commands.py::TestSessionCommands::test_wda_shutdown SKIPPED (NOT IMPLEMENTED: [GET] {{baseURL}}/wda/shutdown) [100%] 93 | 94 | =================================================================================== warnings summary =================================================================================== 95 | e2e_benchmarks/test_debug_commands.py::TestDebug::test_source 96 | /Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/case.py:703: FutureWarning: The behavior of this method will change in future versions. Use specific 'len(elem)' or 'elem is not None' test instead. 97 | if not expr: 98 | 99 | -- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html 100 | =============================================================================== short test summary info ================================================================================ 101 | SKIPPED [1] e2e_benchmarks/test_custom_commands.py:127: WDA API NOT USEFUL: {{baseURL}}/session/{{sessionId}}/wda/getPasteboard 102 | SKIPPED [1] e2e_benchmarks/test_element_commands.py:256: NOT IMPLEMENTED: [POST] {{baseURL}}/session/{{sessionId}}/wda/element/{{uuid}}/dragfromtoforduration 103 | SKIPPED [1] e2e_benchmarks/test_element_commands.py:212: WDA API NOT USEFUL: Not use in SwiftUI. 104 | SKIPPED [1] e2e_benchmarks/test_element_commands.py:299: NOT IMPLEMENTED: [POST] {{baseURL}}/session/{{sessionId}}/wda/element/{{uuid}}/forceTouch 105 | SKIPPED [1] e2e_benchmarks/test_element_commands.py:280: NOT IMPLEMENTED: [POST] {{baseURL}}/session/{{sessionId}}/wda/pickerwheel/{{uuid}}/select 106 | SKIPPED [1] e2e_benchmarks/test_element_commands.py:224: NOT IMPLEMENTED 107 | SKIPPED [1] e2e_benchmarks/test_element_commands.py:234: NOT IMPLEMENTED: [POST] {{baseURL}}/session/{{sessionId}}/wda/element/{{uuid}}/pinch 108 | SKIPPED [1] e2e_benchmarks/test_element_commands.py:168: NOT IMPLEMENTED: [POST] {{baseURL}}/session/{{sessionId}}/screenshot/{{uuid}} 109 | SKIPPED [1] e2e_benchmarks/test_find_element_commands.py:51: NOT IMPLEMENTED: [GET] /session/{{sessionId}}/element/{{uuid}}/elements 110 | SKIPPED [1] e2e_benchmarks/test_find_element_commands.py:87: NOT IMPLEMENTED: [GET] {{baseURL}}/session/{{sessionId}}/element/active 111 | SKIPPED [1] e2e_benchmarks/test_find_element_commands.py:76: NOT IMPLEMENTED: [GET] {{baseURL}}/session/{{sessionId}}/wda/element/{{uuid}}/getVisibleCells 112 | SKIPPED [1] e2e_benchmarks/test_orientation_commands.py:54: NOT IMPLEMENTED: [GET] {{baseURL}}/session/{{sessionId}}/rotation 113 | SKIPPED [1] e2e_benchmarks/test_orientation_commands.py:63: NOT IMPLEMENTED: [POST] {{baseURL}}/session/{{sessionId}}/rotation 114 | SKIPPED [1] e2e_benchmarks/test_session_commands.py:256: NOT IMPLEMENTED: [GET] {{baseURL}}/health 115 | SKIPPED [1] e2e_benchmarks/test_session_commands.py:140: NOT IMPLEMENTED: [GET] {{baseURL}}/session/{{sessionId}} 116 | SKIPPED [1] e2e_benchmarks/test_session_commands.py:33: UNKNOW HOW TO TEST: [POST] {{baseURL}}/session/{{sessionId}}/url 117 | SKIPPED [1] e2e_benchmarks/test_session_commands.py:247: NOT IMPLEMENTED: [GET] {{baseURL}}/wda/shutdown 118 | ================================================================ 61 passed, 17 skipped, 1 warning in 173.98s (0:02:53) ================================================================= -------------------------------------------------------------------------------- /e2e_benchmarks/requirement.txt: -------------------------------------------------------------------------------- 1 | pytest==7.4.4 2 | jsonschema==4.17.3 3 | lxml==5.1.0 -------------------------------------------------------------------------------- /e2e_benchmarks/test_alert_commands.py: -------------------------------------------------------------------------------- 1 | ''' 2 | WDA ALERT Command Testing 3 | source code here: https://github.com/appium/WebDriverAgent/blob/master/WebDriverAgentLib/Commands/FBAlertViewCommands.m 4 | WDA API document and example(not offical): https://documenter.getpostman.com/view/1837823/TVmMhJNB#c6d397f5-8ece-4f2c-8bfd-26e798f83cf2 5 | ''' 6 | import os 7 | import pytest 8 | import unittest 9 | import wda 10 | from .constant import * 11 | 12 | curPath = os.path.abspath(os.path.dirname(__file__)) 13 | 14 | 15 | class TestAlert(unittest.TestCase): 16 | 17 | def setUp(self): 18 | self.under_test_bundle_id = UNDER_TEST_BUNDLE_ID 19 | self.wda_client: wda.Client = wda.Client() 20 | self.app = self.wda_client.session(bundle_id=self.under_test_bundle_id) 21 | 22 | def tearDown(self): 23 | self.wda_client.close() 24 | 25 | 26 | ''' 27 | Method: GET 28 | Endpoint: {{baseURL}}/alert/text 29 | Description: Get the content of the Alert (only the content, not including options), 30 | an exception(wda.exceptions.WDARequestError) will be thrown if no Alert is present. 31 | ''' 32 | 33 | def test_alert_text_endpoint_confirmation(self): 34 | self.app(label='ACCEPT_OR_REJECT_ALERT').click() 35 | self.assertEqual('Confirmation\nDo you accept?', self.wda_client.alert.text) 36 | 37 | def test_alert_text_endpoint_when_no_alert(self): 38 | with pytest.raises(wda.exceptions.WDARequestError, match="status=110, value={'error': \'no such alert', "\ 39 | "'message': 'An attempt was made to operate on a modal dialog when one was not open'}"): 40 | self.wda_client.alert.text 41 | 42 | 43 | ''' 44 | Method: POST 45 | Endpoint: {{baseURL}}/session/{{sessionId}}/alert/text 46 | ''' 47 | def test_alert_text_input(self): 48 | with pytest.raises(wda.exceptions.WDARequestError, match="status=110, value={'error': \'no such alert', "\ 49 | "'message': 'An attempt was made to operate on a modal dialog when one was not open'}"): 50 | self.wda_client.alert.set_text('hello world') 51 | 52 | 53 | '''1 54 | Method: GET 55 | Endpoint: {{baseURL}}/session/{{sessionId}}/wda/alert/buttons 56 | Description: Get buttons for all prompt alert buttons. 57 | ''' 58 | def test_alert_text_button_endpoint_value(self): 59 | self.app(label=ALERT_ENUMS.CONFIRM.value).click() 60 | self.assertEqual(['Reject', 'Accept'], self.wda_client.alert.buttons()) 61 | 62 | 63 | ''' 64 | Method: POST 65 | Endpoint: {{baseURL}}/alert/dismiss 66 | Description: Dimiss the alert which display. 67 | ''' 68 | def test_alert_text_dimiss_endpoint(self): 69 | self.app(label=ALERT_ENUMS.CONFIRM.value).click() 70 | self.app(text='Reject').get(timeout=1) 71 | self.wda_client.alert.dismiss() 72 | try: 73 | self.app(text='Reject').get(timeout=1) 74 | except wda.exceptions.WDAElementNotFoundError: 75 | return 76 | raise AssertionError('Alert not dismissed') 77 | 78 | 79 | ''' 80 | Method: POST 81 | Endpoint: {{baseURL}}/alert/accept 82 | Description: Accept the alert which display. 83 | ''' 84 | def test_alert_text_accept_endpoint(self): 85 | self.app(label=ALERT_ENUMS.CONFIRM.value).click() 86 | self.app.alert.accept() 87 | try: 88 | self.app(text='Accept').get(timeout=1) 89 | except wda.exceptions.WDAElementNotFoundError: 90 | return 91 | raise AssertionError('Alert not dismissed') 92 | -------------------------------------------------------------------------------- /e2e_benchmarks/test_custom_commands.py: -------------------------------------------------------------------------------- 1 | ''' 2 | WDA Custom Command Testing 3 | source code here: https://github.com/appium/WebDriverAgent/blob/master/WebDriverAgentLib/Commands/FBCustomCommands.m#L38 4 | WDA API document and example(not offical): https://documenter.getpostman.com/view/1837823/TVmMhJNB#573d11c5-c434-4753-b845-4a3afcc4b614 5 | ''' 6 | import os 7 | import pytest 8 | import unittest 9 | import jsonschema 10 | import wda 11 | from .constant import * 12 | 13 | curPath = os.path.abspath(os.path.dirname(__file__)) 14 | 15 | 16 | class TestDevice(unittest.TestCase): 17 | 18 | def setUp(self): 19 | self.under_test_bundle_id = UNDER_TEST_BUNDLE_ID 20 | self.wda_client: wda.Client = wda.Client() 21 | self.app = self.wda_client.session(bundle_id=self.under_test_bundle_id) 22 | 23 | def tearDown(self): 24 | self.wda_client.close() 25 | 26 | ''' 27 | Method: POST 28 | Endpoint: {{baseURL}}/session/{{sessionId}}/wda/deactivateApp 29 | Description: Put app into background and than put it back. 30 | ''' 31 | def test_deactivate_app(self): 32 | self.app.deactivate(duration=1) 33 | 34 | 35 | ''' 36 | Method: POST 37 | Endpoint: {{baseURL}}/session/{{sessionId}}/wda/keyboard/dismiss 38 | Description: Put the keyboard into the background. 39 | ''' 40 | def test_keybord_dismiss(self): 41 | with pytest.raises(RuntimeError) as e: 42 | self.app.keyboard_dismiss() 43 | 44 | 45 | ''' 46 | Method: POST 47 | Endpoint: {{baseURL}}/wda/lock 48 | Endpoint: {{baseURL}}/wda/unlock 49 | Description: Lock the device. 50 | ''' 51 | def test_keybord_lock_and_unlock(self): 52 | self.app.lock() 53 | self.app.unlock() 54 | 55 | 56 | ''' 57 | Method: GET 58 | Endpoint: {{baseURL}}/session/{{sessionId}}/wda/screen 59 | Description: UIKit scale factor 60 | Refs: 61 | https://developer.apple.com/library/archive/documentation/DeviceInformation/Reference/iOSDeviceCompatibility/Displays/Displays.html 62 | There is another way to get scale 63 | self._session_http.get("/wda/screen").value returns {"statusBarSize": {'width': 320, 'height': 20}, 'scale': 2} 64 | ''' 65 | def test_scale(self): 66 | self.assertIsInstance(self.app.scale, int) 67 | 68 | 69 | ''' 70 | Method: GET 71 | Endpoint: {{baseURL}}/wda/activeAppInfo 72 | Description: Return bundleId pid and etc. like: 73 | {'processArguments': {'env': {}, 'args': []}, 'name': '', 'pid': 19052, 'bundleId': 'com.test.cert.TestCert'} 74 | ''' 75 | def test_active_app_info(self): 76 | except_json_schema = {"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties": 77 | {"processArguments":{"type":"object","properties":{"env":{"type":"object", 78 | "additionalProperties":False},"args":{"type":"array"}},"additionalProperties":False, 79 | "required":["env","args"]},"name":{"type":"string"},"pid":{"type":"integer"}, 80 | "bundleId":{"type":"string"}},"additionalProperties":False,"required": 81 | ["processArguments","name","pid","bundleId"]} 82 | self.assertTrue(jsonschema.Draft7Validator(except_json_schema).is_valid(self.app.app_current())) 83 | 84 | 85 | ''' 86 | Method: POST 87 | Endpoint: {{baseURL}}/session/{{sessionId}}/wda/setPasteboard 88 | Description: Set paste board board. 89 | ''' 90 | def test_set_paste_board(self): 91 | self.app.set_clipboard('test') 92 | 93 | ''' 94 | Method: POST 95 | Endpoint: {{baseURL}}/session/{{sessionId}}/wda/getPasteboard 96 | #NOTE: IS NOT USED. RETURN NULL. 97 | e.g: 98 | curl http://127.0.0.1:8100/session/3D28C745-5290-4787-948C-43C7A27E6146/wda/getPasteboard -X POST -v 99 | * Trying 127.0.0.1:8100... 100 | * Connected to 127.0.0.1 (127.0.0.1) port 8100 (#0) 101 | > POST /session/3D28C745-5290-4787-948C-43C7A27E6146/wda/getPasteboard HTTP/1.1 102 | > Host: 127.0.0.1:8100 103 | > User-Agent: curl/7.86.0 104 | > Accept: */* 105 | > 106 | * Mark bundle as not supporting multiuse 107 | < HTTP/1.1 400 Bad Request 108 | < Server: WebDriverAgent/1.0 109 | < Access-Control-Allow-Origin: * 110 | < Date: Thu, 21 Mar 2024 02:18:52 GMT 111 | < Access-Control-Allow-Headers: Content-Type, X-Requested-With 112 | < Accept-Ranges: bytes 113 | < Content-Length: 0 114 | < Connection: close 115 | < 116 | * Closing connection 0 117 | ''' 118 | @pytest.mark.skip('WDA API NOT USEFUL: {{baseURL}}/session/{{sessionId}}/wda/getPasteboard') 119 | def test_get_paste_board(self): 120 | '''Wait to PR merge: https://github.com/openatx/facebook-wda/pull/133/files''' 121 | ... 122 | 123 | 124 | ''' 125 | Method: GET 126 | Endpoint: {{baseURL}}/wda/device/info 127 | Description: Return device info. 128 | Example Return: 129 | {"timeZone": "GMT+0800", "currentLocale": "zh_CN", "model": "iPhone", "uuid": 130 | "25E3142B-303E-41FE-9F6A-2C303CB66FBC", "thermalState": 1, "userInterfaceIdiom": 0, 131 | "userInterfaceStyle": "light", "name": "iPhone", "isSimulator": False} 132 | ''' 133 | def test_device_info(self): 134 | expect_schema = {"$schema":"http://json-schema.org/draft-07/schema#","type":"object", 135 | "properties":{"timeZone":{"type":"string"},"currentLocale":{"type":"string"}, 136 | "model":{"type":"string"},"uuid":{"type":"string"},"thermalState":{"type":"integer"}, 137 | "userInterfaceIdiom":{"type":"integer"},"userInterfaceStyle":{"type":"string"}, 138 | "name":{"type":"string"},"isSimulator":{"type":"boolean"}},"additionalProperties":False, 139 | "required":["timeZone","currentLocale","model","uuid","thermalState","userInterfaceIdiom", 140 | "userInterfaceStyle","name","isSimulator"]} 141 | self.assertTrue(jsonschema.Draft7Validator(expect_schema).is_valid(self.app.device_info())) 142 | 143 | 144 | ''' 145 | Method: GET 146 | Endpoint: {{baseURL}}/session/{{sessionId}}/wda/batteryInfo 147 | Description: Return device battery info. 148 | Example Return: 149 | {"level": 0.5799999833106995, "state": 2} 150 | ''' 151 | def test_battery_info(self): 152 | import json 153 | except_json_schema = {"$schema":"http://json-schema.org/draft-07/schema#","type":"object", 154 | "properties":{"level":{"type":"number"},"state":{"type":"integer"}}, 155 | "additionalProperties":False,"required":["level","state"]} 156 | self.assertTrue(jsonschema.Draft7Validator(except_json_schema).is_valid(self.app.battery_info())) 157 | 158 | 159 | ''' 160 | Method: GET 161 | Endpoint: {{baseURL}}/wda/homescreen 162 | Description: back to home screen 163 | ''' 164 | def test_homescreen(self): 165 | self.assertEqual(self.app.app_current().get('bundleId'), UNDER_TEST_BUNDLE_ID) 166 | self.app.home() 167 | self.assertEqual(self.app.app_current().get('bundleId'), HOME_SCREEN_BUNDLE_ID) 168 | 169 | 170 | ''' 171 | Method: GET 172 | Endpoint: {{baseURL}}/wda/locked 173 | Description: check device is locked or not. 174 | ''' 175 | def test_locked(self): 176 | self.assertFalse(self.app.locked()) 177 | -------------------------------------------------------------------------------- /e2e_benchmarks/test_debug_commands.py: -------------------------------------------------------------------------------- 1 | ''' 2 | WDA DEBUG Command Testing 3 | source code here: https://github.com/appium/WebDriverAgent/blob/master/WebDriverAgentLib/Commands/FBDebugCommands.m 4 | WDA API document and example(not offical): https://documenter.getpostman.com/view/1837823/TVmMhJNB#a17feaf2-237e-4fe7-8e2f-75f132420d26 5 | ''' 6 | import os 7 | import pytest 8 | import unittest 9 | import jsonschema 10 | import wda 11 | from lxml import etree 12 | from .constant import * 13 | 14 | curPath = os.path.abspath(os.path.dirname(__file__)) 15 | 16 | #TODO : 更新断言在apprelease后 17 | 18 | class TestDebug(unittest.TestCase): 19 | 20 | def setUp(self): 21 | self.under_test_bundle_id = UNDER_TEST_BUNDLE_ID 22 | self.wda_client: wda.Client = wda.Client() 23 | self.app = self.wda_client.session(bundle_id=self.under_test_bundle_id) 24 | 25 | def tearDown(self): 26 | self.wda_client.close() 27 | 28 | 29 | ''' 30 | Method: GET 31 | Endpoint: {{baseURL}}/source 32 | Description: Fetch the source tree (DOM) of the current page. 33 | ''' 34 | def test_source(self): 35 | self.app(text='ListView').click() 36 | source_tree_xml_str: str = self.app.source() 37 | xml_bytes = source_tree_xml_str.encode('utf-8') 38 | self.assertTrue(etree.fromstring(xml_bytes)) 39 | 40 | 41 | ''' 42 | Method: GET 43 | Endpoint: {{baseURL}}/wda/accessibleSource 44 | ''' 45 | def test_accessible_source(self): 46 | self.app(text='ListView').click() 47 | source = self.app.source(accessible=True) 48 | self.assertIsInstance(source, dict) 49 | assert 'name' in source, "'name' field is missing" 50 | assert 'type' in source, "'type' field is missing" 51 | assert 'children' in source, "'children' field is missing" -------------------------------------------------------------------------------- /e2e_benchmarks/test_element_commands.py: -------------------------------------------------------------------------------- 1 | ''' 2 | WDA Element Command Testing 3 | source code here: https://github.com/appium/WebDriverAgent/blob/master/WebDriverAgentLib/Commands/FBElementCommands.m 4 | WDA API document and example(not offical): https://documenter.getpostman.com/view/1837823/TVmMhJNB#f68f7fd9-3a08-4a0b-9253-f1bedf0ae926 5 | ''' 6 | import os 7 | import pytest 8 | import unittest 9 | import wda 10 | from .constant import * 11 | 12 | curPath = os.path.abspath(os.path.dirname(__file__)) 13 | 14 | 15 | class TestElement(unittest.TestCase): 16 | 17 | def setUp(self): 18 | self.under_test_bundle_id = UNDER_TEST_BUNDLE_ID 19 | self.wda_client: wda.Client = wda.Client() 20 | self.app = self.wda_client.session(bundle_id=self.under_test_bundle_id) 21 | self.temp_file_pic = os.path.join(curPath, 'temp_file_pic.png') 22 | 23 | def tearDown(self): 24 | self.wda_client.close() 25 | [os.remove(temp_file) for temp_file in [self.temp_file_pic] if os.path.exists(temp_file)] 26 | 27 | 28 | ''' 29 | Method: GET 30 | Endpoint: {{baseURL}}/session/{{sessionId}}/window/size 31 | Description: Fetch device window size. 32 | ''' 33 | def test_window_size(self): 34 | assert hasattr(self.app.window_size(), 'width') and type(self.app.window_size().width) == int 35 | assert hasattr(self.app.window_size(), 'height') and type(self.app.window_size().height) == int 36 | 37 | 38 | ''' 39 | Method: GET 40 | Endpoint: {{baseURL}}/session/{{sessionId}}/element/{{uuid}}/enabled 41 | Description: Check element is enbaled or not. 42 | ''' 43 | def test_btn_enable(self): 44 | assert self.app(text='ENABLED_BTN').enabled == True 45 | 46 | def test_btn_enable(self): 47 | assert self.app(text='DISABLED_BTN').enabled == False 48 | 49 | 50 | ''' 51 | Method: GET 52 | Endpoint: {{baseURL}}/session/{{sessionId}}/element/{{uuid}}/rect 53 | ''' 54 | def test_rect(self): 55 | rect = self.app(text='ENABLED_BTN').bounds 56 | assert all([ 57 | hasattr(rect, 'x') and type(rect.x) == int, 58 | hasattr(rect, 'y') and type(rect.y) == int, 59 | hasattr(rect, 'width') and type(rect.width) == int, 60 | hasattr(rect, 'height') and type(rect.height) == int, 61 | ]) 62 | 63 | 64 | ''' 65 | Method: GET 66 | Endpoint: {{baseURL}}/session/{{sessionId}}/element/{{uuid}}/attribute/{{attribute_name}} 67 | ''' 68 | def test_attribute_label(self): 69 | self.assertEqual(self.app(text='ENABLED_BTN').label, 'ENABLED_BTN') 70 | 71 | def test_attribute_invild(self): 72 | with pytest.raises(AttributeError, match="'Element' object has no attribute 'attribute'"): 73 | self.app(text='ENABLED_BTN').attribute('invalid_attribute_name') 74 | 75 | 76 | ''' 77 | Method: GET 78 | Endpoint: {{baseURL}}/session/{{sessionId}}/element/{{uuid}}/text 79 | ''' 80 | def test_attribute_btn_text(self): 81 | self.assertEqual(self.app(text='ENABLED_BTN').text, 'ENABLED_BTN') 82 | 83 | def test_attribute_image_text(self): 84 | self.assertEqual(self.app(id='IMG_BTN').text, 'applogo') 85 | 86 | 87 | ''' 88 | Method: GET 89 | Endpoint: {{baseURL}}/session/{{sessionId}}/element/{{uuid}}/displayed 90 | ''' 91 | def test_attribute_btn_displayed(self): 92 | self.assertEqual(self.app(text='ENABLED_BTN').displayed, True) 93 | 94 | def test_attribute_btn_not_displayed(self): 95 | self.assertEqual(self.app(text='HIDDEN_BTN').displayed, False) 96 | 97 | 98 | ''' 99 | Method: GET 100 | Endpoint: {{baseURL}}/session/{{sessionId}}/element/{{uuid}}/selected 101 | ''' 102 | def test_btn_selected(self): 103 | self.assertEqual(self.app(text='CHECKED_BTN').selected(), True) 104 | 105 | def test_attribute_btn_not_displayed(self): 106 | self.assertEqual(self.app(text='UNCHECKED_BTN').selected(), False) 107 | 108 | 109 | ''' 110 | Method: GET 111 | Endpoint: {{baseURL}}/session/{{sessionId}}/element/{{uuid}}/name 112 | NOTE: Return element type. 113 | ''' 114 | def test_btn_name(self): 115 | self.assertEqual(self.app(text='CHECKED_BTN').name, 'XCUIElementTypeButton') 116 | 117 | def test_img_name(self): 118 | self.assertEqual(self.app(text='IMG_BTN').name, 'XCUIElementTypeImage') 119 | 120 | 121 | ''' 122 | Method: POST 123 | Endpoint: {{baseURL}}/session/{{sessionId}}/element/{{uuid}}/value 124 | ''' 125 | def test_input_text(self): 126 | self.app(id='INPUT_FIELD').set_text('test') 127 | self.assertEqual(self.app(id='INPUT_FIELD').value, 'test') 128 | 129 | 130 | ''' 131 | Method: POST 132 | Endpoint: {{baseURL}}/session/{{sessionId}}/element/{{uuid}}/click 133 | ''' 134 | def test_input_click(self): 135 | input_text_val = 'test' 136 | self.app(id='INPUT_FIELD').set_text(input_text_val) 137 | assert self.app(id='INPUT_FIELD').value == input_text_val 138 | self.app(id='CLEAR_INPUT_BTN').click() 139 | assert self.app(id='INPUT_FIELD').value != input_text_val 140 | 141 | 142 | ''' 143 | Method: POST 144 | Endpoint: {{baseURL}}/session/{{sessionId}}/element/{{uuid}}/clear 145 | ''' 146 | def test_input_clear(self): 147 | input_text_val = 'test' 148 | self.app(id='INPUT_FIELD').set_text(input_text_val) 149 | assert self.app(id='INPUT_FIELD').value == input_text_val 150 | self.app(id='INPUT_FIELD').clear_text() 151 | assert self.app(id='INPUT_FIELD').value != input_text_val 152 | 153 | 154 | ''' 155 | Method: POST 156 | Endpoint: {{baseURL}}/session/{{sessionId}}/element/{{uuid}}/screenshot 157 | ''' 158 | def test_screenshot(self): 159 | self.app.screenshot(png_filename=self.temp_file_pic) 160 | assert os.path.exists(self.temp_file_pic) 161 | 162 | 163 | ''' 164 | Method: POST 165 | Endpoint: {{baseURL}}/session/{{sessionId}}/screenshot/{{uuid}} 166 | Description: screenshot for target element. 167 | ''' 168 | @pytest.mark.skip('NOT IMPLEMENTED: [POST] {{baseURL}}/session/{{sessionId}}/screenshot/{{uuid}}') 169 | def test_screenshot_element(self): 170 | pass 171 | 172 | 173 | ''' 174 | Method: POST 175 | Endpoint: {{baseURL}}/session/{{sessionId}}/wda/element/{{uuid}}/accessible 176 | ''' 177 | def test_ele_accessible_is_false(self): 178 | self.assertFalse(self.app(text='HIDDEN_BTN').accessible) 179 | 180 | def test_ele_accessible_is_true(self): 181 | self.assertTrue(self.app(text='ENABLED_BTN').accessible) 182 | 183 | 184 | ''' 185 | Method: POST 186 | Endpoint: {{baseURL}}/session/{{sessionId}}/wda/element/{{uuid}}/accessibilityContainer 187 | NOTE Swift accessibility Container is not useful, always return false. 188 | 189 | Example: 190 | ``` 191 | import SwiftUI 192 | 193 | struct ContentView: View { 194 | var body: some View { 195 | HStack { 196 | Text("First Item") 197 | Divider() 198 | Text("Second Item") 199 | } 200 | .accessibilityElement(children: .combine) 201 | .accessibility(label: Text("Combined Items")) 202 | } 203 | } 204 | 205 | struct ContentView_Previews: PreviewProvider { 206 | static var previews: some View { 207 | ContentView() 208 | } 209 | } 210 | ``` 211 | ''' 212 | @pytest.mark.skip('WDA API NOT USEFUL: Not use in SwiftUI.') 213 | def test_ele_accessibilityContainer_is_true(self): 214 | self.assertFalse(self.app(id='Combined Items').accessibility_container) 215 | 216 | def test_ele_accessibilityContainer_is_false(self): 217 | self.assertFalse(self.app(id='ENABLED_BTN').accessibility_container) 218 | 219 | 220 | ''' 221 | Method: POST 222 | Endpoint: {{baseURL}}/session/{{sessionId}}/wda/element/{{uuid}}/swipe 223 | ''' 224 | @pytest.mark.skip('NOT IMPLEMENTED') 225 | def test_ele_swipe_top(self): 226 | self.app(text="Go to List").click() 227 | self.app(text="Row1").click() 228 | 229 | 230 | ''' 231 | Method: POST 232 | Endpoint: {{baseURL}}/session/{{sessionId}}/wda/element/{{uuid}}/pinch 233 | ''' 234 | @pytest.mark.skip('NOT IMPLEMENTED: [POST] {{baseURL}}/session/{{sessionId}}/wda/element/{{uuid}}/pinch') 235 | def test_pinch(self): 236 | ... 237 | 238 | 239 | ''' 240 | Method: POST 241 | Endpoint: {{baseURL}}/session/{{sessionId}}/wda/element/{{uuid}}/touchAndHold 242 | ''' 243 | def test_touch_and_hold(self): 244 | try: 245 | self.app(text='OK').get(timeout=1) 246 | except wda.exceptions.WDAElementNotFoundError: 247 | pass 248 | self.app(text='LONG_TAP_ALERT').tap_hold(duration=2) 249 | self.assertTrue(self.app(text='LONG_TAP_ALERT_OK').get(timeout=1).displayed) 250 | 251 | 252 | ''' 253 | Method: POST 254 | Endpoint: {{baseURL}}/session/{{sessionId}}/wda/element/{{uuid}}/dragfromtoforduration 255 | ''' 256 | @pytest.mark.skip('NOT IMPLEMENTED: [POST] {{baseURL}}/session/{{sessionId}}/wda/element/{{uuid}}/dragfromtoforduration') 257 | def test_drag_from_to_for_duration(self): 258 | pass 259 | 260 | 261 | ''' 262 | Method: POST 263 | Endpoint: {{baseURL}}/session/{{sessionId}}/wda/dragfromtoforduration 264 | ''' 265 | def test_app_drag_from_to_for_duration(self): 266 | self.app(text='ListView').click() 267 | self.assertTrue(self.app(text='Row1').get(timeout=1).displayed) 268 | try: 269 | self.app(text='Row30').get(timeout=1) 270 | except wda.exceptions.WDAElementNotFoundError: 271 | pass 272 | self.app.swipe(500, 800, 500, 200, duration=0.5) 273 | self.assertTrue(self.app(text='Row30').get(timeout=1).displayed) 274 | 275 | 276 | ''' 277 | Method: POST 278 | Endpoint: {{baseURL}}/session/{{sessionId}}/wda/pickerwheel/{{uuid}}/select 279 | ''' 280 | @pytest.mark.skip('NOT IMPLEMENTED: [POST] {{baseURL}}/session/{{sessionId}}/wda/pickerwheel/{{uuid}}/select') 281 | def test_ele_select(self): 282 | pass 283 | 284 | 285 | ''' 286 | Method: POST 287 | Endpoint: {{baseURL}}/session/{{sessionId}}/wda/keys 288 | ''' 289 | def test_wda_keys(self): 290 | self.app(text='INPUT_FIELD').click() 291 | self.app.send_keys('hello world') 292 | self.assertEqual(self.app(text='INPUT_FIELD').value, 'hello world') 293 | 294 | 295 | ''' 296 | Method: POST 297 | Endpoint: {{baseURL}}/session/{{sessionId}}/wda/element/{{uuid}}/forceTouch 298 | ''' 299 | @pytest.mark.skip('NOT IMPLEMENTED: [POST] {{baseURL}}/session/{{sessionId}}/wda/element/{{uuid}}/forceTouch') 300 | def test_ele_force_touch(self): 301 | pass 302 | 303 | 304 | ''' 305 | Method: POST 306 | Endpoint: {{baseURL}}/session/{{sessionId}}/wda/doubleTap 307 | ''' 308 | def test_ele_touble_tap(self): 309 | bounds = self.app(text='DOUBLE_TAP_ALERT').bounds 310 | x = int(bounds.x + bounds.width * 0.5) 311 | y = int(bounds.y + bounds.height * 0.5) 312 | self.app.double_tap(x, y) 313 | self.assertTrue(self.app(text='DOUBLE_TAP_ALERT_OK').get(timeout=1).displayed) 314 | 315 | 316 | ''' 317 | Method: POST 318 | Endpoint: {{baseURL}}/session/{{sessionId}}/wda/touchAndHold 319 | ''' 320 | def test_wda_touch_and_hold(self): 321 | bounds = self.app(text='LONG_TAP_ALERT').bounds 322 | x = int(bounds.x + bounds.width * 0.5) 323 | y = int(bounds.y + bounds.height * 0.5) 324 | self.app.tap_hold(x, y, duration=2) 325 | self.assertTrue(self.app(text='LONG_TAP_ALERT_OK').get(timeout=1).displayed) 326 | -------------------------------------------------------------------------------- /e2e_benchmarks/test_find_element_commands.py: -------------------------------------------------------------------------------- 1 | ''' 2 | WDA Find Element Command Testing 3 | source code here: https://github.com/appium/WebDriverAgent/blob/master/WebDriverAgentLib/Commands/FBElementCommands.m 4 | WDA API document and example(not offical): https://documenter.getpostman.com/view/1837823/TVmMhJNB#56e19c88-8571-48d3-a9f1-8e9bd0cbae0d 5 | ''' 6 | import os 7 | import pytest 8 | import unittest 9 | from typing import List 10 | from collections.abc import Iterable 11 | import wda 12 | from .constant import * 13 | 14 | curPath = os.path.abspath(os.path.dirname(__file__)) 15 | 16 | 17 | class TestFindElement(unittest.TestCase): 18 | 19 | def setUp(self): 20 | self.under_test_bundle_id = UNDER_TEST_BUNDLE_ID 21 | self.wda_client: wda.Client = wda.Client() 22 | self.app = self.wda_client.session(bundle_id=self.under_test_bundle_id) 23 | self.temp_file_pic = os.path.join(curPath, 'temp_file_pic.png') 24 | 25 | def tearDown(self): 26 | self.wda_client.close() 27 | [os.remove(temp_file) for temp_file in [self.temp_file_pic] if os.path.exists(temp_file)] 28 | 29 | 30 | ''' 31 | Method: GET 32 | Endpoint: {{baseURL}}/session/{{sessionId}}/elements 33 | ''' 34 | def test_elements(self): 35 | elements = self.app(text='IMG_BTN').find_elements() 36 | self.assertTrue(isinstance(elements, Iterable)) # 检查是否可迭代 37 | for element in elements: 38 | self.assertIsInstance(element, wda.Element) 39 | 40 | def test_element_ids(self): 41 | elements = self.app(text='IMG_BTN').find_element_ids() 42 | self.assertTrue(isinstance(elements, Iterable)) # 检查是否可迭代 43 | for element in elements: 44 | self.assertIsInstance(element, str) 45 | 46 | 47 | ''' 48 | Method: GET 49 | Endpoint: {{baseURL}}/session/{{sessionId}}/element/{{uuid}}/elements 50 | ''' 51 | @pytest.mark.skip('NOT IMPLEMENTED: [GET] /session/{{sessionId}}/element/{{uuid}}/elements') 52 | def test_ele_elements(self): 53 | pass 54 | 55 | 56 | ''' 57 | Method: GET 58 | Endpoint: {{baseURL}}/session/{{sessionId}}/element 59 | ''' 60 | @pytest.mark.skip('NOT IMPLEMENTED: [GET] {{baseURL}}/session/{{sessionId}}/element') 61 | def test_wda_element(self): 62 | pass 63 | 64 | 65 | ''' 66 | Method: GET 67 | Endpoint: {{baseURL}}/session/{{sessionId}}/wda/element/{{uuid}}/getVisibleCells 68 | 69 | Return Example: 70 | ``` 71 | {'value': [{'ELEMENT': '31000000-0000-0000-996B-000000000000', 72 | 'element-6066-11e4-a52e-4f735466cecf': '31000000-0000-0000-996B-000000000000'}], 73 | 'sessionId': 'BC8DE836-6F48-4F7B-B540-FCEC97C06068', 'status': 0} 74 | ``` 75 | ''' 76 | @pytest.mark.skip('NOT IMPLEMENTED: [GET] {{baseURL}}/session/{{sessionId}}/wda/element/{{uuid}}/getVisibleCells') 77 | def test_wda_element(self): 78 | self.app(text='ListView').click() 79 | ele: wda.Element = self.app(label='LIST_CONTAINER').get(timeout=1) 80 | visible_cells_response = ele.http.get(f'/wda/element/{ele.id}/getVisibleCells') 81 | 82 | 83 | ''' 84 | Method: GET 85 | Endpoint: {{baseURL}}/session/{{sessionId}}/element/active 86 | ''' 87 | @pytest.mark.skip('NOT IMPLEMENTED: [GET] {{baseURL}}/session/{{sessionId}}/element/active') 88 | def test_wda_active(self): 89 | pass 90 | -------------------------------------------------------------------------------- /e2e_benchmarks/test_orientation_commands.py: -------------------------------------------------------------------------------- 1 | ''' 2 | WDA Orientation Command Testing 3 | source code here: https://github.com/appium/WebDriverAgent/blob/master/WebDriverAgentLib/Commands/FBOrientationCommands.m 4 | WDA API document and example(not offical): https://documenter.getpostman.com/view/1837823/TVmMhJNB#1ca27827-9931-4315-a6aa-141b6015ac04 5 | ''' 6 | import os 7 | import time 8 | import pytest 9 | import unittest 10 | from typing import List 11 | from collections.abc import Iterable 12 | import wda 13 | from .constant import * 14 | 15 | curPath = os.path.abspath(os.path.dirname(__file__)) 16 | 17 | 18 | class TestOrientation(unittest.TestCase): 19 | 20 | def setUp(self): 21 | self.under_test_bundle_id = UNDER_TEST_BUNDLE_ID 22 | self.wda_client: wda.Client = wda.Client() 23 | self.app = self.wda_client.session(bundle_id=self.under_test_bundle_id) 24 | self.temp_file_pic = os.path.join(curPath, 'temp_file_pic.png') 25 | 26 | def tearDown(self): 27 | self.app.orientation = wda.PORTRAIT 28 | self.wda_client.close() 29 | [os.remove(temp_file) for temp_file in [self.temp_file_pic] if os.path.exists(temp_file)] 30 | 31 | 32 | ''' 33 | Method: GET 34 | Endpoint: {{baseURL}}/session/{{sessionId}}/orientation 35 | ''' 36 | def test_orientation(self): 37 | assert self.app.orientation in ['PORTRAIT', 'LANDSCAPE'] 38 | 39 | 40 | ''' 41 | Method: POST 42 | Endpoint: {{baseURL}}/session/{{sessionId}}/orientation 43 | ''' 44 | def test_set_orientation(self): 45 | self.app.orientation = wda.LANDSCAPE 46 | time.sleep(1) 47 | assert self.app.orientation == 'LANDSCAPE' 48 | 49 | 50 | ''' 51 | Method: GET 52 | Endpoint: {{baseURL}}/session/{{sessionId}}/rotation 53 | ''' 54 | @pytest.mark.skip('NOT IMPLEMENTED: [GET] {{baseURL}}/session/{{sessionId}}/rotation') 55 | def test_get_rotation(self): 56 | pass 57 | 58 | 59 | ''' 60 | Method: POST 61 | Endpoint: {{baseURL}}/session/{{sessionId}}/rotation 62 | ''' 63 | @pytest.mark.skip('NOT IMPLEMENTED: [POST] {{baseURL}}/session/{{sessionId}}/rotation') 64 | def test_set_rotation(self): 65 | pass 66 | 67 | -------------------------------------------------------------------------------- /e2e_benchmarks/test_screenshot_commands.py: -------------------------------------------------------------------------------- 1 | ''' 2 | WDA Screenshot Command Testing 3 | source code here: https://github.com/appium/WebDriverAgent/blob/master/WebDriverAgentLib/Commands/FBScreenshotCommands.m 4 | WDA API document and example(not offical): https://documenter.getpostman.com/view/1837823/TVmMhJNB#bf7cb0a1-cc3b-4eb5-a9ee-1e7e63b55df1 5 | ''' 6 | import os 7 | import unittest 8 | from typing import List 9 | import wda 10 | from .constant import * 11 | 12 | curPath = os.path.abspath(os.path.dirname(__file__)) 13 | 14 | 15 | class TestScreenshot(unittest.TestCase): 16 | 17 | def setUp(self): 18 | self.under_test_bundle_id = UNDER_TEST_BUNDLE_ID 19 | self.wda_client: wda.Client = wda.Client() 20 | self.app = self.wda_client.session(bundle_id=self.under_test_bundle_id) 21 | self.temp_file_pic = os.path.join(curPath, 'temp_file_pic.png') 22 | 23 | def tearDown(self): 24 | self.app.orientation = wda.PORTRAIT 25 | self.wda_client.close() 26 | [os.remove(temp_file) for temp_file in [self.temp_file_pic] if os.path.exists(temp_file)] 27 | 28 | 29 | ''' 30 | Method: POST 31 | Endpoint: {{baseURL}}/screenshot 32 | ''' 33 | def test_screenshot(self): 34 | self.app.screenshot(self.temp_file_pic) 35 | assert os.path.exists(self.temp_file_pic) 36 | -------------------------------------------------------------------------------- /e2e_benchmarks/test_session_commands.py: -------------------------------------------------------------------------------- 1 | ''' 2 | WDA Session Command Testing 3 | API FBSessionCommands.m 4 | source code here: https://github.com/appium/WebDriverAgent/blob/master/WebDriverAgentLib/Commands/FBSessionCommands.m 5 | WDA API document and example(not offical): https://documenter.getpostman.com/view/1837823/TVmMhJNB#f94ebe16-ae2a-4098-879e-570a0138c7f4 6 | ''' 7 | import os 8 | import pytest 9 | import unittest 10 | import jsonschema 11 | import wda 12 | from .constant import * 13 | 14 | curPath = os.path.abspath(os.path.dirname(__file__)) 15 | 16 | 17 | class TestSessionCommands(unittest.TestCase): 18 | 19 | def setUp(self): 20 | self.wda_client: wda.Client = wda.Client() 21 | # self.app = self.wda_client.session(bundle_id=self.under_test_bundle_id) 22 | self.temp_file_pic = os.path.join(curPath, 'temp_file_pic.png') 23 | 24 | def tearDown(self): 25 | self.wda_client.close() 26 | [os.remove(temp_file) for temp_file in [self.temp_file_pic] if os.path.exists(temp_file)] 27 | 28 | 29 | ''' 30 | Method: POST 31 | Endpoint: {{baseURL}}/session/{{sessionId}}/url 32 | ''' 33 | @pytest.mark.skip("UNKNOW HOW TO TEST: [POST] {{baseURL}}/session/{{sessionId}}/url") 34 | def test_url(self): 35 | pass 36 | 37 | 38 | ''' 39 | Method: POST 40 | Endpoint: {{baseURL}}/session 41 | ''' 42 | def test_create_session_id(self): 43 | before_create_session_id = self.wda_client.session_id 44 | self.assertIsNot(self.wda_client.session().session_id, before_create_session_id, None) 45 | client: wda.Client = wda.Client().session(bundle_id=UNDER_TEST_BUNDLE_ID) 46 | 47 | 48 | ''' 49 | Method: POST 50 | Endpoint: {{baseURL}}/session/{{sessionId}}/wda/apps/launch 51 | ''' 52 | def test_launch_app(self): 53 | self.wda_client.app_activate(UNDER_TEST_BUNDLE_ID) 54 | self.wda_client.app_current().get('bundle_id') == UNDER_TEST_BUNDLE_ID 55 | 56 | 57 | ''' 58 | Method: GET 59 | Endpoint: {{baseURL}}/session/{{sessionId}}/wda/apps/list 60 | ''' 61 | def test_app_list(self): 62 | self.wda_client.session(UNDER_TEST_BUNDLE_ID) 63 | self.assertIsInstance(self.wda_client.app_list(), list) 64 | self.assertEqual(self.wda_client.app_list()[0].get('bundleId'), UNDER_TEST_BUNDLE_ID) 65 | 66 | ''' 67 | Method: POST 68 | Endpoint: {{baseURL}}/session/{{sessionId}}/wda/apps/state 69 | ''' 70 | def test_app_state(self): 71 | self.wda_client.session(UNDER_TEST_BUNDLE_ID) 72 | self.assertIsInstance(self.wda_client.app_state(UNDER_TEST_BUNDLE_ID), dict) 73 | self.assertEqual(self.wda_client.app_state(UNDER_TEST_BUNDLE_ID).get('value'), 4) 74 | 75 | def test_unknow_app_state(self): 76 | self.assertEqual(self.wda_client.app_state(UNKNOW_BUNDLE_ID).get('value'), 1) 77 | 78 | 79 | ''' 80 | Method: POST 81 | Endpoint: {{baseURL}}/session/{{sessionId}}/wda/apps/terminate 82 | ''' 83 | def test_app_terminate(self): 84 | self.wda_client.app_terminate(UNDER_TEST_BUNDLE_ID) 85 | self.assertTrue(self.wda_client.app_state(UNDER_TEST_BUNDLE_ID).get('value') == 1) 86 | 87 | 88 | ''' 89 | Method: GET 90 | Endpoint: {{baseURL}}/status 91 | ''' 92 | def test_status_command_return_schema(self): 93 | ''' 94 | Current Origin Status Return: 95 | { 96 | "build": { 97 | "time": "Mar 18 2024 11:29:21", 98 | "productBundleIdentifier": "com.facebook.WebDriverAgentRunner" 99 | }, 100 | "os": { 101 | "testmanagerdVersion": 28, 102 | "name": "iOS", 103 | "sdkVersion": "16.4", 104 | "version": "16.3.1" 105 | }, 106 | "device": "iphone", 107 | "ios": { 108 | "ip": "XX.XX.XX.XX" 109 | }, 110 | "message": "WebDriverAgent is ready to accept commands", 111 | "state": "success", 112 | "ready": true, 113 | "sessionId": "XXXXX" 114 | } 115 | ''' 116 | self.wda_client.session(UNDER_TEST_BUNDLE_ID) 117 | expect_schema = {"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"build":\ 118 | {"type":"object","properties":{"time":{"type":"string"},"productBundleIdentifier":\ 119 | {"type":"string"}},"additionalProperties":"false","required":["time",\ 120 | "productBundleIdentifier"]},"os":{"type":"object","properties":{"testmanagerdVersion"\ 121 | :{"type":"integer"},"name":{"type":"string"},"sdkVersion":{"type":"string"},"version":\ 122 | {"type":"string"}},"additionalProperties":"false","required":["testmanagerdVersion","name",\ 123 | "sdkVersion","version"]},"device":{"type":"string"},"ios":{"type":"object","properties":\ 124 | {"ip":{"type":"string"}},"additionalProperties":"false","required":["ip"]},"message":\ 125 | {"type":"string"},"state":{"type":"string"},"ready":{"type":"boolean"},"sessionId":\ 126 | {"type":"string"}},"additionalProperties":"false","required":["build","os","device","ios",\ 127 | "message","state","ready", "sessionId"]} 128 | 129 | self.assertTrue(jsonschema.Draft7Validator(expect_schema).is_valid(self.wda_client.status())) 130 | 131 | def test_status_command_state_and_ready(self): 132 | status = self.wda_client.status() 133 | assert all([status['state'] == 'success', status['ready'] is True]) 134 | 135 | 136 | ''' 137 | Method: GET 138 | Endpoint: {{baseURL}}/session/{{sessionId}} 139 | ''' 140 | @pytest.mark.skip('NOT IMPLEMENTED: [GET] {{baseURL}}/session/{{sessionId}}') 141 | def test_session_info(self): 142 | pass 143 | 144 | ''' 145 | Method: DELETE 146 | Endpoint: {{baseURL}}/session/{{sessionId}} 147 | ''' 148 | def test_delete_session(self): 149 | self.wda_client.session(UNDER_TEST_BUNDLE_ID) 150 | assert self.wda_client.session_id is not None 151 | print(self.wda_client.close()) 152 | 153 | ''' 154 | Method: POST 155 | Endpoint: {{baseURL}}/session/{{sessionId}}/wda/apps/activate 156 | ''' 157 | def test_app_activate(self): 158 | self.wda_client.session() 159 | self.wda_client.app_activate(UNDER_TEST_BUNDLE_ID) 160 | self.assertEqual(self.wda_client.app_current().get('bundleId'), UNDER_TEST_BUNDLE_ID) 161 | 162 | 163 | ''' 164 | Method: GET 165 | Endpoint: {{baseURL}}/wda/healthcheck 166 | ''' 167 | def test_health_check(self): 168 | health_check = self.wda_client.healthcheck() 169 | assert all([ 170 | hasattr(health_check, 'value'), 171 | hasattr(health_check, 'sessionId'), 172 | health_check.get('value') == None, 173 | health_check.get('sessionId') == None 174 | ]) 175 | 176 | def test_health_check_has_session_id(self): 177 | self.wda_client.session(UNDER_TEST_BUNDLE_ID) 178 | health_check = self.wda_client.healthcheck() 179 | assert all([ 180 | hasattr(health_check, 'value'), 181 | hasattr(health_check, 'sessionId'), 182 | health_check.get('value') == None, 183 | health_check.get('sessionId') == self.wda_client.session_id 184 | ]) 185 | 186 | ''' 187 | Method: GET 188 | Endpoint: {{baseURL}}/session/{{sessionId}}/appium/settings 189 | Return Example: 190 | 191 | ``` 192 | { 193 | "mjpegFixOrientation": false, 194 | "boundElementsByIndex": false, 195 | "mjpegServerFramerate": 10, 196 | "screenshotOrientation": "auto", 197 | "reduceMotion": false, 198 | "elementResponseAttributes": "type,label", 199 | "screenshotQuality": 3, 200 | "mjpegScalingFactor": 100, 201 | "keyboardPrediction": 0, 202 | "defaultActiveApplication": "auto", 203 | "mjpegServerScreenshotQuality": 25, 204 | "defaultAlertAction": "", 205 | "keyboardAutocorrection": 0, 206 | "useFirstMatch": false, 207 | "shouldUseCompactResponses": true, 208 | "customSnapshotTimeout": 15, 209 | "dismissAlertButtonSelector": "", 210 | "activeAppDetectionPoint": "64.00,64.00", 211 | "snapshotMaxDepth": 50, 212 | "waitForIdleTimeout": 10, 213 | "includeNonModalElements": false, 214 | "acceptAlertButtonSelector": "", 215 | "animationCoolOffTimeout": 2 216 | } 217 | ``` 218 | ''' 219 | def test_appium_settings(self): 220 | appium_settings = self.wda_client.appium_settings() 221 | expect_schema = {"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties": 222 | {"mjpegFixOrientation":{"type":"boolean"},"boundElementsByIndex":{"type":"boolean"}, 223 | "mjpegServerFramerate":{"type":"integer"},"screenshotOrientation":{"type":"string"}, 224 | "reduceMotion":{"type":"boolean"},"elementResponseAttributes":{"type":"string"}, 225 | "screenshotQuality":{"type":"integer"},"mjpegScalingFactor":{"type":"integer"}, 226 | "keyboardPrediction":{"type":"integer"},"defaultActiveApplication":{"type":"string"}, 227 | "mjpegServerScreenshotQuality":{"type":"integer"},"defaultAlertAction":{"type":"string"}, 228 | "keyboardAutocorrection":{"type":"integer"},"useFirstMatch":{"type":"boolean"}, 229 | "shouldUseCompactResponses":{"type":"boolean"},"customSnapshotTimeout":{"type":"integer"}, 230 | "dismissAlertButtonSelector":{"type":"string"},"activeAppDetectionPoint":{"type":"string"}, 231 | "snapshotMaxDepth":{"type":"integer"},"waitForIdleTimeout":{"type":"integer"}, 232 | "includeNonModalElements":{"type":"boolean"},"acceptAlertButtonSelector":{"type":"string"}, 233 | "animationCoolOffTimeout":{"type":"integer"}},"additionalProperties":False, 234 | "required":["mjpegFixOrientation","boundElementsByIndex","mjpegServerFramerate", 235 | "screenshotOrientation","reduceMotion","elementResponseAttributes","screenshotQuality", 236 | "mjpegScalingFactor","keyboardPrediction","defaultActiveApplication","mjpegServerScreenshotQuality", 237 | "defaultAlertAction","keyboardAutocorrection","useFirstMatch","shouldUseCompactResponses", 238 | "customSnapshotTimeout","dismissAlertButtonSelector","activeAppDetectionPoint","snapshotMaxDepth", 239 | "waitForIdleTimeout","includeNonModalElements","acceptAlertButtonSelector","animationCoolOffTimeout"]} 240 | self.assertTrue(jsonschema.Draft7Validator(expect_schema).is_valid(appium_settings)) 241 | 242 | 243 | ''' 244 | Method: GET 245 | Endpoint: {{baseURL}}/wda/shutdown 246 | ''' 247 | @pytest.mark.skip('NOT IMPLEMENTED: [GET] {{baseURL}}/wda/shutdown') 248 | def test_wda_shutdown(self): 249 | pass 250 | 251 | 252 | ''' 253 | Method: GET 254 | Endpoint: {{baseURL}}/health 255 | ''' 256 | @pytest.mark.skip('NOT IMPLEMENTED: [GET] {{baseURL}}/health') 257 | def test_health(self): 258 | pass 259 | -------------------------------------------------------------------------------- /examples/com.netease.cloudmusic-pytest/README.txt: -------------------------------------------------------------------------------- 1 | 网易云音乐测试例子 2 | 3 | 运行测试方法 4 | 5 | py.test -vv 6 | 7 | Sample output 8 | 9 | collected 2 items 10 | 11 | test_discover_music.py::test_discover_music PASSED 12 | test_discover_music.py::test_my_music PASSED -------------------------------------------------------------------------------- /examples/com.netease.cloudmusic-pytest/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest -------------------------------------------------------------------------------- /examples/com.netease.cloudmusic-pytest/test_discover_music.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | # 网易云音乐测试示例 4 | # 5 | 6 | import os 7 | import time 8 | import wda 9 | import pytest 10 | from pytest import mark 11 | 12 | 13 | bundle_id = 'com.netease.cloudmusic' 14 | 15 | c = wda.Client() 16 | s = None 17 | 18 | USERNAME = os.getenv('USERNAME') 19 | PASSWORD = os.getenv('PASSWORD') 20 | 21 | def account_logout(s): 22 | s(nameMatches=u'帐[ ]*号', type='Button').tap() # not support \s, wired 23 | s(name=u'退出登录').scroll().tap() 24 | s.alert.click(u'确定') 25 | 26 | def account_netease_login(s): 27 | """ 完成网易邮箱登录 """ 28 | if s(name=u'发现音乐', type='Button').wait(3, raise_error=False): 29 | # Already logged in 30 | return 31 | 32 | s(name=u'网易邮箱').tap() 33 | s(type='TextField').set_text(USERNAME+'\n') 34 | s(type='SecureTextField').set_text(PASSWORD+'\n') 35 | s(name=u'开启云音乐').click_exists(timeout=3.0) 36 | assert s(name=u'发现音乐', type='Button').wait(5.0) 37 | 38 | 39 | def alert_callback(session): 40 | btns = set([u'不再提醒', 'OK', u'知道了', 'Allow']).intersection(session.alert.buttons()) 41 | if len(btns) == 0: 42 | raise RuntimeError("Alert can not handled, buttons: " + ', '.join(session.alert.buttons())) 43 | session.alert.click(list(btns)[0]) 44 | 45 | def create_session(): 46 | s = c.session(bundle_id) 47 | s.set_alert_callback(alert_callback) 48 | return s 49 | 50 | def setup_function(): 51 | global s 52 | s = create_session() 53 | account_netease_login(s) 54 | 55 | def teardown_function(): 56 | s.close() 57 | # s = create_session() 58 | # account_logout(s) 59 | # s.close() 60 | 61 | 62 | def test_discover_music(): 63 | """ 64 | 测试 发现音乐->私人FM 中的播放功能 65 | """ 66 | s(name=u'发现音乐', type='Button').tap() 67 | time.sleep(.5) 68 | assert s(name=u'听歌识曲', visible=True).wait() 69 | s(name=u'私人FM').tap() 70 | assert s(name=u'不再播放').exists 71 | assert s(name=u'添加到我喜欢的音乐').exists 72 | assert s(name=u'00:00', className='StaticText').exists 73 | s(nameMatches=u'(暂停|播放)').tap() 74 | assert s(name=u'00:00', className='StaticText').wait_gone(10.0) 75 | s(name=u'跑步FM').tap() 76 | s(name=u'知道了').click_exists(2.0) 77 | 78 | 79 | def test_my_music(): 80 | """ 81 | 测试 我的音乐->本地音乐 82 | """ 83 | s(name=u'我的音乐', type='Button').tap() 84 | assert s(name=u'最近播放').wait(2.0) 85 | s(name=u'本地音乐').tap() 86 | assert s(name=u'管理').wait() 87 | s(name=u'播放全部').tap() -------------------------------------------------------------------------------- /examples/full_example.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | # 网易云音乐测试示例 4 | # 5 | 6 | import wda 7 | from logzero import logger 8 | 9 | 10 | bundle_id = 'com.apple.Preferences' 11 | 12 | def test_preferences(c: wda.Client): 13 | print("Status:", c.status()) 14 | print("Info:", c.info) 15 | print("BatteryInfo", c.battery_info()) 16 | print("AppCurrent:", c.app_current()) 17 | # page_source = c.source() 18 | # assert "" in page_source 19 | 20 | app = c.session(bundle_id) 21 | selector = app(label="蜂窝网络") 22 | el = selector.get() 23 | el.click() 24 | print("Element bounds:", el.bounds) 25 | 26 | logger.info("Take screenshot: %s", app.screenshot()) 27 | 28 | app.swipe_right() 29 | app.swipe_up() 30 | 31 | app(label="电池").scroll() 32 | app(label="电池").click() 33 | 34 | 35 | def test_open_safari(c: wda.Client): 36 | """ session操作 """ 37 | app = c.session("com.apple.mobilesafari") 38 | app.deactivate(3) # 后台3s 39 | app.close() # 关闭 40 | 41 | 42 | def test_send_keys_callback(c: wda.Client): 43 | def _handle_alert_before_send_keys(client: wda.Client, urlpath: str): 44 | if not urlpath.endswith("/wda/keys"): 45 | return 46 | if client.alert.exists: 47 | client.alert.accept() 48 | print("callback called") 49 | 50 | c.register_callback(wda.Callback.HTTP_REQUEST_BEFORE, _handle_alert_before_send_keys) 51 | c.send_keys("hello callback") 52 | 53 | 54 | def test_error_callback(c: wda.Client): 55 | def err_handler(client: wda.Client, err): 56 | if isinstance(err, wda.WDARequestError): 57 | print("ERROR:", err) 58 | return wda.Callback.RET_ABORT # 直接退出 59 | return wda.Callback.RET_CONTINUE # 忽略错误继续执行 60 | return wda.Callback.RET_RETRY # 重试一下 61 | 62 | c.register_callback(wda.Callback.ERROR, err_handler) 63 | c.send_keys("hello callback") 64 | 65 | 66 | def test_elememt_operation(c: wda.Client): 67 | c(label="DisplayAlert").exists 68 | el = c(label="DisplayAlert").get() 69 | print("accessible:", el.accessible) 70 | print("accessibility_container:", el.accessibility_container) 71 | print("enabled:", el.enabled) 72 | print("visible:", el.visible) 73 | print("label:", el.label) 74 | print("className:", el.className) 75 | el.click() 76 | 77 | print("alertExists:", c.alert.exists) 78 | print("alertButtons:", c.alert.buttons()) 79 | print("alertClick:", c.alert.click("Dismiss")) 80 | 81 | 82 | def test_xpath(c: wda.Client): 83 | c.xpath("//Window/Other/Other").exists 84 | 85 | 86 | 87 | def test_invalid_session(c: wda.Client): 88 | app = c.session("com.apple.Preferences") 89 | # kill app here 90 | app.session_id = "no-exists" 91 | app(label="Haha").exists 92 | 93 | 94 | if __name__ == "__main__": 95 | c = wda.USBClient() 96 | # c.healthcheck() # 恢复WDA状态 97 | # test_error_callback(c) 98 | # test_elememt_operation(c) 99 | # test_preferences(c) 100 | # test_open_safari(c) 101 | # test_xpath(c) 102 | wda.DEBUG = True 103 | test_invalid_session(c) 104 | -------------------------------------------------------------------------------- /images/ios-display.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openatx/facebook-wda/85d944acb2371820c9c6542b6063948def94c120/images/ios-display.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | six 2 | retry 3 | Pillow 4 | cached-property~=1.5.1 5 | Deprecated~=1.2.6 6 | construct>=2 -------------------------------------------------------------------------------- /runtest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash - 2 | # 3 | 4 | if test -n "$TRAVIS" 5 | then 6 | echo "Skip in travis" 7 | exit 0 8 | fi 9 | 10 | # export PYTHONPATH=$PWD:$PYTHONPATH 11 | # python tests/test_client.py 12 | cd tests 13 | py.test -vv test_client.py test_common.py "$@" 14 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = facebook-wda 3 | author = "codeskyblue" 4 | summary = Python Client for Facebook WebDriverAgent 5 | license = MIT 6 | description-file = ABOUT.rst 7 | home-page = https://github.com/openatx/facebook-wda 8 | classifier = 9 | Development Status :: 3 - Alpha 10 | Environment :: Console 11 | Intended Audience :: Developers 12 | Operating System :: POSIX :: Linux 13 | Programming Language :: Python :: 2.7 14 | Programming Language :: Python :: 3.2 15 | Programming Language :: Python :: 3.3 16 | Programming Language :: Python :: 3.4 17 | Programming Language :: Python :: 3.5 18 | Topic :: Software Development :: Libraries :: Python Modules 19 | Topic :: Software Development :: Testing 20 | 21 | [files] 22 | packages = 23 | wda 24 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | # 4 | # Licensed under MIT 5 | # 6 | 7 | import setuptools 8 | setuptools.setup(setup_requires=['pbr'], pbr=True) 9 | -------------------------------------------------------------------------------- /tests/AlertTest.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openatx/facebook-wda/85d944acb2371820c9c6542b6063948def94c120/tests/AlertTest.zip -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | 4 | import wda 5 | import pytest 6 | import os 7 | 8 | 9 | @pytest.fixture 10 | def c(): 11 | if os.getenv("DEVICE_URL"): 12 | return wda.Client(os.getenv("DEVICE_URL")) 13 | return wda.USBClient() 14 | 15 | #wda.DEBUG = True 16 | #__target = os.getenv("DEVICE_URL") or 'http://localhost:8100' 17 | 18 | 19 | @pytest.fixture(scope="function") 20 | def app(c) -> wda.Client: 21 | return c.session('com.apple.Preferences') 22 | -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest>=2.9.1 2 | -------------------------------------------------------------------------------- /tests/test_callback.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | 4 | import wda 5 | import threading 6 | 7 | 8 | def test_callback(app: wda.Client): 9 | event = threading.Event() 10 | def _cb(client: wda.Client, url: str): 11 | if url.endswith("/sendkeys"): 12 | pass 13 | assert isinstance(client, wda.Client) 14 | assert url.endswith("/status") 15 | event.set() 16 | 17 | app.register_callback(wda.Callback.HTTP_REQUEST_BEFORE, _cb) 18 | # app.register_callback(wda.Callback.HTTP_REQUEST_AFTER, lambda url, response: print(url, response)) 19 | app.status() 20 | assert event.is_set(), "callback is not called" 21 | 22 | # test remove_callback 23 | event.clear() 24 | app.unregister_callback(wda.Callback.HTTP_REQUEST_BEFORE, _cb) 25 | app.status() 26 | assert not event.is_set(), "callback is should not be called" 27 | 28 | event.clear() 29 | app.unregister_callback(wda.Callback.HTTP_REQUEST_BEFORE) 30 | app.status() 31 | assert not event.is_set(), "callback is should not be called" 32 | 33 | event.clear() 34 | app.unregister_callback() 35 | app.status() 36 | assert not event.is_set(), "callback is should not be called" -------------------------------------------------------------------------------- /tests/test_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import print_function 5 | 6 | import os 7 | import time 8 | import pytest 9 | import xml.etree.ElementTree as ET 10 | 11 | import wda 12 | from pytest import mark 13 | 14 | 15 | @pytest.fixture(scope="function") 16 | def client(c): 17 | c.home() 18 | return c 19 | 20 | def test_client_status(client: wda.Client): 21 | """ Example response 22 | { 23 | "state": "success", 24 | "os": { 25 | "version": "10.3.3", 26 | "name": "iOS" 27 | }, 28 | "ios": { 29 | "ip": "192.168.2.85", 30 | "simulatorVersion": "10.3.3" 31 | }, 32 | "build": { 33 | "time": "Aug 8 2017 17:06:05" 34 | }, 35 | "sessionId": "xx...x.x.x.x.x.x" # added by python code 36 | } 37 | """ 38 | st = client.status() # json value 39 | assert st['state'] == 'success' 40 | # assert 'sessionId' in st 41 | 42 | 43 | def test_client_session_without_argument(client: wda.Client): 44 | s = client.session('com.apple.Health') 45 | session_id = client.status()['sessionId'] 46 | assert s.session_id == session_id 47 | assert s.bundle_id == 'com.apple.Health' 48 | s.close() 49 | 50 | 51 | @mark.skip("iOS not supported") 52 | def test_client_session_with_argument(client: wda.Client): 53 | """ 54 | In mose case, used to open browser with url 55 | """ 56 | with client.session('com.apple.mobilesafari', ['-u', 'https://www.github.com']) as s: 57 | # time.sleep(1.0) 58 | # s(id='URL').wait() 59 | assert s(name='Share', className="Button").wait() 60 | 61 | 62 | def test_client_screenshot(client: wda.Client): 63 | wda.DEBUG = False 64 | client.screenshot() 65 | 66 | 67 | @mark.skip("unstable api") 68 | def test_client_source(): 69 | wda.DEBUG = False 70 | xml_data = c.source() 71 | root = ET.fromstring(xml_data.encode('utf-8')) 72 | assert root.tag == 'XCUIElementTypeApplication' 73 | 74 | json_data = c.source(format='json') 75 | assert json_data['type'] == 'Application' 76 | 77 | json_data = c.source(accessible=True) 78 | assert json_data['type'] == 'Application' 79 | 80 | 81 | @mark.skip("hard to test") 82 | def test_alert(): 83 | """ 84 | Skip: because alert not always happens 85 | """ 86 | return 87 | # c = wda.Client(__target) 88 | # with c.session('com.apple.Health') as s: 89 | # #print s.alert.text 90 | # pass 91 | 92 | @mark.skip("hard to test") 93 | def test_alert_wait(): 94 | pass 95 | """ Skip because alert not always happens """ 96 | # c = wda.Client(__target) 97 | # with c.session('com.apple.Preferences') as s: 98 | # # start_time = time.time() 99 | # assert s.alert.wait(20) 100 | # # print time.time() - start_time 101 | 102 | 103 | # def test_scroll(): 104 | # c = wda.Client() 105 | # with c.session('com.apple.Preferences') as s: 106 | # s(class_name='Table').scroll('Developer') 107 | # s(text='Developer').tap() 108 | # time.sleep(3) 109 | -------------------------------------------------------------------------------- /tests/test_common.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | 4 | import wda 5 | 6 | 7 | def test_rect(): 8 | r = wda.Rect(10, 20, 10, 30) # x=10, y=20, w=10, h=30 9 | assert r.left == 10 10 | assert r.right == 20 11 | assert r.bottom == 50 12 | assert r.top == 20 13 | assert r.x == 10 and r.y == 20 and r.width == 10 and r.height == 30 14 | assert r.center.x == 15 and r.center.y == 35 15 | assert r.origin.x == 10 and r.origin.y == 20 -------------------------------------------------------------------------------- /tests/test_element.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | 4 | import time 5 | import wda 6 | from pytest import mark 7 | 8 | @mark.skip("Require English") 9 | def test_element_properties(c: wda.Client): 10 | with c.session('com.apple.mobilesafari', ['-u', 'https://www.github.com']) as s: 11 | time.sleep(1.0) 12 | u = None 13 | for e in s(id='URL').find_elements(): 14 | if e.className.endswith('Button'): 15 | u = e 16 | break 17 | assert u.label == 'Address' 18 | assert 'github' in u.value.lower() 19 | assert u.displayed == True 20 | assert u.visible == True 21 | assert u.enabled == True 22 | assert type(u.bounds) is wda.Rect 23 | u.clear_text() 24 | u.set_text('status.github.com\n') 25 | assert 'status' in u.value 26 | 27 | 28 | @mark.skip("Require English") 29 | def test_element_tap_hold(c: wda.Client): 30 | s = c.session() 31 | s(name='Settings').tap_hold(2.0) 32 | assert s(classChain='**/Icon[`name == "Weather"`]/Button[`name == "DeleteButton"`]').get(2.0, raise_error=False) 33 | 34 | 35 | def test_element_name_matches(c: wda.Client): 36 | s = c.session("com.apple.Preferences") 37 | assert s(nameMatches='^蓝牙').exists 38 | info = s(nameMatches='^蓝牙').info 39 | assert info['label'] == '蓝牙' 40 | 41 | 42 | @mark.skip("Require English") 43 | def test_element_scroll_visible(c: wda.Client): 44 | with c.session('com.apple.Preferences') as s: 45 | general = s(name='General') 46 | assert not general.get().visible 47 | general.scroll() 48 | assert general.get().visible 49 | time.sleep(1) 50 | 51 | 52 | @mark.skip("using frequency is low") 53 | def test_element_scroll_direction(c: wda.Client): 54 | with c.session('com.apple.Preferences') as s: 55 | s(className='Table').scroll('up', 0.1) 56 | 57 | @mark.skip("using frequency is low") 58 | def test_element_pinch(c: wda.Client): 59 | with c.session('com.apple.Maps') as s: 60 | def alert_callback(s): 61 | s.alert.accept() 62 | 63 | s.set_alert_callback(alert_callback) 64 | s(className='Button', name='Tracking').tap() 65 | time.sleep(5) -------------------------------------------------------------------------------- /tests/test_session.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import pytest 4 | from pytest import mark 5 | import time 6 | import wda 7 | 8 | 9 | 10 | @mark.skip("no test enviroment") 11 | def test_session_open_url(): 12 | """ TODO: do not know how to use this api """ 13 | pass 14 | 15 | 16 | @mark.skip("wda bug") 17 | def test_session_deactivate(): 18 | with c.session('com.apple.mobilesafari') as s: 19 | s.deactivate(3.0) 20 | assert s(name='Share').wait(2.0, raise_error=False) 21 | 22 | @mark.skip("TODO") 23 | def test_session_just_tap(): 24 | s = c.session() 25 | x, y = s(name='Settings').bounds.center 26 | s.tap(x, y) 27 | assert s(name='Bluetooth').wait(2.0, raise_error=False) 28 | c.home() 29 | 30 | @mark.skip("TODO") 31 | def test_session_double_tap(): 32 | s = c.session() 33 | x, y = s(name='Settings').bounds.center 34 | s.double_tap(x, y) 35 | 36 | @mark.skip("TODO") 37 | def test_session_tap_hold(): 38 | s = c.session() 39 | x, y = s(name='Settings').bounds.center 40 | s.tap_hold(x, y, 2.0) 41 | s(name="DeleteButton").wait(2.0, raise_error=False) 42 | c.home() 43 | 44 | @mark.skip("TODO") 45 | def test_session_swipe(): 46 | s = c.session() 47 | s.swipe_left() 48 | assert not s(name="Settings").displayed 49 | s.swipe_right() 50 | assert s(name="Settings").displayed 51 | s.swipe_up() 52 | assert s(name="Airplane Mode").wait(2.0, raise_error=False) 53 | s.swipe_down() 54 | assert s(name="Airplane Mode").wait_gone(2.0, raise_error=False) 55 | 56 | @mark.skip("wda bug") 57 | def test_session_set_text(): 58 | with c.session('com.apple.mobilesafari') as s: 59 | s(name='URL', className='Button').set_text("status.github.com") 60 | url = s(name='URL', className='TextField').get() 61 | assert url.value == 'status.github.com' 62 | 63 | @mark.skip("TODO") 64 | def test_session_window_size(): 65 | s = c.session() 66 | wsize = s.window_size() 67 | assert wsize.width == 320 68 | assert wsize.height == 568 69 | 70 | @mark.skip("wda bug") 71 | def test_session_send_keys(): 72 | with c.session('com.apple.mobilesafari') as s: 73 | u = s(label='Address', className='Button') 74 | u.clear_text() 75 | s.send_keys('www.github.com') 76 | assert 'www.github.com' == s(label='Address', className='TextField').get().value 77 | 78 | 79 | @mark.skip("wait for WDA fix") 80 | def test_session_keyboard_dismiss(): 81 | with c.session('com.apple.mobilesafari') as s: 82 | u = s(label='Address', className='Button') 83 | u.clear_text() 84 | s.send_keys('www.github.com') 85 | 86 | assert s(className='Keyboard').exists 87 | s.keyboard_dismiss() 88 | assert not s(className='Keyboard').exists 89 | 90 | 91 | def test_session_orientation(c: wda.Client): 92 | c.orientation = wda.PORTRAIT 93 | with c.session('com.apple.mobilesafari') as s: 94 | assert s.orientation == wda.PORTRAIT 95 | s.orientation = wda.LANDSCAPE 96 | time.sleep(1.0) 97 | assert s.orientation == wda.LANDSCAPE 98 | # recover orientation 99 | s.orientation = wda.PORTRAIT 100 | 101 | 102 | def test_session_invalid_with_autofix(c: wda.Client): 103 | c.session("com.apple.Preferences") 104 | c.session_id = "123" 105 | assert c.app_current().bundleId == "com.apple.Preferences" 106 | assert isinstance(c.info, dict) 107 | assert c.session_id != "123" 108 | 109 | 110 | @mark.skip("TODO") 111 | def test_session_wait_gone(): 112 | s = c.session() 113 | elem = s(name="Settings", visible=True) 114 | with pytest.raises(wda.WDAElementNotDisappearError) as e_info: 115 | elem.wait_gone(1.0) 116 | assert not elem.wait_gone(1.0, raise_error=False) 117 | s.swipe_left() 118 | assert elem.wait_gone(1.0) 119 | 120 | @mark.skip("Require English") 121 | def test_text_contains_matches(c: wda.Client): 122 | with c.session('com.apple.Preferences') as s: 123 | s(text='Bluetooth').get() 124 | assert s(textContains="Blue").exists 125 | assert not s(text="Blue").exists 126 | assert s(text="Bluetooth").exists 127 | assert s(textMatches="Blue?").exists 128 | assert s(nameMatches="Blue?").exists 129 | assert not s(textMatches="^lue?").exists 130 | assert not s(textMatches="^Blue$").exists 131 | assert s(textMatches=r"^(Blue|Red).*").exists 132 | 133 | 134 | @pytest.mark.skip("not passed on IRMA") 135 | def test_app_operation(c: wda.Client): 136 | c.session("com.apple.Preferences") 137 | appinfo = c.app_current() 138 | assert appinfo['bundleId'] == 'com.apple.Preferences' 139 | -------------------------------------------------------------------------------- /tests/test_xpath.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | 4 | import wda 5 | 6 | 7 | def test_xpath(app: wda.Client): 8 | with app: 9 | app.xpath("//*[@label='蓝牙']").click() 10 | assert app.xpath('//*[@label="设置"]').wait() 11 | assert app.xpath('//*[@label="设置"]').get().label == "设置" 12 | 13 | # test __getattr__ 14 | assert app.xpath('//*[@label="设置"]').label == "设置" -------------------------------------------------------------------------------- /wda/_proto.py: -------------------------------------------------------------------------------- 1 | # 2 | 3 | __all__ = ['AppiumSettings', 'AlertAction'] 4 | 5 | 6 | import enum 7 | 8 | class AppiumSettings(str, enum.Enum): 9 | """ 10 | {'boundElementsByIndex': False, 11 | 'shouldUseCompactResponses': True, 12 | 'mjpegServerFramerate': 10, 13 | 'snapshotMaxDepth': 50, 14 | 'screenshotOrientation': 'auto', 15 | 'activeAppDetectionPoint': '64.00,64.00', 16 | 'acceptAlertButtonSelector': '', 17 | 'snapshotTimeout': 15, 18 | 'elementResponseAttributes': 'type,label', 19 | 'keyboardPrediction': 0, 20 | 'screenshotQuality': 2, 21 | 'keyboardAutocorrection': 0, 22 | 'useFirstMatch': False, 23 | 'reduceMotion': False, 24 | 'defaultActiveApplication': 'auto', 25 | 'mjpegScalingFactor': 100, 26 | 'mjpegServerScreenshotQuality': 25, 27 | 'dismissAlertButtonSelector': '', 28 | 'includeNonModalElements': False} 29 | """ 30 | AcceptAlertButtonSelector = "acceptAlertButtonSelector" 31 | DismissAlertButtonSelector = "dismissAlertButtonSelector" 32 | 33 | 34 | 35 | # default_alert_accept_selector = "**/XCUIElementTypeButton[`label IN {'允许','好','仅在使用应用期间','暂不'}`]" 36 | # default_alert_dismiss_selector = "**/XCUIElementTypeButton[`label IN {'不允许','暂不'}`]" 37 | 38 | 39 | class AlertAction(str, enum.Enum): 40 | ACCEPT = "accept" 41 | DISMISS = "dismiss" 42 | 43 | -------------------------------------------------------------------------------- /wda/exceptions.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # author: codeskyblue 3 | 4 | import json 5 | 6 | 7 | JSONDecodeError = json.decoder.JSONDecodeError if hasattr( 8 | json.decoder, "JSONDecodeError") else ValueError 9 | 10 | 11 | class MuxError(Exception): 12 | """ Mutex error """ 13 | 14 | 15 | class MuxConnectError(MuxError, ConnectionError): 16 | """ Error when MessageType: Connect """ 17 | 18 | 19 | class WDAError(Exception): 20 | """ base wda error """ 21 | 22 | 23 | class WDABadGateway(WDAError): 24 | """ bad gateway """ 25 | 26 | 27 | class WDAEmptyResponseError(WDAError): 28 | """ response body is empty """ 29 | 30 | 31 | class WDAElementNotFoundError(WDAError): 32 | """ element not found """ 33 | 34 | 35 | class WDAElementNotDisappearError(WDAError): 36 | """ element not disappera """ 37 | 38 | 39 | class WDARequestError(WDAError): 40 | def __init__(self, status, value): 41 | self.status = status 42 | self.value = value 43 | 44 | def __str__(self): 45 | return 'WDARequestError(status=%d, value=%s)' % (self.status, 46 | self.value) 47 | 48 | 49 | class WDAKeyboardNotPresentError(WDARequestError): 50 | # {'error': 'invalid element state', 51 | # 'message': 'Error Domain=com.facebook.WebDriverAgent Code=1 52 | # "The on-screen keyboard must be present to send keys" 53 | # UserInfo={NSLocalizedDescription=The on-screen keyboard must be present to send keys}', 54 | # 'traceback': ''}) 55 | 56 | @staticmethod 57 | def check(v: dict): 58 | if v.get('error') == 'invalid element state' and \ 59 | 'keyboard must be present to send keys' in v.get('message', ''): 60 | return True 61 | return False 62 | 63 | 64 | class WDAInvalidSessionIdError(WDARequestError): 65 | """ 66 | "value" : { 67 | "error" : "invalid session id", 68 | "message" : "Session does not exist", 69 | """ 70 | @staticmethod 71 | def check(v: dict): 72 | if v.get('error') == 'invalid session id': 73 | return True 74 | return False 75 | 76 | 77 | class WDAPossiblyCrashedError(WDARequestError): 78 | @staticmethod 79 | def check(v: dict): 80 | if "possibly crashed" in v.get('message', ''): 81 | return True 82 | return False 83 | 84 | 85 | class WDAUnknownError(WDARequestError): 86 | """ error: unknown error, message: *** - """ 87 | @staticmethod 88 | def check(v: dict): 89 | return v.get("error") == "unknown error" 90 | 91 | 92 | class WDAStaleElementReferenceError(WDARequestError): 93 | """ error: 'stale element reference' """ 94 | @staticmethod 95 | def check(v: dict): 96 | return v.get("error") == 'stale element reference' 97 | -------------------------------------------------------------------------------- /wda/usbmux/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """Created on Thu Dec 09 2021 09:56:30 by codeskyblue 5 | """ 6 | 7 | import json 8 | from http.client import HTTPConnection, HTTPSConnection, HTTPResponse 9 | from urllib.parse import urlparse 10 | 11 | from wda.usbmux.exceptions import HTTPError, MuxConnectError, MuxError 12 | from wda.usbmux.pyusbmux import select_device 13 | 14 | _DEFAULT_CHUNK_SIZE = 4096 15 | 16 | def http_create(url: str) -> HTTPConnection: 17 | u = urlparse(url) 18 | if u.scheme == "http+usbmux": 19 | udid, device_wda_port = u.netloc.split(":") 20 | device = select_device(udid) 21 | return device.make_http_connection(int(device_wda_port)) 22 | elif u.scheme == "http": 23 | return HTTPConnection(u.netloc) 24 | elif u.scheme == "https": 25 | return HTTPSConnection(u.netloc) 26 | else: 27 | raise ValueError(f"unknown scheme: {u.scheme}") 28 | 29 | 30 | class HTTPResponseWrapper: 31 | def __init__(self, content: bytes, status_code: int): 32 | self.content = content 33 | self.status_code = status_code 34 | 35 | def json(self): 36 | return json.loads(self.content) 37 | 38 | @property 39 | def text(self) -> str: 40 | return self.content.decode("utf-8") 41 | 42 | def getcode(self) -> int: 43 | return self.status_code 44 | 45 | 46 | def fetch(url: str, method="GET", data=None, timeout=None, chunk_size: int = _DEFAULT_CHUNK_SIZE) -> HTTPResponseWrapper: 47 | """ 48 | thread safe http request 49 | 50 | Raises: 51 | HTTPError 52 | """ 53 | try: 54 | method = method.upper() 55 | conn = http_create(url) 56 | conn.timeout = timeout 57 | u = urlparse(url) 58 | urlpath = url[len(u.scheme) + len(u.netloc) + 3:] 59 | 60 | if not data: 61 | conn.request(method, urlpath) 62 | else: 63 | conn.request(method, urlpath, json.dumps(data), headers={"Content-Type": "application/json"}) 64 | response = conn.getresponse() 65 | content = _read_response(response, chunk_size) 66 | resp = HTTPResponseWrapper(content, response.status) 67 | return resp 68 | except Exception as e: 69 | raise HTTPError(e) 70 | 71 | 72 | def _read_response(response:HTTPResponse, chunk_size: int = _DEFAULT_CHUNK_SIZE) -> bytearray: 73 | content = bytearray() 74 | while True: 75 | chunk = response.read(chunk_size) 76 | if len(chunk) == 0: 77 | break 78 | content.extend(chunk) 79 | return content 80 | -------------------------------------------------------------------------------- /wda/usbmux/exceptions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """Created on Tue Mar 05 2024 10:18:09 by codeskyblue 5 | 6 | Copy from https://github.com/doronz88/pymobiledevice3 7 | """ 8 | 9 | class NotPairedError(Exception): 10 | pass 11 | 12 | 13 | class MuxError(Exception): 14 | pass 15 | 16 | 17 | class MuxVersionError(MuxError): 18 | pass 19 | 20 | 21 | class BadCommandError(MuxError): 22 | pass 23 | 24 | 25 | class BadDevError(MuxError): 26 | pass 27 | 28 | 29 | class MuxConnectError(MuxError): 30 | pass 31 | 32 | 33 | class MuxConnectToUsbmuxdError(MuxConnectError): 34 | pass 35 | 36 | 37 | class ArgumentError(Exception): 38 | pass 39 | 40 | 41 | class HTTPError(Exception): 42 | pass 43 | -------------------------------------------------------------------------------- /wda/usbmux/pyusbmux.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copy from https://github.com/doronz88/pymobiledevice3 3 | 4 | Add http.client.HTTPConnection 5 | """ 6 | import abc 7 | import plistlib 8 | import socket 9 | import sys 10 | import time 11 | from dataclasses import dataclass 12 | from http.client import HTTPConnection 13 | from typing import List, Mapping, Optional 14 | 15 | from construct import Const, CString, Enum, FixedSized, GreedyBytes, Int16ul, Int32ul, Padding, Prefixed, StreamError, \ 16 | Struct, Switch, this 17 | 18 | from wda.usbmux.exceptions import BadCommandError, BadDevError, MuxConnectError, \ 19 | MuxConnectToUsbmuxdError, MuxError, MuxVersionError, NotPairedError 20 | 21 | usbmuxd_version = Enum(Int32ul, 22 | BINARY=0, 23 | PLIST=1, 24 | ) 25 | 26 | usbmuxd_result = Enum(Int32ul, 27 | OK=0, 28 | BADCOMMAND=1, 29 | BADDEV=2, 30 | CONNREFUSED=3, 31 | BADVERSION=6, 32 | ) 33 | 34 | usbmuxd_msgtype = Enum(Int32ul, 35 | RESULT=1, 36 | CONNECT=2, 37 | LISTEN=3, 38 | ADD=4, 39 | REMOVE=5, 40 | PAIRED=6, 41 | PLIST=8, 42 | ) 43 | 44 | usbmuxd_header = Struct( 45 | 'version' / usbmuxd_version, # protocol version 46 | 'message' / usbmuxd_msgtype, # message type 47 | 'tag' / Int32ul, # responses to this query will echo back this tag 48 | ) 49 | 50 | usbmuxd_request = Prefixed(Int32ul, Struct( 51 | 'header' / usbmuxd_header, 52 | 'data' / Switch(this.header.message, { 53 | usbmuxd_msgtype.CONNECT: Struct( 54 | 'device_id' / Int32ul, 55 | 'port' / Int16ul, # TCP port number 56 | 'reserved' / Const(0, Int16ul), 57 | ), 58 | usbmuxd_msgtype.PLIST: GreedyBytes, 59 | }), 60 | ), includelength=True) 61 | 62 | usbmuxd_device_record = Struct( 63 | 'device_id' / Int32ul, 64 | 'product_id' / Int16ul, 65 | 'serial_number' / FixedSized(256, CString('ascii')), 66 | Padding(2), 67 | 'location' / Int32ul 68 | ) 69 | 70 | usbmuxd_response = Prefixed(Int32ul, Struct( 71 | 'header' / usbmuxd_header, 72 | 'data' / Switch(this.header.message, { 73 | usbmuxd_msgtype.RESULT: Struct( 74 | 'result' / usbmuxd_result, 75 | ), 76 | usbmuxd_msgtype.ADD: usbmuxd_device_record, 77 | usbmuxd_msgtype.REMOVE: Struct( 78 | 'device_id' / Int32ul, 79 | ), 80 | usbmuxd_msgtype.PLIST: GreedyBytes, 81 | }), 82 | ), includelength=True) 83 | 84 | 85 | 86 | 87 | @dataclass 88 | class MuxDevice: 89 | devid: int 90 | serial: str 91 | connection_type: str 92 | 93 | def connect(self, port: int, usbmux_address: Optional[str] = None) -> socket.socket: 94 | mux = create_mux(usbmux_address=usbmux_address) 95 | try: 96 | return mux.connect(self, port) 97 | except: # noqa: E722 98 | mux.close() 99 | raise 100 | 101 | @property 102 | def is_usb(self) -> bool: 103 | return self.connection_type == 'USB' 104 | 105 | @property 106 | def is_network(self) -> bool: 107 | return self.connection_type == 'Network' 108 | 109 | def matches_udid(self, udid: str) -> bool: 110 | return self.serial.replace('-', '') == udid.replace('-', '') 111 | 112 | def make_http_connection(self, port: int) -> HTTPConnection: 113 | return USBMuxHTTPConnection(self, port) 114 | 115 | 116 | class SafeStreamSocket: 117 | """ wrapper to native python socket object to be used with construct as a stream """ 118 | 119 | def __init__(self, address, family): 120 | self._offset = 0 121 | self.sock = socket.socket(family, socket.SOCK_STREAM) 122 | self.sock.connect(address) 123 | 124 | def send(self, msg: bytes) -> int: 125 | self._offset += len(msg) 126 | self.sock.sendall(msg) 127 | return len(msg) 128 | 129 | def recv(self, size: int) -> bytes: 130 | msg = b'' 131 | while len(msg) < size: 132 | chunk = self.sock.recv(size - len(msg)) 133 | self._offset += len(chunk) 134 | if not chunk: 135 | raise MuxError('socket connection broken') 136 | msg += chunk 137 | return msg 138 | 139 | def close(self) -> None: 140 | self.sock.close() 141 | 142 | def settimeout(self, interval: float) -> None: 143 | self.sock.settimeout(interval) 144 | 145 | def setblocking(self, blocking: bool) -> None: 146 | self.sock.setblocking(blocking) 147 | 148 | def tell(self) -> int: 149 | return self._offset 150 | 151 | read = recv 152 | write = send 153 | 154 | 155 | class MuxConnection: 156 | # used on Windows 157 | ITUNES_HOST = ('127.0.0.1', 27015) 158 | 159 | # used for macOS and Linux 160 | USBMUXD_PIPE = '/var/run/usbmuxd' 161 | 162 | @staticmethod 163 | def create_usbmux_socket(usbmux_address: Optional[str] = None) -> SafeStreamSocket: 164 | try: 165 | if usbmux_address is not None: 166 | if ':' in usbmux_address: 167 | # assume tcp address 168 | hostname, port = usbmux_address.split(':') 169 | port = int(port) 170 | address = (hostname, port) 171 | family = socket.AF_INET 172 | else: 173 | # assume unix domain address 174 | address = usbmux_address 175 | family = socket.AF_UNIX 176 | else: 177 | if sys.platform in ['win32', 'cygwin']: 178 | address = MuxConnection.ITUNES_HOST 179 | family = socket.AF_INET 180 | else: 181 | address = MuxConnection.USBMUXD_PIPE 182 | family = socket.AF_UNIX 183 | return SafeStreamSocket(address, family) 184 | except ConnectionRefusedError: 185 | raise MuxConnectToUsbmuxdError() 186 | 187 | @staticmethod 188 | def create(usbmux_address: Optional[str] = None): 189 | # first attempt to connect with possibly the wrong version header (plist protocol) 190 | sock = MuxConnection.create_usbmux_socket(usbmux_address=usbmux_address) 191 | 192 | message = usbmuxd_request.build({ 193 | 'header': {'version': usbmuxd_version.PLIST, 'message': usbmuxd_msgtype.PLIST, 'tag': 1}, 194 | 'data': plistlib.dumps({'MessageType': 'ReadBUID'}) 195 | }) 196 | sock.send(message) 197 | response = usbmuxd_response.parse_stream(sock) 198 | 199 | # if we sent a bad request, we should re-create the socket in the correct version this time 200 | sock.close() 201 | sock = MuxConnection.create_usbmux_socket(usbmux_address=usbmux_address) 202 | 203 | if response.header.version == usbmuxd_version.BINARY: 204 | return BinaryMuxConnection(sock) 205 | elif response.header.version == usbmuxd_version.PLIST: 206 | return PlistMuxConnection(sock) 207 | 208 | raise MuxVersionError(f'usbmuxd returned unsupported version: {response.version}') 209 | 210 | def __init__(self, sock: SafeStreamSocket): 211 | self._sock = sock 212 | 213 | # after initiating the "Connect" packet, this same socket will be used to transfer data into the service 214 | # residing inside the target device. when this happens, we can no longer send/receive control commands to 215 | # usbmux on same socket 216 | self._connected = False 217 | 218 | # message sequence number. used when verifying the response matched the request 219 | self._tag = 1 220 | 221 | self.devices = [] 222 | 223 | @abc.abstractmethod 224 | def _connect(self, device_id: int, port: int): 225 | """ initiate a "Connect" request to target port """ 226 | pass 227 | 228 | @abc.abstractmethod 229 | def get_device_list(self, timeout: float = None): 230 | """ 231 | request an update to current device list 232 | """ 233 | pass 234 | 235 | def connect(self, device: MuxDevice, port: int) -> socket.socket: 236 | """ connect to a relay port on target machine and get a raw python socket object for the connection """ 237 | self._connect(device.devid, socket.htons(port)) 238 | self._connected = True 239 | return self._sock.sock 240 | 241 | def close(self): 242 | """ close current socket """ 243 | self._sock.close() 244 | 245 | def _assert_not_connected(self): 246 | """ verify active state is in state for control messages """ 247 | if self._connected: 248 | raise MuxError('Mux is connected, cannot issue control packets') 249 | 250 | def _raise_mux_exception(self, result: int, message: str = None): 251 | exceptions = { 252 | int(usbmuxd_result.BADCOMMAND): BadCommandError, 253 | int(usbmuxd_result.BADDEV): BadDevError, 254 | int(usbmuxd_result.CONNREFUSED): MuxConnectError, 255 | int(usbmuxd_result.BADVERSION): MuxVersionError, 256 | } 257 | exception = exceptions.get(result, MuxError) 258 | raise exception(message) 259 | 260 | def __enter__(self): 261 | return self 262 | 263 | def __exit__(self, exc_type, exc_val, exc_tb): 264 | self.close() 265 | 266 | 267 | class BinaryMuxConnection(MuxConnection): 268 | """ old binary protocol """ 269 | 270 | def __init__(self, sock: SafeStreamSocket): 271 | super().__init__(sock) 272 | self._version = usbmuxd_version.BINARY 273 | 274 | def get_device_list(self, timeout: float = None): 275 | """ use timeout to wait for the device list to be fully populated """ 276 | self._assert_not_connected() 277 | end = time.time() + timeout 278 | self.listen() 279 | while time.time() < end: 280 | self._sock.settimeout(end - time.time()) 281 | try: 282 | self._receive_device_state_update() 283 | except (BlockingIOError, StreamError): 284 | continue 285 | except IOError: 286 | try: 287 | self._sock.setblocking(True) 288 | self.close() 289 | except OSError: 290 | pass 291 | raise MuxError('Exception in listener socket') 292 | 293 | def listen(self): 294 | """ start listening for events of attached and detached devices """ 295 | self._send_receive(usbmuxd_msgtype.LISTEN) 296 | 297 | def _connect(self, device_id: int, port: int): 298 | self._send({'header': {'version': self._version, 299 | 'message': usbmuxd_msgtype.CONNECT, 300 | 'tag': self._tag}, 301 | 'data': {'device_id': device_id, 'port': port}, 302 | }) 303 | response = self._receive() 304 | if response.header.message != usbmuxd_msgtype.RESULT: 305 | raise MuxError(f'unexpected message type received: {response}') 306 | 307 | if response.data.result != usbmuxd_result.OK: 308 | raise self._raise_mux_exception(int(response.data.result), 309 | f'failed to connect to device: {device_id} at port: {port}. reason: ' 310 | f'{response.data.result}') 311 | 312 | def _send(self, data: Mapping): 313 | self._assert_not_connected() 314 | self._sock.send(usbmuxd_request.build(data)) 315 | self._tag += 1 316 | 317 | def _receive(self, expected_tag: int = None): 318 | self._assert_not_connected() 319 | response = usbmuxd_response.parse_stream(self._sock) 320 | if expected_tag and response.header.tag != expected_tag: 321 | raise MuxError(f'Reply tag mismatch: expected {expected_tag}, got {response.header.tag}') 322 | return response 323 | 324 | def _send_receive(self, message_type: int): 325 | self._send({'header': {'version': self._version, 'message': message_type, 'tag': self._tag}, 326 | 'data': b''}) 327 | response = self._receive(self._tag - 1) 328 | if response.header.message != usbmuxd_msgtype.RESULT: 329 | raise MuxError(f'unexpected message type received: {response}') 330 | 331 | result = response.data.result 332 | if result != usbmuxd_result.OK: 333 | raise self._raise_mux_exception(int(result), f'{message_type} failed: error {result}') 334 | 335 | def _add_device(self, device: MuxDevice): 336 | self.devices.append(device) 337 | 338 | def _remove_device(self, device_id: int): 339 | self.devices = [device for device in self.devices if device.devid != device_id] 340 | 341 | def _receive_device_state_update(self): 342 | response = self._receive() 343 | if response.header.message == usbmuxd_msgtype.ADD: 344 | # old protocol only supported USB devices 345 | self._add_device(MuxDevice(response.data.device_id, response.data.serial_number, 'USB')) 346 | elif response.header.message == usbmuxd_msgtype.REMOVE: 347 | self._remove_device(response.data.device_id) 348 | else: 349 | raise MuxError(f'Invalid packet type received: {response}') 350 | 351 | 352 | class PlistMuxConnection(BinaryMuxConnection): 353 | def __init__(self, sock: SafeStreamSocket): 354 | super().__init__(sock) 355 | self._version = usbmuxd_version.PLIST 356 | 357 | def listen(self) -> None: 358 | self._send_receive({'MessageType': 'Listen'}) 359 | 360 | def get_pair_record(self, serial: str) -> Mapping: 361 | # serials are saved inside usbmuxd without '-' 362 | self._send({'MessageType': 'ReadPairRecord', 'PairRecordID': serial}) 363 | response = self._receive(self._tag - 1) 364 | pair_record = response.get('PairRecordData') 365 | if pair_record is None: 366 | raise NotPairedError('device should be paired first') 367 | return plistlib.loads(pair_record) 368 | 369 | def get_device_list(self, timeout: float = None) -> None: 370 | """ get device list synchronously without waiting the timeout """ 371 | self.devices = [] 372 | self._send({'MessageType': 'ListDevices'}) 373 | for response in self._receive(self._tag - 1)['DeviceList']: 374 | if response['MessageType'] == 'Attached': 375 | super()._add_device(MuxDevice(response['DeviceID'], response['Properties']['SerialNumber'], 376 | response['Properties']['ConnectionType'])) 377 | elif response['MessageType'] == 'Detached': 378 | super()._remove_device(response['DeviceID']) 379 | else: 380 | raise MuxError(f'Invalid packet type received: {response}') 381 | 382 | def get_buid(self) -> str: 383 | """ get SystemBUID """ 384 | self._send({'MessageType': 'ReadBUID'}) 385 | return self._receive(self._tag - 1)['BUID'] 386 | 387 | def save_pair_record(self, serial: str, device_id: int, record_data: bytes): 388 | # serials are saved inside usbmuxd without '-' 389 | self._send_receive({'MessageType': 'SavePairRecord', 390 | 'PairRecordID': serial, 391 | 'PairRecordData': record_data, 392 | 'DeviceID': device_id}) 393 | 394 | def _connect(self, device_id: int, port: int): 395 | self._send_receive({'MessageType': 'Connect', 'DeviceID': device_id, 'PortNumber': port}) 396 | 397 | def _send(self, data: Mapping): 398 | request = {'ClientVersionString': 'qt4i-usbmuxd', 'ProgName': 'pymobiledevice3', 'kLibUSBMuxVersion': 3} 399 | request.update(data) 400 | super()._send({'header': {'version': self._version, 401 | 'message': usbmuxd_msgtype.PLIST, 402 | 'tag': self._tag}, 403 | 'data': plistlib.dumps(request), 404 | }) 405 | 406 | def _receive(self, expected_tag: int = None) -> Mapping: 407 | response = super()._receive(expected_tag=expected_tag) 408 | if response.header.message != usbmuxd_msgtype.PLIST: 409 | raise MuxError(f'Received non-plist type {response}') 410 | return plistlib.loads(response.data) 411 | 412 | def _send_receive(self, data: Mapping): 413 | self._send(data) 414 | response = self._receive(self._tag - 1) 415 | if response['MessageType'] != 'Result': 416 | raise MuxError(f'got an invalid message: {response}') 417 | if response['Number'] != 0: 418 | raise self._raise_mux_exception(response['Number'], f'got an error message: {response}') 419 | 420 | 421 | def create_mux(usbmux_address: Optional[str] = None) -> MuxConnection: 422 | return MuxConnection.create(usbmux_address=usbmux_address) 423 | 424 | 425 | def list_devices(usbmux_address: Optional[str] = None) -> List[MuxDevice]: 426 | mux = create_mux(usbmux_address=usbmux_address) 427 | mux.get_device_list(0.1) 428 | devices = mux.devices 429 | mux.close() 430 | return devices 431 | 432 | 433 | def select_device(udid: str = None, connection_type: str = None, usbmux_address: Optional[str] = None) \ 434 | -> Optional[MuxDevice]: 435 | """ 436 | select a UsbMux device according to given arguments. 437 | if more than one device could be selected, always prefer the usb one. 438 | """ 439 | tmp = None 440 | for device in list_devices(usbmux_address=usbmux_address): 441 | if connection_type is not None and device.connection_type != connection_type: 442 | # if a specific connection_type was desired and not of this one then skip 443 | continue 444 | 445 | if udid is not None and not device.matches_udid(udid): 446 | # if a specific udid was desired and not of this one then skip 447 | continue 448 | 449 | # save best result as a temporary 450 | tmp = device 451 | 452 | if device.is_usb: 453 | # always prefer usb connection 454 | return device 455 | 456 | return tmp 457 | 458 | 459 | def select_devices_by_connection_type(connection_type: str, usbmux_address: Optional[str] = None) -> List[MuxDevice]: 460 | """ 461 | select all UsbMux devices by connection type 462 | """ 463 | tmp = [] 464 | for device in list_devices(usbmux_address=usbmux_address): 465 | if device.connection_type == connection_type: 466 | tmp.append(device) 467 | 468 | return tmp 469 | 470 | 471 | 472 | class USBMuxHTTPConnection(HTTPConnection): 473 | def __init__(self, device: MuxDevice, port=8100): 474 | super().__init__("localhost", port) 475 | self.__device = device 476 | self.__port = port 477 | 478 | def connect(self): 479 | self.sock = self.__device.connect(self.__port) 480 | 481 | def __enter__(self) -> HTTPConnection: 482 | return self 483 | 484 | def __exit__(self, exc_type, exc_value, traceback): 485 | self.close() -------------------------------------------------------------------------------- /wda/utils.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import functools 4 | import typing 5 | import inspect 6 | 7 | 8 | def inject_call(fn, *args, **kwargs): 9 | """ 10 | Call function without known all the arguments 11 | 12 | Args: 13 | fn: function 14 | args: arguments 15 | kwargs: key-values 16 | 17 | Returns: 18 | as the fn returns 19 | """ 20 | assert callable(fn), "first argument must be callable" 21 | 22 | st = inspect.signature(fn) 23 | fn_kwargs = { 24 | key: kwargs[key] 25 | for key in st.parameters.keys() if key in kwargs 26 | } 27 | ba = st.bind(*args, **fn_kwargs) 28 | ba.apply_defaults() 29 | return fn(*ba.args, **ba.kwargs) 30 | 31 | 32 | def limit_call_depth(n: int): 33 | """ 34 | n = 0 means not allowed recursive call 35 | """ 36 | def wrapper(fn: typing.Callable): 37 | if not hasattr(fn, '_depth'): 38 | fn._depth = 0 39 | 40 | @functools.wraps(fn) 41 | def _inner(*args, **kwargs): 42 | if fn._depth > n: 43 | raise RuntimeError("call depth exceed %d" % n) 44 | 45 | fn._depth += 1 46 | try: 47 | return fn(*args, **kwargs) 48 | finally: 49 | fn._depth -= 1 50 | 51 | _inner._fn = fn 52 | return _inner 53 | 54 | return wrapper 55 | 56 | 57 | class AttrDict(dict): 58 | def __getattr__(self, key): 59 | if isinstance(key, str) and key in self: 60 | return self[key] 61 | raise AttributeError("Attribute key not found", key) 62 | 63 | 64 | def convert(dictionary): 65 | """ 66 | Convert dict to namedtuple 67 | """ 68 | return AttrDict(dictionary) 69 | -------------------------------------------------------------------------------- /wda/xcui_element_types.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import unicode_literals 5 | 6 | 7 | ELEMENTS = [ 8 | 'Any', 9 | 'Other', 10 | 'Application', 11 | 'Group', 12 | 'Window', 13 | 'Sheet', 14 | 'Drawer', 15 | 'Alert', 16 | 'Dialog', 17 | 'Button', 18 | 'RadioButton', 19 | 'RadioGroup', 20 | 'CheckBox', 21 | 'DisclosureTriangle', 22 | 'PopUpButton', 23 | 'ComboBox', 24 | 'MenuButton', 25 | 'ToolbarButton', 26 | 'Popover', 27 | 'Keyboard', 28 | 'Key', 29 | 'NavigationBar', 30 | 'TabBar', 31 | 'TabGroup', 32 | 'Toolbar', 33 | 'StatusBar', 34 | 'Table', 35 | 'TableRow', 36 | 'TableColumn', 37 | 'Outline', 38 | 'OutlineRow', 39 | 'Browser', 40 | 'CollectionView', 41 | 'Slider', 42 | 'PageIndicator', 43 | 'ProgressIndicator', 44 | 'ActivityIndicator', 45 | 'SegmentedControl', 46 | 'Picker', 47 | 'PickerWheel', 48 | 'Switch', 49 | 'Toggle', 50 | 'Link', 51 | 'Image', 52 | 'Icon', 53 | 'SearchField', 54 | 'ScrollView', 55 | 'ScrollBar', 56 | 'StaticText', 57 | 'TextField', 58 | 'SecureTextField', 59 | 'DatePicker', 60 | 'TextView', 61 | 'Menu', 62 | 'MenuItem', 63 | 'MenuBar', 64 | 'MenuBarItem', 65 | 'Map', 66 | 'WebView', 67 | 'IncrementArrow', 68 | 'DecrementArrow', 69 | 'Timeline', 70 | 'RatingIndicator', 71 | 'ValueIndicator', 72 | 'SplitGroup', 73 | 'Splitter', 74 | 'RelevanceIndicator', 75 | 'ColorWell', 76 | 'HelpTag', 77 | 'Matte', 78 | 'DockItem', 79 | 'Ruler', 80 | 'RulerMarker', 81 | 'Grid', 82 | 'LevelIndicator', 83 | 'Cell', 84 | 'LayoutArea', 85 | 'LayoutItem', 86 | 'Handle', 87 | 'Stepper', 88 | 'Tab' 89 | ] 90 | --------------------------------------------------------------------------------