├── .github ├── ISSUE_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── functional-test.yml │ ├── pr-title.yml │ ├── publish.js.yml │ └── unit-test.yml ├── .gitignore ├── .mocharc.js ├── .npmrc ├── .releaserc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── eslint.config.mjs ├── index.js ├── lib ├── helpers.js ├── logger.js ├── simctl.js └── subcommands │ ├── addmedia.js │ ├── appinfo.js │ ├── boot.js │ ├── bootstatus.js │ ├── create.js │ ├── delete.js │ ├── erase.js │ ├── get_app_container.js │ ├── getenv.js │ ├── install.js │ ├── io.js │ ├── keychain.js │ ├── launch.js │ ├── list.js │ ├── location.js │ ├── openurl.js │ ├── pbcopy.js │ ├── pbpaste.js │ ├── privacy.js │ ├── push.js │ ├── shutdown.js │ ├── spawn.js │ ├── terminate.js │ ├── ui.js │ └── uninstall.js ├── package.json ├── test ├── e2e │ └── simctl-e2e-specs.js └── unit │ ├── fixtures │ ├── devices-simple.json │ ├── devices-with-unavailable-simple.json │ ├── devices-with-unavailable.json │ └── devices.json │ └── simctl-specs.cjs └── tsconfig.json /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Warning:** 2 | 3 | These issues are not tracked. Please create new issues in the main Appium 4 | repository: https://github.com/appium/appium/issues/new 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "11:00" 8 | open-pull-requests-limit: 10 9 | commit-message: 10 | prefix: "chore" 11 | include: "scope" 12 | -------------------------------------------------------------------------------- /.github/workflows/functional-test.yml: -------------------------------------------------------------------------------- 1 | name: Functional Tests 2 | 3 | on: [pull_request] 4 | 5 | 6 | jobs: 7 | test: 8 | 9 | # https://github.com/actions/runner-images/tree/main/images/macos 10 | strategy: 11 | matrix: 12 | include: 13 | - xcodeVersion: '16.4' 14 | iosVersion: '18.5' 15 | deviceName: 'iPhone 16' 16 | platform: macos-15 17 | - xcodeVersion: '15.4' 18 | iosVersion: '17.5' 19 | deviceName: 'iPhone 15' 20 | platform: macos-14 21 | fail-fast: false 22 | 23 | env: 24 | CI: true 25 | _FORCE_LOGS: 1 26 | DEVICE_NAME: ${{ matrix.deviceName }} 27 | XCODE_VERSION: ${{ matrix.xcodeVersion }} 28 | IOS_SDK: ${{ matrix.iosVersion }} 29 | runs-on: ${{ matrix.platform }} 30 | steps: 31 | - uses: actions/checkout@v3 32 | - uses: actions/setup-node@v3 33 | with: 34 | node-version: lts/* 35 | check-latest: true 36 | - uses: maxim-lobanov/setup-xcode@v1 37 | with: 38 | xcode-version: "${{ matrix.xcodeVersion }}" 39 | - run: xcrun simctl list 40 | name: List devices 41 | - run: npm install 42 | name: Install dev dependencies 43 | - run: npm run e2e-test 44 | name: Run e2e tests on Xcode@${{ matrix.xcodeVersion }} 45 | -------------------------------------------------------------------------------- /.github/workflows/pr-title.yml: -------------------------------------------------------------------------------- 1 | name: Conventional Commits 2 | on: 3 | pull_request: 4 | types: [opened, edited, synchronize, reopened] 5 | 6 | 7 | jobs: 8 | lint: 9 | name: https://www.conventionalcommits.org 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: beemojs/conventional-pr-action@v3 13 | with: 14 | config-preset: angular 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | -------------------------------------------------------------------------------- /.github/workflows/publish.js.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ master ] 7 | 8 | permissions: 9 | contents: write 10 | pull-requests: write 11 | issues: write 12 | 13 | jobs: 14 | build: 15 | runs-on: macos-latest 16 | steps: 17 | - uses: actions/checkout@v2 18 | - uses: actions/setup-node@v3 19 | with: 20 | node-version: lts/* 21 | check-latest: true 22 | - run: npm install --no-package-lock 23 | name: Install dev dependencies 24 | - run: npm run build 25 | name: Run build 26 | - run: npm run test 27 | name: Run test 28 | - run: npx semantic-release 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 32 | name: Release 33 | 34 | -------------------------------------------------------------------------------- /.github/workflows/unit-test.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | 3 | on: [pull_request, push] 4 | 5 | 6 | jobs: 7 | prepare_matrix: 8 | runs-on: ubuntu-latest 9 | outputs: 10 | versions: ${{ steps.generate-matrix.outputs.lts }} 11 | steps: 12 | - name: Select LTS versions of Node.js 13 | id: generate-matrix 14 | uses: msimerson/node-lts-versions@v1 15 | 16 | test: 17 | needs: 18 | - prepare_matrix 19 | strategy: 20 | matrix: 21 | node-version: ${{ fromJSON(needs.prepare_matrix.outputs.versions) }} 22 | runs-on: macos-latest 23 | steps: 24 | - uses: actions/checkout@v3 25 | - uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | - run: npm install --no-package-lock 29 | name: Install dev dependencies 30 | - run: npm run lint 31 | name: Run linter 32 | - run: npm run test 33 | name: Run unit tests 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | *.log 4 | coverage 5 | package-lock.json* 6 | -------------------------------------------------------------------------------- /.mocharc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | require: ['ts-node/register'], 3 | forbidOnly: Boolean(process.env.CI) 4 | }; 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | ["@semantic-release/commit-analyzer", { 4 | "preset": "angular", 5 | "releaseRules": [ 6 | {"type": "chore", "release": "patch"} 7 | ] 8 | }], 9 | ["@semantic-release/release-notes-generator", { 10 | "preset": "conventionalcommits", 11 | "presetConfig": { 12 | "types": [ 13 | {"type": "feat", "section": "Features"}, 14 | {"type": "fix", "section": "Bug Fixes"}, 15 | {"type": "perf", "section": "Performance Improvements"}, 16 | {"type": "revert", "section": "Reverts"}, 17 | {"type": "chore", "section": "Miscellaneous Chores"}, 18 | {"type": "refactor", "section": "Code Refactoring"}, 19 | {"type": "docs", "section": "Documentation", "hidden": true}, 20 | {"type": "style", "section": "Styles", "hidden": true}, 21 | {"type": "test", "section": "Tests", "hidden": true}, 22 | {"type": "build", "section": "Build System", "hidden": true}, 23 | {"type": "ci", "section": "Continuous Integration", "hidden": true} 24 | ] 25 | } 26 | }], 27 | ["@semantic-release/changelog", { 28 | "changelogFile": "CHANGELOG.md" 29 | }], 30 | "@semantic-release/npm", 31 | ["@semantic-release/git", { 32 | "assets": ["docs", "package.json", "CHANGELOG.md"], 33 | "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" 34 | }], 35 | "@semantic-release/github" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [8.0.5](https://github.com/appium/node-simctl/compare/v8.0.4...v8.0.5) (2025-10-17) 2 | 3 | ### Miscellaneous Chores 4 | 5 | * **deps-dev:** bump semantic-release from 24.2.9 to 25.0.0 ([#274](https://github.com/appium/node-simctl/issues/274)) ([c56ecaa](https://github.com/appium/node-simctl/commit/c56ecaa45471c0e42d71346bd481cecf53c44e67)) 6 | 7 | ## [8.0.4](https://github.com/appium/node-simctl/compare/v8.0.3...v8.0.4) (2025-10-09) 8 | 9 | ### Miscellaneous Chores 10 | 11 | * **deps:** bump uuid from 11.1.0 to 13.0.0 ([#272](https://github.com/appium/node-simctl/issues/272)) ([e15f90d](https://github.com/appium/node-simctl/commit/e15f90d93f7c759de27b55cd506e6b21a126e244)) 12 | 13 | ## [8.0.3](https://github.com/appium/node-simctl/compare/v8.0.2...v8.0.3) (2025-08-23) 14 | 15 | ### Miscellaneous Chores 16 | 17 | * **deps-dev:** bump chai from 5.3.2 to 6.0.0 ([#270](https://github.com/appium/node-simctl/issues/270)) ([9a47873](https://github.com/appium/node-simctl/commit/9a478734c6197cc037398357abdfa50424975324)) 18 | 19 | ## [8.0.2](https://github.com/appium/node-simctl/compare/v8.0.1...v8.0.2) (2025-08-20) 20 | 21 | ### Miscellaneous Chores 22 | 23 | * **deps-dev:** bump appium-xcode from 5.2.23 to 6.0.0 ([#269](https://github.com/appium/node-simctl/issues/269)) ([3822395](https://github.com/appium/node-simctl/commit/3822395a7763e176da7ad712120881af49881c20)) 24 | 25 | ## [8.0.1](https://github.com/appium/node-simctl/compare/v8.0.0...v8.0.1) (2025-08-17) 26 | 27 | ### Miscellaneous Chores 28 | 29 | * **deps:** bump rimraf from 5.0.9 to 6.0.1 ([#252](https://github.com/appium/node-simctl/issues/252)) ([8cad9b9](https://github.com/appium/node-simctl/commit/8cad9b99b7f23fad9cbfe84315a6c439b9af74bc)) 30 | 31 | ## [8.0.0](https://github.com/appium/node-simctl/compare/v7.7.5...v8.0.0) (2025-08-16) 32 | 33 | ### ⚠ BREAKING CHANGES 34 | 35 | * Required Node.js version has been bumped to ^20.19.0 || ^22.12.0 || >=24.0.0 36 | * Required npm version has been bumped to >=10 37 | 38 | ### Features 39 | 40 | * Bump Node.js version ([#268](https://github.com/appium/node-simctl/issues/268)) ([fe51657](https://github.com/appium/node-simctl/commit/fe51657f423844431941ecd754d81f0b7dda30f0)) 41 | 42 | ## [7.7.5](https://github.com/appium/node-simctl/compare/v7.7.4...v7.7.5) (2025-06-13) 43 | 44 | ### Miscellaneous Chores 45 | 46 | * **deps-dev:** bump sinon from 20.0.0 to 21.0.0 ([#267](https://github.com/appium/node-simctl/issues/267)) ([4c0cec9](https://github.com/appium/node-simctl/commit/4c0cec98928c0aa54c8e10ff1b497879c84b7949)) 47 | 48 | ## [7.7.4](https://github.com/appium/node-simctl/compare/v7.7.3...v7.7.4) (2025-06-10) 49 | 50 | ### Miscellaneous Chores 51 | 52 | * **deps-dev:** bump @types/node from 22.15.31 to 24.0.0 ([#266](https://github.com/appium/node-simctl/issues/266)) ([8642c14](https://github.com/appium/node-simctl/commit/8642c14377f35b36f83c1dd8fe92800b7d672616)) 53 | 54 | ## [7.7.3](https://github.com/appium/node-simctl/compare/v7.7.2...v7.7.3) (2025-05-21) 55 | 56 | ### Miscellaneous Chores 57 | 58 | * **deps-dev:** bump conventional-changelog-conventionalcommits ([#265](https://github.com/appium/node-simctl/issues/265)) ([ec3c3d5](https://github.com/appium/node-simctl/commit/ec3c3d5e4f2fa1f4255def5a2be8a82648ee2424)) 59 | 60 | ## [7.7.2](https://github.com/appium/node-simctl/compare/v7.7.1...v7.7.2) (2025-03-25) 61 | 62 | ### Miscellaneous Chores 63 | 64 | * **deps-dev:** bump sinon from 19.0.5 to 20.0.0 ([#264](https://github.com/appium/node-simctl/issues/264)) ([e7639f7](https://github.com/appium/node-simctl/commit/e7639f7781be0e87f5e6b73ca6aa388d59c2a5b3)) 65 | 66 | ## [7.7.1](https://github.com/appium/node-simctl/compare/v7.7.0...v7.7.1) (2025-01-27) 67 | 68 | ### Bug Fixes 69 | 70 | * add definitions in Simctl ([#263](https://github.com/appium/node-simctl/issues/263)) ([60ee831](https://github.com/appium/node-simctl/commit/60ee831765369e7cd9b3c91693a984a3c6d7ea1d)) 71 | 72 | ## [7.7.0](https://github.com/appium/node-simctl/compare/v7.6.5...v7.7.0) (2025-01-26) 73 | 74 | ### Features 75 | 76 | * add content_size and increase_contrast commands for ui ([#262](https://github.com/appium/node-simctl/issues/262)) ([379c933](https://github.com/appium/node-simctl/commit/379c9332687d51b08f04553f28ef51ee3f2ec927)) 77 | 78 | ## [7.6.5](https://github.com/appium/node-simctl/compare/v7.6.4...v7.6.5) (2025-01-05) 79 | 80 | ### Miscellaneous Chores 81 | 82 | * **deps:** bump which from 4.0.0 to 5.0.0 ([#257](https://github.com/appium/node-simctl/issues/257)) ([9d158a5](https://github.com/appium/node-simctl/commit/9d158a5824fcd2b39db34fba71adbb2c8b4fdee1)) 83 | 84 | ## [7.6.4](https://github.com/appium/node-simctl/compare/v7.6.3...v7.6.4) (2025-01-03) 85 | 86 | ### Miscellaneous Chores 87 | 88 | * **deps-dev:** bump @appium/eslint-config-appium-ts from 0.3.3 to 1.0.1 ([#261](https://github.com/appium/node-simctl/issues/261)) ([d5f22e9](https://github.com/appium/node-simctl/commit/d5f22e9bf5f40a576a393425bf5a527d535dd155)) 89 | 90 | ## [7.6.3](https://github.com/appium/node-simctl/compare/v7.6.2...v7.6.3) (2024-12-03) 91 | 92 | ### Miscellaneous Chores 93 | 94 | * **deps-dev:** bump mocha from 10.8.2 to 11.0.1 ([#260](https://github.com/appium/node-simctl/issues/260)) ([2effed6](https://github.com/appium/node-simctl/commit/2effed6f7e9260f53762da7e9de5a919edd3dbab)) 95 | 96 | ## [7.6.2](https://github.com/appium/node-simctl/compare/v7.6.1...v7.6.2) (2024-10-28) 97 | 98 | ### Miscellaneous Chores 99 | 100 | * **deps:** bump uuid from 10.0.0 to 11.0.1 ([#259](https://github.com/appium/node-simctl/issues/259)) ([642cbfe](https://github.com/appium/node-simctl/commit/642cbfef8c8d3720aec6ffc0861e486ec0e443a2)) 101 | 102 | ## [7.6.1](https://github.com/appium/node-simctl/compare/v7.6.0...v7.6.1) (2024-09-13) 103 | 104 | ### Miscellaneous Chores 105 | 106 | * **deps-dev:** bump sinon from 18.0.1 to 19.0.1 ([#256](https://github.com/appium/node-simctl/issues/256)) ([2578c1e](https://github.com/appium/node-simctl/commit/2578c1ed41751fbb33d21fe1202ed54df7fed8da)) 107 | 108 | ## [7.6.0](https://github.com/appium/node-simctl/compare/v7.5.5...v7.6.0) (2024-08-31) 109 | 110 | ### Features 111 | 112 | * add timeout option for exec ([#254](https://github.com/appium/node-simctl/issues/254)) ([1c2e1d8](https://github.com/appium/node-simctl/commit/1c2e1d829815f1e465c0497b0e87914e56222e8e)) 113 | 114 | ## [7.5.5](https://github.com/appium/node-simctl/compare/v7.5.4...v7.5.5) (2024-08-29) 115 | 116 | ### Bug Fixes 117 | 118 | * Update tests for node.js 22.7 ([#255](https://github.com/appium/node-simctl/issues/255)) ([723568b](https://github.com/appium/node-simctl/commit/723568b320364051289e94dda384fd25b50f19d9)) 119 | 120 | ## [7.5.4](https://github.com/appium/node-simctl/compare/v7.5.3...v7.5.4) (2024-07-29) 121 | 122 | ### Miscellaneous Chores 123 | 124 | * **deps-dev:** bump @types/node from 20.14.13 to 22.0.0 ([#253](https://github.com/appium/node-simctl/issues/253)) ([f9d7303](https://github.com/appium/node-simctl/commit/f9d73033bca8eb8627127f6d72e9d936db1201d0)) 125 | 126 | ## [7.5.3](https://github.com/appium/node-simctl/compare/v7.5.2...v7.5.3) (2024-07-03) 127 | 128 | ### Miscellaneous Chores 129 | 130 | * Simplify booting status monitoring ([#250](https://github.com/appium/node-simctl/issues/250)) ([508a2e5](https://github.com/appium/node-simctl/commit/508a2e55021c8bc8119c07ba20fbc2073bd84192)) 131 | 132 | ## [7.5.2](https://github.com/appium/node-simctl/compare/v7.5.1...v7.5.2) (2024-06-18) 133 | 134 | ### Miscellaneous Chores 135 | 136 | * Bump chai and chai-as-promised ([#249](https://github.com/appium/node-simctl/issues/249)) ([7572c9c](https://github.com/appium/node-simctl/commit/7572c9c326205a1737227c603a347f1c21f7ddda)) 137 | 138 | ## [7.5.1](https://github.com/appium/node-simctl/compare/v7.5.0...v7.5.1) (2024-06-17) 139 | 140 | ### Miscellaneous Chores 141 | 142 | * **deps:** bump uuid from 9.0.1 to 10.0.0 ([#247](https://github.com/appium/node-simctl/issues/247)) ([415dcea](https://github.com/appium/node-simctl/commit/415dceac4fa3b81e137ac78bd248b9258af85533)) 143 | 144 | ## [7.5.0](https://github.com/appium/node-simctl/compare/v7.4.5...v7.5.0) (2024-06-07) 145 | 146 | ### Features 147 | 148 | * Replace the deprecated npmlog with @appium/logger fork ([#246](https://github.com/appium/node-simctl/issues/246)) ([059afe8](https://github.com/appium/node-simctl/commit/059afe8e3e2e97ec61c1bd0b28e4b159d3d31004)) 149 | 150 | ## [7.4.5](https://github.com/appium/node-simctl/compare/v7.4.4...v7.4.5) (2024-06-04) 151 | 152 | ### Miscellaneous Chores 153 | 154 | * **deps-dev:** bump semantic-release from 23.1.1 to 24.0.0 and conventional-changelog-conventionalcommits to 8.0.0 ([#245](https://github.com/appium/node-simctl/issues/245)) ([b793746](https://github.com/appium/node-simctl/commit/b793746404a46e28649d5949c3ced75066c4a9a5)) 155 | 156 | ## [7.4.4](https://github.com/appium/node-simctl/compare/v7.4.3...v7.4.4) (2024-05-16) 157 | 158 | 159 | ### Miscellaneous Chores 160 | 161 | * Update dev dependencies ([22b51c8](https://github.com/appium/node-simctl/commit/22b51c839455651b3d38d46a53ba19f48a3d557c)) 162 | 163 | ## [7.4.3](https://github.com/appium/node-simctl/compare/v7.4.2...v7.4.3) (2024-05-16) 164 | 165 | 166 | ### Miscellaneous Chores 167 | 168 | * **deps-dev:** bump sinon from 17.0.2 to 18.0.0 ([#244](https://github.com/appium/node-simctl/issues/244)) ([9bbf649](https://github.com/appium/node-simctl/commit/9bbf649edcabb06174948b0a4c8475acda82bbba)) 169 | 170 | ## [7.4.2](https://github.com/appium/node-simctl/compare/v7.4.1...v7.4.2) (2024-04-09) 171 | 172 | 173 | ### Miscellaneous Chores 174 | 175 | * **deps-dev:** bump @typescript-eslint/parser from 6.21.0 to 7.6.0 ([#240](https://github.com/appium/node-simctl/issues/240)) ([baa83c9](https://github.com/appium/node-simctl/commit/baa83c9783b8c0aa09e6c88aed5d9e58dbcceaa2)) 176 | * Remove extra imports ([3dbe75b](https://github.com/appium/node-simctl/commit/3dbe75b594cb7e6a993e7e0b7681c94372c79457)) 177 | 178 | ## [7.4.1](https://github.com/appium/node-simctl/compare/v7.4.0...v7.4.1) (2024-03-24) 179 | 180 | 181 | ### Bug Fixes 182 | 183 | * types pubishing ([6755d64](https://github.com/appium/node-simctl/commit/6755d64d8185df65963f51697f58f8cb3c02cdb8)) 184 | 185 | 186 | ### Miscellaneous Chores 187 | 188 | * **deps-dev:** bump semantic-release from 22.0.12 to 23.0.2 ([#230](https://github.com/appium/node-simctl/issues/230)) ([7c06e04](https://github.com/appium/node-simctl/commit/7c06e04e4913c51eeec61cb4dbd34d7c261abefc)) 189 | 190 | ## [7.4.0](https://github.com/appium/node-simctl/compare/v7.3.13...v7.4.0) (2024-03-24) 191 | 192 | 193 | ### Features 194 | 195 | * Add types to package.json ([#236](https://github.com/appium/node-simctl/issues/236)) ([fc33007](https://github.com/appium/node-simctl/commit/fc33007e8073287ae245c20696b1e583c8e3595f)) 196 | 197 | ## [7.3.13](https://github.com/appium/node-simctl/compare/v7.3.12...v7.3.13) (2023-11-08) 198 | 199 | 200 | ### Miscellaneous Chores 201 | 202 | * **deps-dev:** bump @types/sinon from 10.0.20 to 17.0.1 ([#222](https://github.com/appium/node-simctl/issues/222)) ([60e7dd1](https://github.com/appium/node-simctl/commit/60e7dd1f4b10b307406460f8cdf53a0a185284b7)) 203 | 204 | ## [7.3.12](https://github.com/appium/node-simctl/compare/v7.3.11...v7.3.12) (2023-11-01) 205 | 206 | 207 | ### Miscellaneous Chores 208 | 209 | * **deps:** bump asyncbox from 2.9.4 to 3.0.0 ([#220](https://github.com/appium/node-simctl/issues/220)) ([8085447](https://github.com/appium/node-simctl/commit/80854473df00942931401fa271077058f4203ff6)) 210 | 211 | ## [7.3.11](https://github.com/appium/node-simctl/compare/v7.3.10...v7.3.11) (2023-10-25) 212 | 213 | 214 | ### Miscellaneous Chores 215 | 216 | * **deps-dev:** bump @typescript-eslint/eslint-plugin from 5.62.0 to 6.9.0 ([#219](https://github.com/appium/node-simctl/issues/219)) ([f9cc806](https://github.com/appium/node-simctl/commit/f9cc80608b2bf72e0ac9db6c9c398d8236f041bd)) 217 | 218 | ## [7.3.10](https://github.com/appium/node-simctl/compare/v7.3.9...v7.3.10) (2023-10-24) 219 | 220 | 221 | ### Miscellaneous Chores 222 | 223 | * **deps-dev:** bump semantic-release from 21.1.2 to 22.0.5 ([#208](https://github.com/appium/node-simctl/issues/208)) ([bf4e580](https://github.com/appium/node-simctl/commit/bf4e580734e88df73820222adaf2c19c2d9f5f90)) 224 | * Use latest types ([3ce87f9](https://github.com/appium/node-simctl/commit/3ce87f95868e6b5cff3b8205c0d4c31a56813449)) 225 | 226 | ## [7.3.9](https://github.com/appium/node-simctl/compare/v7.3.8...v7.3.9) (2023-10-24) 227 | 228 | 229 | ### Miscellaneous Chores 230 | 231 | * **deps-dev:** bump sinon from 16.1.3 to 17.0.0 ([#218](https://github.com/appium/node-simctl/issues/218)) ([d21e964](https://github.com/appium/node-simctl/commit/d21e9645d5a529a918a8a73b1fdc33e90568d889)) 232 | 233 | ## [7.3.8](https://github.com/appium/node-simctl/compare/v7.3.7...v7.3.8) (2023-10-19) 234 | 235 | 236 | ### Miscellaneous Chores 237 | 238 | * Always use the latest types ([e44fdf5](https://github.com/appium/node-simctl/commit/e44fdf58830474ee70eda47600477efa4b87dab1)) 239 | * **deps-dev:** bump @types/teen_process from 2.0.1 to 2.0.2 ([#214](https://github.com/appium/node-simctl/issues/214)) ([e53930b](https://github.com/appium/node-simctl/commit/e53930bbafdcf0931729966f09e7a79a795c7969)) 240 | * **deps-dev:** bump eslint-config-prettier from 8.10.0 to 9.0.0 ([#217](https://github.com/appium/node-simctl/issues/217)) ([9b7ba38](https://github.com/appium/node-simctl/commit/9b7ba38d719a1ecc63efdffa8878309b1d6b24ec)) 241 | * **deps-dev:** bump lint-staged from 14.0.1 to 15.0.2 ([#216](https://github.com/appium/node-simctl/issues/216)) ([462f131](https://github.com/appium/node-simctl/commit/462f131e3cd6e663e4f790b586ffa31e34c3edbc)) 242 | * Use latest teen_process types ([e967251](https://github.com/appium/node-simctl/commit/e9672515b6914dd626cda0d686828b9866f13a24)) 243 | 244 | ## [7.3.7](https://github.com/appium/node-simctl/compare/v7.3.6...v7.3.7) (2023-09-21) 245 | 246 | 247 | ### Bug Fixes 248 | 249 | * Tune conditional type declarations ([#206](https://github.com/appium/node-simctl/issues/206)) ([a44b0d2](https://github.com/appium/node-simctl/commit/a44b0d2a57c6532c9feff3a65c46bd3a067d2bfb)) 250 | 251 | ## [7.3.6](https://github.com/appium/node-simctl/compare/v7.3.5...v7.3.6) (2023-09-21) 252 | 253 | 254 | ### Miscellaneous Chores 255 | 256 | * Import all mixin methods explicitly ([#205](https://github.com/appium/node-simctl/issues/205)) ([c691f20](https://github.com/appium/node-simctl/commit/c691f20ef4d843349b3c8ae0847b0a0d896482a0)) 257 | 258 | ## [7.3.5](https://github.com/appium/node-simctl/compare/v7.3.4...v7.3.5) (2023-09-14) 259 | 260 | 261 | ### Miscellaneous Chores 262 | 263 | * **deps-dev:** bump @types/teen_process from 2.0.0 to 2.0.1 ([#201](https://github.com/appium/node-simctl/issues/201)) ([24b224c](https://github.com/appium/node-simctl/commit/24b224cec5124990f5098f474bbffc4749830776)) 264 | * **deps-dev:** bump sinon from 15.2.0 to 16.0.0 ([#202](https://github.com/appium/node-simctl/issues/202)) ([e436b63](https://github.com/appium/node-simctl/commit/e436b632962a87cdea543e1395bd83307dd2427f)) 265 | 266 | ## [7.3.4](https://github.com/appium/node-simctl/compare/v7.3.3...v7.3.4) (2023-09-01) 267 | 268 | 269 | ### Miscellaneous Chores 270 | 271 | * Improve type declarations ([#198](https://github.com/appium/node-simctl/issues/198)) ([314df12](https://github.com/appium/node-simctl/commit/314df12dfa9f2e7afa06d67f211b3dffe84755dc)) 272 | 273 | ## [7.3.3](https://github.com/appium/node-simctl/compare/v7.3.2...v7.3.3) (2023-08-31) 274 | 275 | 276 | ### Miscellaneous Chores 277 | 278 | * **deps:** bump which from 3.0.1 to 4.0.0 ([#197](https://github.com/appium/node-simctl/issues/197)) ([fa2d8c3](https://github.com/appium/node-simctl/commit/fa2d8c344cc8623af2504fd8bc103f1e948183f4)) 279 | 280 | ## [7.3.2](https://github.com/appium/node-simctl/compare/v7.3.1...v7.3.2) (2023-08-28) 281 | 282 | 283 | ### Miscellaneous Chores 284 | 285 | * **deps-dev:** bump conventional-changelog-conventionalcommits ([#195](https://github.com/appium/node-simctl/issues/195)) ([bd066dc](https://github.com/appium/node-simctl/commit/bd066dc151dd205d4da36c1f65b71478958d1d07)) 286 | 287 | ## [7.3.1](https://github.com/appium/node-simctl/compare/v7.3.0...v7.3.1) (2023-08-25) 288 | 289 | 290 | ### Miscellaneous Chores 291 | 292 | * **deps-dev:** bump semantic-release from 20.1.3 to 21.1.0 ([#194](https://github.com/appium/node-simctl/issues/194)) ([2943a2f](https://github.com/appium/node-simctl/commit/2943a2f27095a9df2ec5965d3dd9dbd82d4c26ad)) 293 | 294 | ## [7.3.0](https://github.com/appium/node-simctl/compare/v7.2.2...v7.3.0) (2023-08-18) 295 | 296 | 297 | ### Features 298 | 299 | * Switch from babel to typescript ([#190](https://github.com/appium/node-simctl/issues/190)) ([bed549f](https://github.com/appium/node-simctl/commit/bed549f4be5c0261ebc2e479fdbed68db963fc15)) 300 | 301 | ## [7.2.2](https://github.com/appium/node-simctl/compare/v7.2.1...v7.2.2) (2023-08-14) 302 | 303 | 304 | ### Miscellaneous Chores 305 | 306 | * **deps-dev:** bump lint-staged from 13.3.0 to 14.0.0 ([#189](https://github.com/appium/node-simctl/issues/189)) ([a42aaf5](https://github.com/appium/node-simctl/commit/a42aaf5698deadc722c909c1e505b8fbeccaecb4)) 307 | 308 | ## [7.2.1](https://github.com/appium/node-simctl/compare/v7.2.0...v7.2.1) (2023-08-10) 309 | 310 | 311 | ### Bug Fixes 312 | 313 | * Construct proper execution arguments ([68295e8](https://github.com/appium/node-simctl/commit/68295e8247c0b11620fc231cc7e5f4ec143e363d)) 314 | 315 | ## [7.2.0](https://github.com/appium/node-simctl/compare/v7.1.17...v7.2.0) (2023-08-09) 316 | 317 | 318 | ### Features 319 | 320 | * Add a possibility to select architecture while executing xcrun commands ([#188](https://github.com/appium/node-simctl/issues/188)) ([ab555d7](https://github.com/appium/node-simctl/commit/ab555d73d855991360af842f140a4164af50b6a6)) 321 | 322 | ## [7.1.17](https://github.com/appium/node-simctl/compare/v7.1.16...v7.1.17) (2023-07-07) 323 | 324 | 325 | ### Miscellaneous Chores 326 | 327 | * **deps-dev:** bump prettier from 2.8.8 to 3.0.0 ([#187](https://github.com/appium/node-simctl/issues/187)) ([5d2523c](https://github.com/appium/node-simctl/commit/5d2523c823b686c2cb4207200d4b582bfb88120f)) 328 | 329 | ## [7.1.16](https://github.com/appium/node-simctl/compare/v7.1.15...v7.1.16) (2023-06-07) 330 | 331 | 332 | ### Miscellaneous Chores 333 | 334 | * **deps-dev:** bump conventional-changelog-conventionalcommits ([#183](https://github.com/appium/node-simctl/issues/183)) ([398ab1c](https://github.com/appium/node-simctl/commit/398ab1c7893eb6ef23040462e7e8282faf6ecda9)) 335 | 336 | ## [7.1.15](https://github.com/appium/node-simctl/compare/v7.1.14...v7.1.15) (2023-05-02) 337 | 338 | 339 | ### Miscellaneous Chores 340 | 341 | * **deps:** bump npmlog from 6.0.2 to 7.0.1 ([#166](https://github.com/appium/node-simctl/issues/166)) ([0ca5cb1](https://github.com/appium/node-simctl/commit/0ca5cb17c5c19c551325a7f5f7b877895d27cad2)) 342 | 343 | ## [7.1.14](https://github.com/appium/node-simctl/compare/v7.1.13...v7.1.14) (2023-05-02) 344 | 345 | 346 | ### Miscellaneous Chores 347 | 348 | * **deps:** bump rimraf from 4.4.1 to 5.0.0 ([#178](https://github.com/appium/node-simctl/issues/178)) ([5c40d7d](https://github.com/appium/node-simctl/commit/5c40d7dd3e7d213210a67fd5067f26c0324ff255)) 349 | 350 | ## [7.1.13](https://github.com/appium/node-simctl/compare/v7.1.12...v7.1.13) (2023-05-02) 351 | 352 | 353 | ### Miscellaneous Chores 354 | 355 | * **deps:** bump which from 2.0.2 to 3.0.1 ([#180](https://github.com/appium/node-simctl/issues/180)) ([0152093](https://github.com/appium/node-simctl/commit/0152093a7a2d3262d2da4c8f94c3b905bfbafce8)) 356 | 357 | ## [7.1.12](https://github.com/appium/node-simctl/compare/v7.1.11...v7.1.12) (2023-02-25) 358 | 359 | 360 | ### Bug Fixes 361 | 362 | * Update rimraf and fs promises usage ([#174](https://github.com/appium/node-simctl/issues/174)) ([3c2f510](https://github.com/appium/node-simctl/commit/3c2f5107dc51c92898afd44a0a2da99ddc65ed8b)) 363 | 364 | ## [7.1.11](https://github.com/appium/node-simctl/compare/v7.1.10...v7.1.11) (2023-01-17) 365 | 366 | 367 | ### Miscellaneous Chores 368 | 369 | * **deps-dev:** bump semantic-release from 19.0.5 to 20.0.2 ([#171](https://github.com/appium/node-simctl/issues/171)) ([4d9294c](https://github.com/appium/node-simctl/commit/4d9294c078e2746fd4d39f0d4f6ae8b572ad518b)) 370 | 371 | ## [7.1.10](https://github.com/appium/node-simctl/compare/v7.1.9...v7.1.10) (2023-01-13) 372 | 373 | 374 | ### Miscellaneous Chores 375 | 376 | * **deps-dev:** bump appium-xcode from 4.0.5 to 5.0.0 ([#173](https://github.com/appium/node-simctl/issues/173)) ([474fcd4](https://github.com/appium/node-simctl/commit/474fcd4a4c799edccd434793a8b002977bc05fd9)) 377 | 378 | ## [7.1.9](https://github.com/appium/node-simctl/compare/v7.1.8...v7.1.9) (2023-01-13) 379 | 380 | 381 | ### Miscellaneous Chores 382 | 383 | * **deps:** bump rimraf from 3.0.2 to 4.0.4 ([#172](https://github.com/appium/node-simctl/issues/172)) ([93bce62](https://github.com/appium/node-simctl/commit/93bce628ff65f53695f1c91a0cc13ed01428c3b9)) 384 | 385 | ## [7.1.8](https://github.com/appium/node-simctl/compare/v7.1.7...v7.1.8) (2022-12-01) 386 | 387 | 388 | ### Miscellaneous Chores 389 | 390 | * update releaserc ([#170](https://github.com/appium/node-simctl/issues/170)) ([378e58f](https://github.com/appium/node-simctl/commit/378e58fdfe10f163aaf0e2be510c12d20d440c17)) 391 | 392 | ## [7.1.7](https://github.com/appium/node-simctl/compare/v7.1.6...v7.1.7) (2022-11-29) 393 | 394 | ## [7.1.6](https://github.com/appium/node-simctl/compare/v7.1.5...v7.1.6) (2022-11-06) 395 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright JS Foundation and other contributors, https://js.foundation 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## node-simctl 2 | 3 | [![NPM version](http://img.shields.io/npm/v/node-simctl.svg)](https://npmjs.org/package/node-simctl) 4 | [![Downloads](http://img.shields.io/npm/dm/node-simctl.svg)](https://npmjs.org/package/node-simctl) 5 | 6 | [![Release](https://github.com/appium/node-simctl/actions/workflows/publish.js.yml/badge.svg?branch=master)](https://github.com/appium/node-simctl/actions/workflows/publish.js.yml) 7 | 8 | ES6/7 Node wrapper around Apple's `simctl` binary, the "Command line utility to control the iOS Simulator". `simctl` is run as a sub-command of [xcrun](https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man1/xcrun.1.html) 9 | 10 | ### Installation 11 | 12 | Install through [npm](https://www.npmjs.com/package/node-simctl). 13 | 14 | ``` 15 | npm install node-simctl 16 | ``` 17 | 18 | ### API 19 | 20 | The module exports single class `Simctl`. This class contains methods 21 | which wrap the following simctl subcommands: 22 | 23 | #### create (Create a new device) 24 | - createDevice `*` 25 | 26 | #### clone (Clone an existing device) 27 | _None_ 28 | 29 | #### upgrade (Upgrade a device to a newer runtime) 30 | _None_ 31 | 32 | #### delete (Delete specified devices, unavailable devices, or all devices) 33 | - deleteDevice 34 | 35 | #### pair (Create a new watch and phone pair) 36 | _None_ 37 | 38 | #### unpair (Unpair a watch and phone pair) 39 | _None_ 40 | 41 | #### pair_activate (Set a given pair as active) 42 | _None_ 43 | 44 | #### erase (Erase a device's contents and settings) 45 | - eraseDevice 46 | 47 | #### boot (Boot a device) 48 | - bootDevice 49 | 50 | #### shutdown (Shutdown a device) 51 | - shutdownDevice 52 | 53 | #### rename (Rename a device) 54 | _None_ 55 | 56 | #### getenv (Print an environment variable from a running device) 57 | - getEnv 58 | 59 | #### openurl (Open a URL in a device) 60 | - openUrl 61 | 62 | #### addmedia (Add photos, live photos, videos, or contacts to the library of a device) 63 | - addMedia 64 | 65 | #### install (Install an app on a device) 66 | - installApp 67 | 68 | #### uninstall (Uninstall an app from a device) 69 | - uninstallApp 70 | 71 | #### get_app_container (Print the path of the installed app's container) 72 | - getAppContainer 73 | 74 | #### launch (Launch an application by identifier on a device) 75 | - launchApp 76 | 77 | #### terminate (Terminate an application by identifier on a device) 78 | - terminateApp 79 | 80 | #### spawn (Spawn a process by executing a given executable on a device) 81 | - spawnProcess 82 | - spawnSubProcess 83 | 84 | #### list (List available devices, device types, runtimes, or device pairs) 85 | - getDevicesByParsing `*` 86 | - getDevices `*` 87 | - getRuntimeForPlatformVersionViaJson `*` 88 | - getRuntimeForPlatformVersion `*` 89 | - getDeviceTypes `*` 90 | - list `*` 91 | 92 | #### icloud_sync (Trigger iCloud sync on a device) 93 | _None_ 94 | 95 | #### pbsync (Sync the pasteboard content from one pasteboard to another) 96 | _None_ 97 | 98 | #### pbcopy (Copy standard input onto the device pasteboard) 99 | - setPasteboard 100 | 101 | #### pbpaste (Print the contents of the device's pasteboard to standard output) 102 | - getPasteboard 103 | 104 | #### help (Prints the usage for a given subcommand) 105 | _None_ 106 | 107 | #### io (Set up a device IO operation) 108 | - getScreeenshot 109 | 110 | #### diagnose (Collect diagnostic information and logs) 111 | _None_ 112 | 113 | #### logverbose (enable or disable verbose logging for a device) 114 | _None_ 115 | 116 | #### status_bar (Set or clear status bar overrides) 117 | _None_ 118 | 119 | #### ui (Get or Set UI options) 120 | - getAppearance 121 | - setAppearance 122 | 123 | #### push (Send a simulated push notification) 124 | - pushNotification 125 | 126 | #### privacy (Grant, revoke, or reset privacy and permissions) 127 | - grantPermission 128 | - revokePermission 129 | - resetPermission 130 | 131 | #### keychain (Manipulate a device's keychain) 132 | - addRootCertificate 133 | - addCertificate 134 | - resetKeychain 135 | 136 | #### appinfo (Undocumented) 137 | - appInfo 138 | 139 | #### bootstatus (Undocumented) 140 | - startBootMonitor 141 | 142 | Methods marked with the star (`*`) character *do not* require the `udid` property to be set 143 | on the `Simctl` instance upon their invocation. All other methods will *throw an error* if the `udid` 144 | property is unset while they are being invoked. 145 | 146 | All public methods are supplied with docstrings that describe their arguments and returned values. 147 | 148 | The `Simctl` class constructor supports the following options: 149 | 150 | - `xcrun` (Object): The xcrun properties. Currently only one property 151 | is supported, which is `path` and it by default contains `null`, which enforces 152 | the instance to automatically detect the full path to `xcrun` tool and to throw 153 | an exception if it cannot be detected. If the path is set upon instance creation 154 | then it is going to be used by `exec` and no autodetection will happen. 155 | - `execTimeout` (number[600000]): The maximum number of milliseconds 156 | to wait for a single synchronous xcrun command. 157 | - `logErrors` (boolean[true]): Whether to write xcrun error messages 158 | into the debug log before throwing them as errors. 159 | - `udid` (string[null]): The unique identifier of the current device, which is 160 | going to be implicitly passed to all methods, which require it (see above). It can either be set 161 | upon instance creation if it is already known or later when/if needed via the corresponding 162 | setter. 163 | - `devicesSetPath` (string[null]): Full path to the set of devices that you want to manage. 164 | By default this path usually equals to ~/Library/Developer/CoreSimulator/Devices. This option 165 | has a getter and a setter which allows to switch between multiple device sets during the Simctl 166 | instance life cycle. 167 | 168 | 169 | ### Advanced Usage 170 | 171 | Any simctl subcommand could be called via `exec` method, which accepts the subcommand itself 172 | as the first argument and the set of options, which may contain additional command args, 173 | environment variables, encoding, etc. For example: 174 | 175 | ```js 176 | import Simctl from 'node-simctl'; 177 | 178 | const simctl = new Simctl(); 179 | const name = 'My Device Name'; 180 | simctl.udid = await simctl.createDevice(name, 'iPhone X', '13.3'); 181 | await simctl.bootDevice(); 182 | await simctl.startBootMonitor({timeout: 120000}); 183 | await simctl.exec('pbsync'); 184 | console.log(`Pasteboard content: ${await simctl.getPasteboard()}`); 185 | const {stdout} = await simctl.exec('status_bar', { 186 | args: [simctl.udid, 'list'] 187 | }); 188 | console.log(output); 189 | simctl.udid = void(await simctl.deleteDevice()); 190 | ``` 191 | 192 | See [specs](test/simctl-specs.js) for examples of usage. 193 | 194 | 195 | ### Running Multiple Simulator SDKs On a Single Computer 196 | 197 | It is possible to run multiple simulators using different Xcode SDKs on a single machine. 198 | Simply set a proper value to `DEVELOPER_DIR` environment variable for each process. 199 | 200 | Read this [MacOps article](https://macops.ca/developer-binaries-on-os-x-xcode-select-and-xcrun/) for more details. 201 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import appiumConfig from '@appium/eslint-config-appium-ts'; 2 | 3 | export default [ 4 | ...appiumConfig, 5 | { 6 | ignores: [ 7 | 'docs/**', 8 | ], 9 | }, 10 | ]; 11 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { Simctl } from './lib/simctl'; 2 | 3 | export { Simctl }; 4 | export default Simctl; 5 | -------------------------------------------------------------------------------- /lib/helpers.js: -------------------------------------------------------------------------------- 1 | import * as semver from 'semver'; 2 | 3 | export const DEFAULT_EXEC_TIMEOUT = 10 * 60 * 1000; // ms 4 | export const SIM_RUNTIME_NAME = 'com.apple.CoreSimulator.SimRuntime.'; 5 | 6 | /** 7 | * "Normalize" the version, since iOS uses 'major.minor' but the runtimes can 8 | * be 'major.minor.patch' 9 | * 10 | * @param {string} version - the string version 11 | * @return {string} The version in 'major.minor' form 12 | * @throws {Error} If the version not parseable by the `semver` package 13 | */ 14 | export function normalizeVersion (version) { 15 | const semverVersion = semver.coerce(version); 16 | if (!semverVersion) { 17 | throw new Error(`Unable to parse version '${version}'`); 18 | } 19 | return `${semverVersion.major}.${semverVersion.minor}`; 20 | } 21 | 22 | /** 23 | * @returns {string} 24 | */ 25 | export function getXcrunBinary () { 26 | return process.env.XCRUN_BINARY || 'xcrun'; 27 | } 28 | 29 | /** 30 | * Generate a UUID v4 31 | * 32 | * @returns {Promise} 33 | */ 34 | export async function uuidV4 () { 35 | const uuidLib = await import('uuid'); 36 | return uuidLib.v4(); 37 | } 38 | -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | import appiumLogger from '@appium/logger'; 2 | 3 | const LOG_PREFIX = 'simctl'; 4 | 5 | function getLogger () { 6 | const logger = global._global_npmlog || appiumLogger; 7 | if (!logger.debug) { 8 | logger.addLevel('debug', 1000, { fg: 'blue', bg: 'black' }, 'dbug'); 9 | } 10 | return logger; 11 | } 12 | 13 | const log = getLogger(); 14 | 15 | export { LOG_PREFIX }; 16 | export default log; 17 | -------------------------------------------------------------------------------- /lib/simctl.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import which from 'which'; 3 | import log, { LOG_PREFIX } from './logger'; 4 | import { 5 | DEFAULT_EXEC_TIMEOUT, getXcrunBinary, 6 | } from './helpers'; 7 | import { exec as tpExec, SubProcess } from 'teen_process'; 8 | import addmediaCommands from './subcommands/addmedia'; 9 | import appinfoCommands from './subcommands/appinfo'; 10 | import bootCommands from './subcommands/boot'; 11 | import bootstatusCommands from './subcommands/bootstatus'; 12 | import createCommands from './subcommands/create'; 13 | import deleteCommands from './subcommands/delete'; 14 | import eraseCommands from './subcommands/erase'; 15 | import getappcontainerCommands from './subcommands/get_app_container'; 16 | import installCommands from './subcommands/install'; 17 | import ioCommands from './subcommands/io'; 18 | import keychainCommands from './subcommands/keychain'; 19 | import launchCommands from './subcommands/launch'; 20 | import listCommands from './subcommands/list'; 21 | import openurlCommands from './subcommands/openurl'; 22 | import pbcopyCommands from './subcommands/pbcopy'; 23 | import pbpasteCommands from './subcommands/pbpaste'; 24 | import privacyCommands from './subcommands/privacy'; 25 | import pushCommands from './subcommands/push'; 26 | import envCommands from './subcommands/getenv'; 27 | import shutdownCommands from './subcommands/shutdown'; 28 | import spawnCommands from './subcommands/spawn'; 29 | import terminateCommands from './subcommands/terminate'; 30 | import uiCommands from './subcommands/ui'; 31 | import uninstallCommands from './subcommands/uninstall'; 32 | import locationCommands from './subcommands/location'; 33 | 34 | const SIMCTL_ENV_PREFIX = 'SIMCTL_CHILD_'; 35 | const DEFAULT_OPTS = { 36 | xcrun: { 37 | path: null, 38 | }, 39 | execTimeout: DEFAULT_EXEC_TIMEOUT, 40 | logErrors: true, 41 | }; 42 | 43 | /** 44 | * @typedef {Object} XCRun 45 | * @property {string?} path Full path to the xcrun script 46 | */ 47 | 48 | /** 49 | * @typedef {{asynchronous: true}} TAsyncOpts 50 | */ 51 | 52 | /** 53 | * @typedef {Object} ExecOpts 54 | * @property {string[]} [args=[]] - The list of additional subcommand arguments. 55 | * It's empty by default. 56 | * @property {Record} [env={}] - Environment variables mapping. All these variables 57 | * will be passed Simulator and used in the executing function. 58 | * @property {boolean} [logErrors=true] - Set it to _false_ to throw execution errors 59 | * immediately without logging any additional information. 60 | * @property {boolean} [asynchronous=false] - Whether to execute the given command 61 | * 'synchronously' or 'asynchronously'. Affects the returned result of the function. 62 | * @property {string} [encoding] - Explicitly sets streams encoding for the executed 63 | * command input and outputs. 64 | * @property {string|string[]} [architectures] - One or more architecture names to be enforced while 65 | * executing xcrun. See https://github.com/appium/appium/issues/18966 for more details. 66 | * @property {number} [timeout] - The maximum number of milliseconds 67 | * to wait for single synchronous xcrun command. If not provided explicitly, then 68 | * the value of execTimeout property is used by default. 69 | */ 70 | 71 | /** 72 | * @typedef {Object} SimctlOpts 73 | * @property {XCRun} [xcrun] - The xcrun properties. Currently only one property 74 | * is supported, which is `path` and it by default contains `null`, which enforces 75 | * the instance to automatically detect the full path to `xcrun` tool and to throw 76 | * an exception if it cannot be detected. If the path is set upon instance creation 77 | * then it is going to be used by `exec` and no autodetection will happen. 78 | * @property {number} [execTimeout=600000] - The default maximum number of milliseconds 79 | * to wait for single synchronous xcrun command. 80 | * @property {boolean} [logErrors=true] - Whether to wire xcrun error messages 81 | * into debug log before throwing them. 82 | * @property {string?} [udid] - The unique identifier of the current device, which is 83 | * going to be implicitly passed to all methods, which require it. It can either be set 84 | * upon instance creation if it is already known in advance or later when/if needed via the 85 | * corresponding instance setter. 86 | * @property {string?} [devicesSetPath] - Full path to the set of devices that you want to manage. 87 | * By default this path usually equals to ~/Library/Developer/CoreSimulator/Devices 88 | */ 89 | 90 | 91 | class Simctl { 92 | /** @type {XCRun} */ 93 | xcrun; 94 | 95 | /** @type {number} */ 96 | execTimeout; 97 | 98 | /** @type {boolean} */ 99 | logErrors; 100 | 101 | /** 102 | * @param {SimctlOpts} [opts={}] 103 | */ 104 | constructor (opts = {}) { 105 | opts = _.cloneDeep(opts); 106 | _.defaultsDeep(opts, DEFAULT_OPTS); 107 | for (const key of _.keys(DEFAULT_OPTS)) { 108 | this[key] = opts[key]; 109 | } 110 | /** @type {string?} */ 111 | this._udid = _.isNil(opts.udid) ? null : opts.udid; 112 | /** @type {string?} */ 113 | this._devicesSetPath = _.isNil(opts.devicesSetPath) ? null : opts.devicesSetPath; 114 | } 115 | 116 | set udid (value) { 117 | this._udid = value; 118 | } 119 | 120 | get udid () { 121 | return this._udid; 122 | } 123 | 124 | set devicesSetPath (value) { 125 | this._devicesSetPath = value; 126 | } 127 | 128 | get devicesSetPath () { 129 | return this._devicesSetPath; 130 | } 131 | 132 | /** 133 | * @param {string?} [commandName=null] 134 | * @returns {string} 135 | */ 136 | requireUdid (commandName = null) { 137 | if (!this.udid) { 138 | throw new Error(`udid is required to be set for ` + 139 | (commandName ? `the '${commandName}' command` : 'this simctl command')); 140 | } 141 | return this.udid; 142 | } 143 | 144 | /** 145 | * @returns {Promise} 146 | */ 147 | async requireXcrun () { 148 | const xcrunBinary = getXcrunBinary(); 149 | 150 | if (!this.xcrun.path) { 151 | try { 152 | this.xcrun.path = await which(xcrunBinary); 153 | } catch { 154 | throw new Error(`${xcrunBinary} tool has not been found in PATH. ` + 155 | `Are Xcode developers tools installed?`); 156 | } 157 | } 158 | return /** @type {string} */ (this.xcrun.path); 159 | } 160 | 161 | /** 162 | * Execute the particular simctl command. 163 | * 164 | * @template {ExecOpts} TExecOpts 165 | * @param {string} subcommand - One of available simctl subcommands. 166 | * Execute `xcrun simctl` in Terminal to see the full list of available subcommands. 167 | * @param {TExecOpts} [opts] 168 | * @return {Promise} 169 | * Either the result of teen process's `exec` or 170 | * `SubProcess` instance depending of `opts.asynchronous` value. 171 | * @throws {Error} If the simctl subcommand command returns non-zero return code. 172 | */ 173 | async exec (subcommand, opts) { 174 | let { 175 | args = [], 176 | env = {}, 177 | asynchronous = false, 178 | encoding, 179 | logErrors = true, 180 | architectures, 181 | timeout, 182 | } = opts ?? {}; 183 | // run a particular simctl command 184 | args = [ 185 | 'simctl', 186 | ...(this.devicesSetPath ? ['--set', this.devicesSetPath] : []), 187 | subcommand, 188 | ...args 189 | ]; 190 | // Prefix all passed in environment variables with 'SIMCTL_CHILD_', simctl 191 | // will then pass these to the child (spawned) process. 192 | env = _.defaults( 193 | _.mapKeys(env, 194 | (value, key) => _.startsWith(key, SIMCTL_ENV_PREFIX) ? key : `${SIMCTL_ENV_PREFIX}${key}`), 195 | process.env 196 | ); 197 | 198 | const execOpts = { 199 | env, 200 | encoding, 201 | }; 202 | if (!asynchronous) { 203 | execOpts.timeout = timeout || this.execTimeout; 204 | } 205 | const xcrun = await this.requireXcrun(); 206 | try { 207 | let execArgs = [xcrun, args, execOpts]; 208 | if (architectures?.length) { 209 | const archArgs = _.flatMap( 210 | (_.isArray(architectures) ? architectures : [architectures]).map((arch) => ['-arch', arch]) 211 | ); 212 | execArgs = ['arch', [...archArgs, xcrun, ...args], execOpts]; 213 | } 214 | // @ts-ignore We know what we are doing here 215 | return asynchronous ? new SubProcess(...execArgs) : await tpExec(...execArgs); 216 | } catch (e) { 217 | if (!this.logErrors || !logErrors) { 218 | // if we don't want to see the errors, just throw and allow the calling 219 | // code do what it wants 220 | } else if (e.stderr) { 221 | const msg = `Error running '${subcommand}': ${e.stderr.trim()}`; 222 | log.debug(LOG_PREFIX, msg); 223 | e.message = msg; 224 | } else { 225 | log.debug(LOG_PREFIX, e.message); 226 | } 227 | throw e; 228 | } 229 | } 230 | 231 | addMedia = addmediaCommands.addMedia; 232 | 233 | appInfo = appinfoCommands.appInfo; 234 | 235 | bootDevice = bootCommands.bootDevice; 236 | 237 | startBootMonitor = bootstatusCommands.startBootMonitor; 238 | 239 | createDevice = createCommands.createDevice; 240 | 241 | deleteDevice = deleteCommands.deleteDevice; 242 | 243 | eraseDevice = eraseCommands.eraseDevice; 244 | 245 | getAppContainer = getappcontainerCommands.getAppContainer; 246 | 247 | getEnv = envCommands.getEnv; 248 | 249 | installApp = installCommands.installApp; 250 | 251 | getScreenshot = ioCommands.getScreenshot; 252 | 253 | addRootCertificate = keychainCommands.addRootCertificate; 254 | addCertificate = keychainCommands.addCertificate; 255 | resetKeychain = keychainCommands.resetKeychain; 256 | 257 | launchApp = launchCommands.launchApp; 258 | 259 | getDevicesByParsing = listCommands.getDevicesByParsing; 260 | getDevices = listCommands.getDevices; 261 | getRuntimeForPlatformVersionViaJson = listCommands.getRuntimeForPlatformVersionViaJson; 262 | getRuntimeForPlatformVersion = listCommands.getRuntimeForPlatformVersion; 263 | getDeviceTypes = listCommands.getDeviceTypes; 264 | list = listCommands.list; 265 | 266 | setLocation = locationCommands.setLocation; 267 | clearLocation = locationCommands.clearLocation; 268 | 269 | openUrl = openurlCommands.openUrl; 270 | 271 | setPasteboard = pbcopyCommands.setPasteboard; 272 | 273 | getPasteboard = pbpasteCommands.getPasteboard; 274 | 275 | grantPermission = privacyCommands.grantPermission; 276 | revokePermission = privacyCommands.revokePermission; 277 | resetPermission = privacyCommands.resetPermission; 278 | 279 | pushNotification = pushCommands.pushNotification; 280 | 281 | shutdownDevice = shutdownCommands.shutdownDevice; 282 | 283 | spawnProcess = spawnCommands.spawnProcess; 284 | spawnSubProcess = spawnCommands.spawnSubProcess; 285 | 286 | terminateApp = terminateCommands.terminateApp; 287 | 288 | getAppearance = uiCommands.getAppearance; 289 | setAppearance = uiCommands.setAppearance; 290 | getIncreaseContrast = uiCommands.getIncreaseContrast; 291 | setIncreaseContrast = uiCommands.setIncreaseContrast; 292 | getContentSize = uiCommands.getContentSize; 293 | setContentSize = uiCommands.setContentSize; 294 | 295 | removeApp = uninstallCommands.removeApp; 296 | } 297 | 298 | export default Simctl; 299 | export { Simctl }; 300 | -------------------------------------------------------------------------------- /lib/subcommands/addmedia.js: -------------------------------------------------------------------------------- 1 | const commands = {}; 2 | 3 | /** 4 | * Add the particular media file to Simulator's library. 5 | * It is required that Simulator is in _booted_ state. 6 | * 7 | * @this {import('../simctl').Simctl} 8 | * @param {string} filePath - Full path to a media file on the local 9 | * file system. 10 | * @return {Promise} Command execution result. 11 | * @throws {Error} If the corresponding simctl subcommand command 12 | * returns non-zero return code. 13 | * @throws {Error} If the `udid` instance property is unset 14 | */ 15 | commands.addMedia = async function addMedia (filePath) { 16 | return await this.exec('addmedia', { 17 | args: [this.requireUdid('addmedia'), filePath], 18 | }); 19 | }; 20 | 21 | export default commands; 22 | -------------------------------------------------------------------------------- /lib/subcommands/appinfo.js: -------------------------------------------------------------------------------- 1 | const commands = {}; 2 | 3 | /** 4 | * Invoke hidden appinfo subcommand to get the information 5 | * about applications installed on Simulator, including 6 | * system applications ({@link getAppContainer} does not "see" such apps). 7 | * Simulator server should be in 'booted' state for this call to work properly. 8 | * The tool is only available since Xcode SDK 8.1 9 | * 10 | * @this {import('../simctl').Simctl} 11 | * @param {string} bundleId - The bundle identifier of the target application. 12 | * @return {Promise} The information about installed application. 13 | * 14 | * Example output for non-existing application container: 15 | *
16 |  * {
17 |  *   CFBundleIdentifier = "com.apple.MobileSafari";
18 |  *   GroupContainers =     {
19 |  *   };
20 |  *   SBAppTags =     (
21 |  *   );
22 |  * }
23 |  * 
24 | * 25 | * Example output for an existing system application container: 26 | *
27 |  * {
28 |  *   ApplicationType = Hidden;
29 |  *   Bundle = "file:///Applications/Xcode-beta.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/CoreServices/SpringBoard.app";
30 |  *   CFBundleDisplayName = SpringBoard;
31 |  *   CFBundleExecutable = SpringBoard;
32 |  *   CFBundleIdentifier = "com.apple.springboard";
33 |  *   CFBundleName = SpringBoard;
34 |  *   CFBundleVersion = 50;
35 |  *   GroupContainers =     {
36 |  *   };
37 |  *   Path = "/Applications/Xcode-beta.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/CoreServices/SpringBoard.app";
38 |  *   SBAppTags =     (
39 |  *   );
40 |  * }
41 |  * 
42 | * @throws {Error} If the `udid` instance property is unset 43 | */ 44 | commands.appInfo = async function appInfo (bundleId) { 45 | const {stdout} = await this.exec('appinfo', { 46 | args: [this.requireUdid('appinfo'), bundleId], 47 | }); 48 | return (stdout || '').trim(); 49 | }; 50 | 51 | export default commands; 52 | -------------------------------------------------------------------------------- /lib/subcommands/boot.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import log, { LOG_PREFIX } from '../logger'; 3 | 4 | 5 | const commands = {}; 6 | 7 | /** 8 | * Boot the particular Simulator if it is not running. 9 | * 10 | * @this {import('../simctl').Simctl} 11 | * @throws {Error} If the corresponding simctl subcommand command 12 | * returns non-zero return code. 13 | * @throws {Error} If the `udid` instance property is unset 14 | */ 15 | commands.bootDevice = async function bootDevice () { 16 | try { 17 | await this.exec('boot', { 18 | args: [this.requireUdid('boot')] 19 | }); 20 | } catch (e) { 21 | if (_.includes(e.message, 'Unable to boot device in current state: Booted')) { 22 | throw e; 23 | } 24 | log.debug(LOG_PREFIX, `Simulator already in 'Booted' state. Continuing`); 25 | } 26 | }; 27 | 28 | export default commands; 29 | -------------------------------------------------------------------------------- /lib/subcommands/bootstatus.js: -------------------------------------------------------------------------------- 1 | import log from '../logger'; 2 | import { waitForCondition } from 'asyncbox'; 3 | import _ from 'lodash'; 4 | 5 | const commands = {}; 6 | 7 | /** 8 | * @typedef {Object} BootMonitorOptions 9 | * @property {number} [timeout=240000] - Simulator booting timeout in ms. 10 | * @property {Function} [onWaitingDataMigration] - This event is fired when data migration stage starts. 11 | * @property {Function} [onWaitingSystemApp] - This event is fired when system app wait stage starts. 12 | * @property {Function} [onFinished] - This event is fired when Simulator is fully booted. 13 | * @property {Function} [onError] - This event is fired when there was an error while monitoring the booting process 14 | * or when the timeout has expired. 15 | * @property {boolean} [shouldPreboot=false] Whether to preboot the Simulator 16 | * if this command is called and it is not already in booted or booting state. 17 | */ 18 | 19 | /** 20 | * Start monitoring for boot status of the particular Simulator. 21 | * If onFinished property is not set then the method will block 22 | * until Simulator booting is completed. 23 | * The method is only available since Xcode8. 24 | * 25 | * @this {import('../simctl').Simctl} 26 | * @param {BootMonitorOptions} [opts={}] - Monitoring options. 27 | * @returns {Promise} The instance of the corresponding monitoring process. 28 | * @throws {Error} If the Simulator fails to finish booting within the given timeout and onFinished 29 | * property is not set. 30 | * @throws {Error} If the `udid` instance property is unset 31 | */ 32 | commands.startBootMonitor = async function startBootMonitor (opts = {}) { 33 | const { 34 | timeout = 240000, 35 | onWaitingDataMigration, 36 | onWaitingSystemApp, 37 | onFinished, 38 | onError, 39 | shouldPreboot, 40 | } = opts; 41 | const udid = this.requireUdid('bootstatus'); 42 | 43 | /** @type {string[]} */ 44 | const status = []; 45 | let isBootingFinished = false; 46 | let error = null; 47 | let timeoutHandler = null; 48 | const args = [udid]; 49 | if (shouldPreboot) { 50 | args.push('-b'); 51 | } 52 | const bootMonitor = await this.exec('bootstatus', { 53 | args, 54 | asynchronous: true, 55 | }); 56 | const onStreamLine = (/** @type {string} */ line) => { 57 | status.push(line); 58 | if (onWaitingDataMigration && line.includes('Waiting on Data Migration')) { 59 | onWaitingDataMigration(); 60 | } else if (onWaitingSystemApp && line.includes('Waiting on System App')) { 61 | onWaitingSystemApp(); 62 | } 63 | }; 64 | for (const streamName of ['stdout', 'stderr']) { 65 | bootMonitor.on(`line-${streamName}`, onStreamLine); 66 | } 67 | bootMonitor.once('exit', (code, signal) => { 68 | if (timeoutHandler) { 69 | clearTimeout(timeoutHandler); 70 | } 71 | if (code === 0) { 72 | if (onFinished) { 73 | onFinished(); 74 | } 75 | isBootingFinished = true; 76 | } else { 77 | const errMessage = _.isEmpty(status) 78 | ? `The simulator booting process has exited with code ${code} by signal ${signal}` 79 | : status.join('\n'); 80 | error = new Error(errMessage); 81 | if (onError) { 82 | onError(error); 83 | } 84 | } 85 | }); 86 | await bootMonitor.start(0); 87 | const stopMonitor = async () => { 88 | if (bootMonitor.isRunning) { 89 | try { 90 | await bootMonitor.stop(); 91 | } catch (e) { 92 | log.warn(e.message); 93 | } 94 | } 95 | }; 96 | const start = process.hrtime(); 97 | if (onFinished) { 98 | timeoutHandler = setTimeout(stopMonitor, timeout); 99 | } else { 100 | try { 101 | await waitForCondition(() => { 102 | if (error) { 103 | throw error; 104 | } 105 | return isBootingFinished; 106 | }, {waitMs: timeout, intervalMs: 500}); 107 | } catch { 108 | await stopMonitor(); 109 | const [seconds] = process.hrtime(start); 110 | throw new Error( 111 | `The simulator ${udid} has failed to finish booting after ${seconds}s. ` + 112 | `Original status: ${status.join('\n')}` 113 | ); 114 | } 115 | } 116 | return bootMonitor; 117 | }; 118 | 119 | export default commands; 120 | -------------------------------------------------------------------------------- /lib/subcommands/create.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import log, { LOG_PREFIX } from '../logger'; 3 | import { retryInterval } from 'asyncbox'; 4 | import { SIM_RUNTIME_NAME, normalizeVersion } from '../helpers'; 5 | 6 | 7 | const SIM_RUNTIME_NAME_SUFFIX_IOS = 'iOS'; 8 | const DEFAULT_CREATE_SIMULATOR_TIMEOUT = 10000; 9 | 10 | const commands = {}; 11 | 12 | /** 13 | * @typedef {Object} SimCreationOpts 14 | * @property {string} [platform='iOS'] - Platform name in order to specify runtime such as 'iOS', 'tvOS', 'watchOS' 15 | * @property {number} [timeout=10000] - The maximum number of milliseconds to wait 16 | * unit device creation is completed. 17 | */ 18 | 19 | /** 20 | * Create Simulator device with given name for the particular 21 | * platform type and version. 22 | * 23 | * @this {import('../simctl').Simctl} 24 | * @param {string} name - The device name to be created. 25 | * @param {string} deviceTypeId - Device type, for example 'iPhone 6'. 26 | * @param {string} platformVersion - Platform version, for example '10.3'. 27 | * @param {SimCreationOpts} [opts={}] - Simulator options for creating devices. 28 | * @return {Promise} The UDID of the newly created device. 29 | * @throws {Error} If the corresponding simctl subcommand command 30 | * returns non-zero return code. 31 | */ 32 | commands.createDevice = async function createDevice (name, deviceTypeId, platformVersion, opts = {}) { 33 | const { 34 | platform = SIM_RUNTIME_NAME_SUFFIX_IOS, 35 | timeout = DEFAULT_CREATE_SIMULATOR_TIMEOUT 36 | } = opts; 37 | 38 | let runtimeIds = []; 39 | 40 | // Try getting runtimeId using JSON flag 41 | try { 42 | runtimeIds.push(await this.getRuntimeForPlatformVersionViaJson(platformVersion, platform)); 43 | } catch {} 44 | 45 | if (_.isEmpty(runtimeIds)) { 46 | // at first make sure that the runtime id is the right one 47 | // in some versions of Xcode it will be a patch version 48 | let runtimeId; 49 | try { 50 | runtimeId = await this.getRuntimeForPlatformVersion(platformVersion, platform); 51 | } catch { 52 | log.warn(`Unable to find runtime for iOS '${platformVersion}'. Continuing`); 53 | runtimeId = platformVersion; 54 | } 55 | 56 | // get the possible runtimes, which will be iterated over 57 | 58 | // start with major-minor version 59 | let potentialRuntimeIds = [normalizeVersion(runtimeId)]; 60 | if (runtimeId.split('.').length === 3) { 61 | // add patch version if it exists 62 | potentialRuntimeIds.push(runtimeId); 63 | } 64 | 65 | // add modified versions, since modern Xcodes use this, then the bare 66 | // versions, to accomodate older Xcodes 67 | runtimeIds.push( 68 | ...(potentialRuntimeIds.map((id) => `${SIM_RUNTIME_NAME}${platform}-${id.replace(/\./g, '-')}`)), 69 | ...potentialRuntimeIds 70 | ); 71 | } 72 | 73 | // go through the runtime ids and try to create a simulator with each 74 | let udid; 75 | for (const runtimeId of runtimeIds) { 76 | log.debug(LOG_PREFIX, 77 | `Creating simulator with name '${name}', device type id '${deviceTypeId}' and runtime id '${runtimeId}'`); 78 | try { 79 | const {stdout} = await this.exec('create', { 80 | args: [name, deviceTypeId, runtimeId] 81 | }); 82 | udid = stdout.trim(); 83 | break; 84 | } catch { 85 | // the error gets logged in `simExec` 86 | } 87 | } 88 | 89 | if (!udid) { 90 | throw new Error(`Could not create simulator with name '${name}', device ` + 91 | `type id '${deviceTypeId}', with runtime ids ` + 92 | `${runtimeIds.map((id) => `'${id}'`).join(', ')}`); 93 | } 94 | 95 | // make sure that it gets out of the "Creating" state 96 | const retries = parseInt(`${timeout / 1000}`, 10); 97 | await retryInterval(retries, 1000, async () => { 98 | const devices = _.values(await this.getDevices()); 99 | for (const deviceArr of _.values(devices)) { 100 | for (const device of deviceArr) { 101 | if (device.udid === udid) { 102 | if (device.state === 'Creating') { 103 | // need to retry 104 | throw new Error(`Device with udid '${udid}' still being created`); 105 | } else { 106 | // stop looking, we're done 107 | return; 108 | } 109 | } 110 | } 111 | } 112 | throw new Error(`Device with udid '${udid}' not yet created`); 113 | }); 114 | 115 | return udid; 116 | }; 117 | 118 | export default commands; 119 | -------------------------------------------------------------------------------- /lib/subcommands/delete.js: -------------------------------------------------------------------------------- 1 | const commands = {}; 2 | 3 | /** 4 | * Delete the particular Simulator from available devices list. 5 | * 6 | * @this {import('../simctl').Simctl} 7 | * @throws {Error} If the corresponding simctl subcommand command 8 | * returns non-zero return code. 9 | * @throws {Error} If the `udid` instance property is unset 10 | */ 11 | commands.deleteDevice = async function deleteDevice () { 12 | await this.exec('delete', { 13 | args: [this.requireUdid('delete')] 14 | }); 15 | }; 16 | 17 | export default commands; 18 | -------------------------------------------------------------------------------- /lib/subcommands/erase.js: -------------------------------------------------------------------------------- 1 | import { retryInterval } from 'asyncbox'; 2 | 3 | const commands = {}; 4 | 5 | /** 6 | * Reset the content and settings of the particular Simulator. 7 | * It is required that Simulator is in _shutdown_ state. 8 | * 9 | * @this {import('../simctl').Simctl} 10 | * @param {number} [timeout=10000] - The maximum number of milliseconds to wait 11 | * unit device reset is completed. 12 | * @throws {Error} If the corresponding simctl subcommand command 13 | * returns non-zero return code. 14 | * @throws {Error} If the `udid` instance property is unset 15 | */ 16 | commands.eraseDevice = async function eraseDevice (timeout = 1000) { 17 | // retry erase with a sleep in between because it's flakey 18 | const retries = parseInt(`${timeout / 200}`, 10); 19 | await retryInterval(retries, 200, 20 | async () => await this.exec('erase', { 21 | args: [this.requireUdid('erase')] 22 | }) 23 | ); 24 | }; 25 | 26 | export default commands; 27 | -------------------------------------------------------------------------------- /lib/subcommands/get_app_container.js: -------------------------------------------------------------------------------- 1 | const commands = {}; 2 | 3 | /** 4 | * Get the full path to the particular application container 5 | * on the local file system. Note, that this subcommand throws 6 | * an error if bundle id of a system application is provided, 7 | * like 'com.apple.springboard'. 8 | * It is required that Simulator is in _booted_ state. 9 | * 10 | * @this {import('../simctl').Simctl} 11 | * @param {string} bundleId - Bundle identifier of an application. 12 | * @param {string?} [containerType=null] - Which container type to return. Possible values 13 | * are 'app', 'data', 'groups', ''. 14 | * The default value is 'app'. 15 | * @return {Promise} Full path to the given application container on the local 16 | * file system. 17 | * @throws {Error} If the corresponding simctl subcommand command 18 | * returns non-zero return code. 19 | * @throws {Error} If the `udid` instance property is unset 20 | */ 21 | commands.getAppContainer = async function getAppContainer (bundleId, containerType = null) { 22 | const args = [this.requireUdid('get_app_container'), bundleId]; 23 | if (containerType) { 24 | args.push(containerType); 25 | } 26 | const {stdout} = await this.exec('get_app_container', {args}); 27 | return (stdout || '').trim(); 28 | }; 29 | 30 | export default commands; 31 | -------------------------------------------------------------------------------- /lib/subcommands/getenv.js: -------------------------------------------------------------------------------- 1 | const commands = {}; 2 | 3 | /** 4 | * Retrieves the value of a Simulator environment variable 5 | * 6 | * @this {import('../simctl').Simctl} 7 | * @param {string} varName - The name of the variable to be retrieved 8 | * @returns {Promise} The value of the variable or null if the given variable 9 | * is not present in the Simulator environment 10 | * @throws {Error} If there was an error while running the command 11 | * @throws {Error} If the `udid` instance property is unset 12 | */ 13 | commands.getEnv = async function getEnv (varName) { 14 | const {stdout, stderr} = await this.exec('getenv', { 15 | args: [this.requireUdid('getenv'), varName], 16 | logErrors: false, 17 | }); 18 | return stderr ? null : stdout; 19 | }; 20 | 21 | export default commands; 22 | -------------------------------------------------------------------------------- /lib/subcommands/install.js: -------------------------------------------------------------------------------- 1 | const commands = {}; 2 | 3 | /** 4 | * Install the particular application package on Simulator. 5 | * It is required that Simulator is in _booted_ state. 6 | * 7 | * @this {import('../simctl').Simctl} 8 | * @param {string} appPath - Full path to .app package, which is 9 | * going to be installed. 10 | * @throws {Error} If the corresponding simctl subcommand command 11 | * returns non-zero return code. 12 | * @throws {Error} If the `udid` instance property is unset 13 | */ 14 | commands.installApp = async function installApp (appPath) { 15 | await this.exec('install', { 16 | args: [this.requireUdid('install'), appPath], 17 | }); 18 | }; 19 | 20 | export default commands; 21 | -------------------------------------------------------------------------------- /lib/subcommands/io.js: -------------------------------------------------------------------------------- 1 | import { rimraf } from 'rimraf'; 2 | import path from 'path'; 3 | import os from 'os'; 4 | import fs from 'fs/promises'; 5 | import { uuidV4 } from '../helpers'; 6 | 7 | const commands = {}; 8 | 9 | /** 10 | * Gets base64 screenshot for device 11 | * It is required that Simulator is in _booted_ state. 12 | * 13 | * @this {import('../simctl').Simctl} 14 | * @since Xcode SDK 8.1 15 | * @return {Promise} Base64-encoded Simulator screenshot. 16 | * @throws {Error} If the corresponding simctl subcommand command 17 | * returns non-zero return code. 18 | * @throws {Error} If the `udid` instance property is unset 19 | */ 20 | commands.getScreenshot = async function getScreenshot () { 21 | const udid = this.requireUdid('io screenshot'); 22 | const pathToScreenshotPng = path.resolve(os.tmpdir(), `${await uuidV4()}.png`); 23 | try { 24 | await this.exec('io', { 25 | args: [udid, 'screenshot', pathToScreenshotPng], 26 | }); 27 | return (await fs.readFile(pathToScreenshotPng)).toString('base64'); 28 | } finally { 29 | await rimraf(pathToScreenshotPng); 30 | } 31 | }; 32 | 33 | export default commands; 34 | -------------------------------------------------------------------------------- /lib/subcommands/keychain.js: -------------------------------------------------------------------------------- 1 | import os from 'os'; 2 | import fs from 'fs/promises'; 3 | import { uuidV4 } from '../helpers'; 4 | import path from 'path'; 5 | import _ from 'lodash'; 6 | import { rimraf } from 'rimraf'; 7 | 8 | const commands = {}; 9 | 10 | /** 11 | * 12 | * @param {string|Buffer} payload 13 | * @param {(filePath: string) => Promise} onPayloadStored 14 | */ 15 | async function handleRawPayload (payload, onPayloadStored) { 16 | const filePath = path.resolve(os.tmpdir(), `${await uuidV4()}.pem`); 17 | try { 18 | if (_.isBuffer(payload)) { 19 | await fs.writeFile(filePath, payload); 20 | } else { 21 | await fs.writeFile(filePath, payload, 'utf8'); 22 | } 23 | await onPayloadStored(filePath); 24 | } finally { 25 | await rimraf(filePath); 26 | } 27 | } 28 | 29 | 30 | /** 31 | * @typedef {Object} CertOptions 32 | * @property {boolean} [raw=false] - whether the `cert` argument 33 | * is the path to the certificate on the local file system or 34 | * a raw certificate content 35 | */ 36 | 37 | /** 38 | * Adds the given certificate to the Trusted Root Store on the simulator 39 | * 40 | * @since Xcode 11.4 SDK 41 | * @this {import('../simctl').Simctl} 42 | * @param {string} cert the full path to a valid .cert file containing 43 | * the certificate content or the certificate content itself, depending on 44 | * options 45 | * @param {CertOptions} [opts={}] 46 | * @throws {Error} if the current SDK version does not support the command 47 | * or there was an error while adding the certificate 48 | * @throws {Error} If the `udid` instance property is unset 49 | */ 50 | commands.addRootCertificate = async function addRootCertificate (cert, opts = {}) { 51 | const { 52 | raw = false, 53 | } = opts; 54 | const execMethod = async (/** @type {string} */certPath) => await this.exec('keychain', { 55 | args: [this.requireUdid('keychain add-root-cert'), 'add-root-cert', certPath], 56 | }); 57 | if (raw) { 58 | await handleRawPayload(cert, execMethod); 59 | } else { 60 | await execMethod(cert); 61 | } 62 | }; 63 | 64 | /** 65 | * Adds the given certificate to the Keychain Store on the simulator 66 | * 67 | * @since Xcode 11.4 SDK 68 | * @this {import('../simctl').Simctl} 69 | * @param {string} cert the full path to a valid .cert file containing 70 | * the certificate content or the certificate content itself, depending on 71 | * options 72 | * @param {CertOptions} [opts={}] 73 | * @throws {Error} if the current SDK version does not support the command 74 | * or there was an error while adding the certificate 75 | * @throws {Error} If the `udid` instance property is unset 76 | */ 77 | commands.addCertificate = async function addCertificate (cert, opts = {}) { 78 | const { 79 | raw = false, 80 | } = opts; 81 | const execMethod = async (certPath) => await this.exec('keychain', { 82 | args: [this.requireUdid('keychain add-cert'), 'add-cert', certPath], 83 | }); 84 | if (raw) { 85 | await handleRawPayload(cert, execMethod); 86 | } else { 87 | await execMethod(cert); 88 | } 89 | }; 90 | 91 | /** 92 | * Resets the simulator keychain 93 | * 94 | * @since Xcode 11.4 SDK 95 | * @this {import('../simctl').Simctl} 96 | * @throws {Error} if the current SDK version does not support the command 97 | * or there was an error while resetting the keychain 98 | * @throws {Error} If the `udid` instance property is unset 99 | */ 100 | commands.resetKeychain = async function resetKeychain () { 101 | await this.exec('keychain', { 102 | args: [this.requireUdid('keychain reset'), 'reset'], 103 | }); 104 | }; 105 | 106 | export default commands; 107 | -------------------------------------------------------------------------------- /lib/subcommands/launch.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { retryInterval } from 'asyncbox'; 3 | 4 | const commands = {}; 5 | 6 | /** 7 | * Execute the particular application package on Simulator. 8 | * It is required that Simulator is in _booted_ state and 9 | * the application with given bundle identifier is already installed. 10 | * 11 | * @this {import('../simctl').Simctl} 12 | * @param {string} bundleId - Bundle identifier of the application, 13 | * which is going to be removed. 14 | * @param {number} [tries=5] - The maximum number of retries before 15 | * throwing an exception. 16 | * @return {Promise} the actual command output 17 | * @throws {Error} If the corresponding simctl subcommand command 18 | * returns non-zero return code. 19 | * @throws {Error} If the `udid` instance property is unset 20 | */ 21 | commands.launchApp = async function launchApp (bundleId, tries = 5) { 22 | // @ts-ignore A string will always be returned 23 | return await retryInterval(tries, 1000, async () => { 24 | const {stdout} = await this.exec('launch', { 25 | args: [this.requireUdid('launch'), bundleId], 26 | }); 27 | return _.trim(stdout); 28 | }); 29 | }; 30 | 31 | export default commands; 32 | -------------------------------------------------------------------------------- /lib/subcommands/list.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { SIM_RUNTIME_NAME, normalizeVersion } from '../helpers'; 3 | import log, { LOG_PREFIX } from '../logger'; 4 | 5 | 6 | const commands = {}; 7 | 8 | /** 9 | * @typedef {Object} DeviceInfo 10 | * @property {string} name - The device name. 11 | * @property {string} udid - The device UDID. 12 | * @property {string} state - The current Simulator state, for example 'booted' or 'shutdown'. 13 | * @property {string} sdk - The SDK version, for example '10.3'. 14 | */ 15 | 16 | /** 17 | * Parse the list of existing Simulator devices to represent 18 | * it as convenient mapping. 19 | * 20 | * @this {import('../simctl').Simctl} 21 | * @param {string?} [platform] - The platform name, for example 'watchOS'. 22 | * @return {Promise>} The resulting mapping. Each key is platform version, 23 | * for example '10.3' and the corresponding value is an 24 | * array of the matching {@link DeviceInfo} instances. 25 | * @throws {Error} If the corresponding simctl subcommand command 26 | * returns non-zero return code. 27 | */ 28 | commands.getDevicesByParsing = async function getDevicesByParsing (platform) { 29 | const {stdout} = await this.exec('list', { 30 | args: ['devices'], 31 | }); 32 | 33 | // expect to get a listing like 34 | // -- iOS 8.1 -- 35 | // iPhone 4s (3CA6E7DD-220E-45E5-B716-1E992B3A429C) (Shutdown) 36 | // ... 37 | // -- iOS 8.2 -- 38 | // iPhone 4s (A99FFFC3-8E19-4DCF-B585-7D9D46B4C16E) (Shutdown) 39 | // ... 40 | // so, get the `-- iOS X.X --` line to find the sdk (X.X) 41 | // and the rest of the listing in order to later find the devices 42 | const deviceSectionRe = _.isEmpty(platform) || !platform 43 | ? new RegExp(`\\-\\-\\s+(\\S+)\\s+(\\S+)\\s+\\-\\-(\\n\\s{4}.+)*`, 'mgi') 44 | : new RegExp(`\\-\\-\\s+${_.escapeRegExp(platform)}\\s+(\\S+)\\s+\\-\\-(\\n\\s{4}.+)*`, 'mgi'); 45 | const matches = []; 46 | let match; 47 | // make an entry for each sdk version 48 | while ((match = deviceSectionRe.exec(stdout))) { 49 | matches.push(match); 50 | } 51 | if (_.isEmpty(matches)) { 52 | throw new Error('Could not find device section'); 53 | } 54 | 55 | const lineRe = /([^\s].+) \((\w+-.+\w+)\) \((\w+\s?\w+)\)/; // https://regex101.com/r/lG7mK6/3 56 | // get all the devices for each sdk 57 | const devices = {}; 58 | for (match of matches) { 59 | const sdk = platform ? match[1] : match[2]; 60 | devices[sdk] = devices[sdk] || []; 61 | // split the full match into lines and remove the first 62 | for (const line of match[0].split('\n').slice(1)) { 63 | if (line.includes('(unavailable, ')) { 64 | continue; 65 | } 66 | // a line is something like 67 | // iPhone 4s (A99FFFC3-8E19-4DCF-B585-7D9D46B4C16E) (Shutdown) 68 | // retrieve: 69 | // iPhone 4s 70 | // A99FFFC3-8E19-4DCF-B585-7D9D46B4C16E 71 | // Shutdown 72 | const lineMatch = lineRe.exec(line); 73 | if (!lineMatch) { 74 | throw new Error(`Could not match line: ${line}`); 75 | } 76 | // save the whole thing as ab object in the list for this sdk 77 | devices[sdk].push({ 78 | name: lineMatch[1], 79 | udid: lineMatch[2], 80 | state: lineMatch[3], 81 | sdk, 82 | platform: platform || match[1], 83 | }); 84 | } 85 | } 86 | return devices; 87 | }; 88 | 89 | /** 90 | * Parse the list of existing Simulator devices to represent 91 | * it as convenient mapping for the particular platform version. 92 | * 93 | * @this {import('../simctl').Simctl} 94 | * @param {string?} [forSdk] - The sdk version, 95 | * for which the devices list should be parsed, 96 | * for example '10.3'. 97 | * @param {string?} [platform] - The platform name, for example 'watchOS'. 98 | * @return {Promise} If _forSdk_ is set then the list 99 | * of devices for the particular platform version. 100 | * Otherwise the same result as for {@link getDevicesByParsing} 101 | * function. 102 | * @throws {Error} If the corresponding simctl subcommand command 103 | * returns non-zero return code or if no matching 104 | * platform version is found in the system. 105 | */ 106 | commands.getDevices = async function getDevices (forSdk, platform) { 107 | let devices = {}; 108 | try { 109 | const {stdout} = await this.exec('list', { 110 | args: ['devices', '-j'], 111 | }); 112 | /* JSON should be 113 | * { 114 | * "devices" : { 115 | * "iOS " : [ // or 116 | * "com.apple.CoreSimulator.SimRuntime.iOS- : [ 117 | * { 118 | * "state" : "Booted", 119 | * "availability" : "(available)", 120 | * "isAvailable" : true, 121 | * "name" : "iPhone 6", 122 | * "udid" : "75E34140-18E8-4D1A-9F45-AAC735DF75DF" 123 | * } 124 | * ] 125 | * } 126 | * } 127 | */ 128 | const versionMatchRe = _.isEmpty(platform) || !platform 129 | ? new RegExp(`^([^\\s-]+)[\\s-](\\S+)`, 'i') 130 | : new RegExp(`^${_.escapeRegExp(platform)}[\\s-](\\S+)`, 'i'); 131 | for (let [sdkName, entries] of _.toPairs(JSON.parse(stdout).devices)) { 132 | // there could be a longer name, so remove it 133 | sdkName = sdkName.replace(SIM_RUNTIME_NAME, ''); 134 | const versionMatch = versionMatchRe.exec(sdkName); 135 | if (!versionMatch) { 136 | continue; 137 | } 138 | 139 | // the sdk can have dashes (`12-2`) or dots (`12.1`) 140 | const sdk = (platform ? versionMatch[1] : versionMatch[2]).replace('-', '.'); 141 | devices[sdk] = devices[sdk] || []; 142 | devices[sdk].push(...entries.filter((el) => _.isUndefined(el.isAvailable) || el.isAvailable) 143 | .map((el) => { 144 | delete el.availability; 145 | return { 146 | sdk, 147 | ...el, 148 | platform: platform || versionMatch[1], 149 | }; 150 | }) 151 | ); 152 | } 153 | } catch (err) { 154 | log.debug(LOG_PREFIX, `Unable to get JSON device list: ${err.stack}`); 155 | log.debug(LOG_PREFIX, 'Falling back to manual parsing'); 156 | devices = await this.getDevicesByParsing(platform); 157 | } 158 | 159 | if (!forSdk) { 160 | return devices; 161 | } 162 | // if a `forSdk` was passed in, return only the corresponding list 163 | if (devices[forSdk]) { 164 | return devices[forSdk]; 165 | } 166 | 167 | let errMsg = `'${forSdk}' does not exist in the list of simctl SDKs.`; 168 | const availableSDKs = _.keys(devices); 169 | errMsg += availableSDKs.length 170 | ? ` Only the following Simulator SDK versions are available on your system: ${availableSDKs.join(', ')}` 171 | : ` No Simulator SDK versions are available on your system. Please install some via Xcode preferences.`; 172 | throw new Error(errMsg); 173 | }; 174 | 175 | /** 176 | * Get the runtime for the particular platform version using --json flag 177 | * 178 | * @this {import('../simctl').Simctl} 179 | * @param {string} platformVersion - The platform version name, 180 | * for example '10.3'. 181 | * @param {string} [platform='iOS'] - The platform name, for example 'watchOS'. 182 | * @return {Promise} The corresponding runtime name for the given 183 | * platform version. 184 | */ 185 | commands.getRuntimeForPlatformVersionViaJson = async function getRuntimeForPlatformVersionViaJson ( 186 | platformVersion, platform = 'iOS') { 187 | const {stdout} = await this.exec('list', { 188 | args: ['runtimes', '--json'], 189 | }); 190 | for (const {version, identifier, name} of JSON.parse(stdout).runtimes) { 191 | if (normalizeVersion(version) === normalizeVersion(platformVersion) 192 | && name.toLowerCase().startsWith(platform.toLowerCase())) { 193 | return identifier; 194 | } 195 | } 196 | throw new Error(`Could not use --json flag to parse platform version`); 197 | }; 198 | 199 | /** 200 | * Get the runtime for the particular platform version. 201 | * 202 | * @this {import('../simctl').Simctl} 203 | * @param {string} platformVersion - The platform version name, 204 | * for example '10.3'. 205 | * @param {string} [platform='iOS'] - The platform name, for example 'watchOS'. 206 | * @return {Promise} The corresponding runtime name for the given 207 | * platform version. 208 | */ 209 | commands.getRuntimeForPlatformVersion = async function getRuntimeForPlatformVersion ( 210 | platformVersion, platform = 'iOS') { 211 | // Try with parsing 212 | try { 213 | const {stdout} = await this.exec('list', { 214 | args: ['runtimes'], 215 | }); 216 | // https://regex101.com/r/UykjQZ/1 217 | const runtimeRe = 218 | new RegExp(`${_.escapeRegExp(platform)}\\s+(\\d+\\.\\d+)\\s+\\((\\d+\\.\\d+\\.*\\d*)`, 'i'); 219 | for (const line of stdout.split('\n')) { 220 | const match = runtimeRe.exec(line); 221 | if (match && match[1] === platformVersion) { 222 | return match[2]; 223 | } 224 | } 225 | } catch {} 226 | 227 | // if nothing was found, pass platform version back 228 | return platformVersion; 229 | }; 230 | 231 | /** 232 | * Get the list of device types available in the current Xcode installation 233 | * 234 | * @this {import('../simctl').Simctl} 235 | * @return {Promise} List of the types of devices available 236 | * @throws {Error} If the corresponding simctl command fails 237 | */ 238 | commands.getDeviceTypes = async function getDeviceTypes () { 239 | const {stdout} = await this.exec('list', { 240 | args: ['devicetypes', '-j'], 241 | }); 242 | /* 243 | * JSON will be like: 244 | * { 245 | * "devicetypes" : [ 246 | * { 247 | * "name" : "iPhone 4s", 248 | * "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-4s" 249 | * }, 250 | * ... 251 | * } 252 | */ 253 | try { 254 | const deviceTypes = JSON.parse(stdout.trim()); 255 | return deviceTypes.devicetypes.map((type) => type.name); 256 | } catch (err) { 257 | throw new Error(`Unable to get list of device types: ${err.message}`); 258 | } 259 | }; 260 | 261 | /** 262 | * Get the full list of runtimes, devicetypes, devices and pairs as Object 263 | * 264 | * @this {import('../simctl').Simctl} 265 | * @return {Promise} Object containing device types, runtimes devices and pairs. 266 | * The resulting JSON will be like: 267 | * { 268 | * "devicetypes" : [ 269 | * { 270 | * "name" : "iPhone 4s", 271 | * "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-4s" 272 | * }, 273 | * ... 274 | * ], 275 | * "runtimes" : [ 276 | * { 277 | * "version" : '13.0', 278 | * "bundlePath" : '/Applications/Xcode11beta4.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime', 279 | * "isAvailable" : true, 280 | * "name" : 'iOS 13.0', 281 | * "identifier" : 'com.apple.CoreSimulator.SimRuntime.iOS-13-0', 282 | * "buildversion" : '17A5534d' 283 | * }, 284 | * ... 285 | * }, 286 | * "devices" : 287 | * { 288 | * 'com.apple.CoreSimulator.SimRuntime.iOS-13-0': [ [Object], [Object] ] }, 289 | * ... 290 | * }, 291 | * "pairs" : {} } 292 | * 293 | * } 294 | * @throws {Error} If the corresponding simctl command fails 295 | */ 296 | commands.list = async function list () { 297 | const {stdout} = await this.exec('list', { 298 | args: ['-j'], 299 | }); 300 | try { 301 | return JSON.parse(stdout.trim()); 302 | } catch (e) { 303 | throw new Error(`Unable to parse simctl list: ${e.message}`); 304 | } 305 | }; 306 | 307 | export default commands; 308 | -------------------------------------------------------------------------------- /lib/subcommands/location.js: -------------------------------------------------------------------------------- 1 | const commands = {}; 2 | 3 | /** 4 | * Formats the given location argument for simctl usage 5 | * 6 | * @param {string} name Argument name 7 | * @param {string|number} value Location argument value 8 | * @returns {string} Formatted value, for example -73.768254 9 | */ 10 | function formatArg (name, value) { 11 | const flt = parseFloat(`${value}`); 12 | if (isNaN(flt)) { 13 | throw new TypeError(`${name} must be a valid number, got '${value}' instead`); 14 | } 15 | return flt.toFixed(7); 16 | } 17 | 18 | /** 19 | * Set the Simulator location to a specific latitude and longitude. 20 | * This functionality is only available since Xcode 14. 21 | * 22 | * @this {import('../simctl').Simctl} 23 | * @param {string|number} latitude Location latitude value 24 | * @param {string|number} longitude Location longitude value 25 | * @throws {Error} If the corresponding simctl subcommand command 26 | * returns non-zero return code. 27 | * @throws {TypeError} If any of the arguments is not a valid value. 28 | */ 29 | commands.setLocation = async function setLocation (latitude, longitude) { 30 | const lat = formatArg('latitude', latitude); 31 | const lon = formatArg('longitude', longitude); 32 | await this.exec('location', { 33 | args: [this.requireUdid('location'), 'set', `${lat},${lon}`], 34 | }); 35 | }; 36 | 37 | /** 38 | * Stop any running scenario and clear any simulated location. 39 | * 40 | * @since Xcode 14. 41 | * @this {import('../simctl').Simctl} 42 | */ 43 | commands.clearLocation = async function clearLocation () { 44 | await this.exec('location', { 45 | args: [this.requireUdid('location'), 'clear'], 46 | }); 47 | }; 48 | 49 | export default commands; 50 | -------------------------------------------------------------------------------- /lib/subcommands/openurl.js: -------------------------------------------------------------------------------- 1 | const commands = {}; 2 | 3 | /** 4 | * Open URL scheme on Simulator. iOS will automatically try 5 | * to find a matching application, which supports the given scheme. 6 | * It is required that Simulator is in _booted_ state. 7 | * 8 | * @this {import('../simctl').Simctl} 9 | * @param {string} url - The URL scheme to open, for example http://appiom.io 10 | * will be opened by the built-in mobile browser. 11 | * @return {Promise} Command execution result. 12 | * @throws {Error} If the corresponding simctl subcommand command 13 | * returns non-zero return code. 14 | * @throws {Error} If the `udid` instance property is unset 15 | */ 16 | commands.openUrl = async function openUrl (url) { 17 | return await this.exec('openurl', { 18 | args: [this.requireUdid('openurl'), url], 19 | }); 20 | }; 21 | 22 | export default commands; 23 | -------------------------------------------------------------------------------- /lib/subcommands/pbcopy.js: -------------------------------------------------------------------------------- 1 | const commands = {}; 2 | 3 | /** 4 | * Set the content of Simulator pasteboard. 5 | * It is required that Simulator is in _booted_ state. 6 | * 7 | * @since Xcode SDK 8.1 8 | * @this {import('../simctl').Simctl} 9 | * @param {string} content - The actual string content to be set. 10 | * @param {BufferEncoding} [encoding='utf8'] - The encoding of the given pasteboard content. 11 | * utf8 by default. 12 | * @throws {Error} If the corresponding simctl subcommand command 13 | * returns non-zero return code. 14 | * @throws {Error} If the `udid` instance property is unset 15 | */ 16 | commands.setPasteboard = async function setPasteboard (content, encoding = 'utf8') { 17 | const pbCopySubprocess = await this.exec('pbcopy', { 18 | args: [this.requireUdid('pbcopy')], 19 | asynchronous: true, 20 | }); 21 | await pbCopySubprocess.start(0); 22 | const exitCodeVerifier = pbCopySubprocess.join(); 23 | const stdin = pbCopySubprocess.proc?.stdin; 24 | if (stdin) { 25 | stdin.setDefaultEncoding(encoding); 26 | stdin.write(content); 27 | stdin.end(); 28 | } 29 | await exitCodeVerifier; 30 | }; 31 | 32 | export default commands; 33 | -------------------------------------------------------------------------------- /lib/subcommands/pbpaste.js: -------------------------------------------------------------------------------- 1 | const commands = {}; 2 | 3 | /** 4 | * Get the content of Simulator pasteboard. 5 | * It is required that Simulator is in _booted_ state. 6 | * 7 | * @since Xcode 8.1 SDK 8 | * @this {import('../simctl').Simctl} 9 | * @param {string} [encoding='utf8'] - The encoding of the returned pasteboard content. 10 | * UTF-8 by default. 11 | * @return {Promise} Current content of Simulator pasteboard or an empty string. 12 | * @throws {Error} If the corresponding simctl subcommand command 13 | * returns non-zero return code. 14 | * @throws {Error} If the `udid` instance property is unset 15 | */ 16 | commands.getPasteboard = async function getPasteboard (encoding = 'utf8') { 17 | const {stdout} = await this.exec('pbpaste', { 18 | args: [this.requireUdid('pbpaste')], 19 | encoding, 20 | }); 21 | return stdout; 22 | }; 23 | 24 | export default commands; 25 | -------------------------------------------------------------------------------- /lib/subcommands/privacy.js: -------------------------------------------------------------------------------- 1 | const commands = {}; 2 | 3 | /** 4 | * Grants the given permission on the app with the given bundle identifier 5 | * 6 | * @since Xcode 11.4 SDK 7 | * @this {import('../simctl').Simctl} 8 | * @param {string} bundleId the identifier of the application whose 9 | * privacy settings are going to be changed 10 | * @param {string} perm one of possible permission values: 11 | * - all: Apply the action to all services. 12 | * - calendar: Allow access to calendar. 13 | * - contacts-limited: Allow access to basic contact info. 14 | * - contacts: Allow access to full contact details. 15 | * - location: Allow access to location services when app is in use. 16 | * - location-always: Allow access to location services at all times. 17 | * - photos-add: Allow adding photos to the photo library. 18 | * - photos: Allow full access to the photo library. 19 | * - media-library: Allow access to the media library. 20 | * - microphone: Allow access to audio input. 21 | * - motion: Allow access to motion and fitness data. 22 | * - reminders: Allow access to reminders. 23 | * - siri: Allow use of the app with Siri. 24 | * @throws {Error} if the current SDK version does not support the command 25 | * or there was an error while granting the permission 26 | * @throws {Error} If the `udid` instance property is unset 27 | */ 28 | commands.grantPermission = async function grantPermission (bundleId, perm) { 29 | await this.exec('privacy', { 30 | args: [this.requireUdid('privacy grant'), 'grant', perm, bundleId], 31 | }); 32 | }; 33 | 34 | /** 35 | * Revokes the given permission on the app with the given bundle identifier 36 | * after it has been granted 37 | * 38 | * @since Xcode 11.4 SDK 39 | * @this {import('../simctl').Simctl} 40 | * @param {string} bundleId the identifier of the application whose 41 | * privacy settings are going to be changed 42 | * @param {string} perm one of possible permission values (see `grantPermission`) 43 | * @throws {Error} if the current SDK version does not support the command 44 | * or there was an error while revoking the permission 45 | * @throws {Error} If the `udid` instance property is unset 46 | */ 47 | commands.revokePermission = async function revokePermission (bundleId, perm) { 48 | await this.exec('privacy', { 49 | args: [this.requireUdid('privacy revoke'), 'revoke', perm, bundleId], 50 | }); 51 | }; 52 | 53 | /** 54 | * Resets the given permission on the app with the given bundle identifier 55 | * to its default state 56 | * 57 | * @since Xcode 11.4 SDK 58 | * @this {import('../simctl').Simctl} 59 | * @param {string} bundleId the identifier of the application whose 60 | * privacy settings are going to be changed 61 | * @param {string} perm one of possible permission values (see `grantPermission`) 62 | * @throws {Error} if the current SDK version does not support the command 63 | * or there was an error while resetting the permission 64 | * @throws {Error} If the `udid` instance property is unset 65 | */ 66 | commands.resetPermission = async function resetPermission (bundleId, perm) { 67 | await this.exec('privacy', { 68 | args: [this.requireUdid('private reset'), 'reset', perm, bundleId], 69 | }); 70 | }; 71 | 72 | export default commands; 73 | -------------------------------------------------------------------------------- /lib/subcommands/push.js: -------------------------------------------------------------------------------- 1 | import { rimraf } from 'rimraf'; 2 | import { uuidV4 } from '../helpers'; 3 | import path from 'path'; 4 | import os from 'os'; 5 | import fs from 'fs/promises'; 6 | 7 | const commands = {}; 8 | 9 | /** 10 | * Send a simulated push notification 11 | * 12 | * @since Xcode 11.4 SDK 13 | * @this {import('../simctl').Simctl} 14 | * @param {Object} payload - The object that describes Apple push notification content. 15 | * It must contain a top-level "Simulator Target Bundle" key with a string value matching 16 | * the target application‘s bundle identifier and "aps" key with valid Apple Push Notification values. 17 | * For example: 18 | * { 19 | * "Simulator Target Bundle": "com.apple.Preferences", 20 | * "aps": { 21 | * "alert": "This is a simulated notification!", 22 | * "badge": 3, 23 | * "sound": "default" 24 | * } 25 | * } 26 | * @throws {Error} if the current SDK version does not support the command 27 | * or there was an error while pushing the notification 28 | * @throws {Error} If the `udid` instance property is unset 29 | */ 30 | commands.pushNotification = async function pushNotification (payload) { 31 | const dstPath = path.resolve(os.tmpdir(), `${await uuidV4()}.json`); 32 | try { 33 | await fs.writeFile(dstPath, JSON.stringify(payload), 'utf8'); 34 | await this.exec('push', { 35 | args: [this.requireUdid('push'), dstPath], 36 | }); 37 | } finally { 38 | await rimraf(dstPath); 39 | } 40 | }; 41 | 42 | export default commands; 43 | -------------------------------------------------------------------------------- /lib/subcommands/shutdown.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import log, { LOG_PREFIX } from '../logger'; 3 | 4 | const commands = {}; 5 | 6 | /** 7 | * Shutdown the given Simulator if it is running. 8 | * 9 | * @this {import('../simctl').Simctl} 10 | * @throws {Error} If the corresponding simctl subcommand command 11 | * returns non-zero return code. 12 | * @throws {Error} If the `udid` instance property is unset 13 | */ 14 | commands.shutdownDevice = async function shutdownDevice () { 15 | try { 16 | await this.exec('shutdown', { 17 | args: [this.requireUdid('shutdown')], 18 | }); 19 | } catch (e) { 20 | if (!_.includes(e.message, 'current state: Shutdown')) { 21 | throw e; 22 | } 23 | log.debug(LOG_PREFIX, `Simulator already in 'Shutdown' state. Continuing`); 24 | } 25 | }; 26 | 27 | export default commands; 28 | -------------------------------------------------------------------------------- /lib/subcommands/spawn.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | 4 | const commands = {}; 5 | 6 | /** 7 | * Spawn the particular process on Simulator. 8 | * It is required that Simulator is in _booted_ state. 9 | * 10 | * @this {import('../simctl').Simctl} 11 | * @param {string|string[]} args - Spawn arguments 12 | * @param {object} [env={}] - Additional environment variables mapping. 13 | * @return {Promise} Command execution result. 14 | * @throws {Error} If the corresponding simctl subcommand command 15 | * returns non-zero return code. 16 | * @throws {Error} If the `udid` instance property is unset 17 | */ 18 | commands.spawnProcess = async function spawnProcess (args, env = {}) { 19 | if (_.isEmpty(args)) { 20 | throw new Error('Spawn arguments are required'); 21 | } 22 | 23 | return await this.exec('spawn', { 24 | args: [this.requireUdid('spawn'), ...(_.isArray(args) ? args : [args])], 25 | env, 26 | }); 27 | }; 28 | 29 | /** 30 | * Prepare SubProcess instance for a new process, which is going to be spawned 31 | * on Simulator. 32 | * 33 | * @this {import('../simctl').Simctl} 34 | * @param {string|string[]} args - Spawn arguments 35 | * @param {object} [env={}] - Additional environment variables mapping. 36 | * @return {Promise} The instance of the process to be spawned. 37 | * @throws {Error} If the `udid` instance property is unset 38 | */ 39 | commands.spawnSubProcess = async function spawnSubProcess (args, env = {}) { 40 | if (_.isEmpty(args)) { 41 | throw new Error('Spawn arguments are required'); 42 | } 43 | 44 | return await this.exec('spawn', { 45 | args: [this.requireUdid('spawn'), ...(_.isArray(args) ? args : [args])], 46 | env, 47 | asynchronous: true, 48 | }); 49 | }; 50 | 51 | export default commands; 52 | -------------------------------------------------------------------------------- /lib/subcommands/terminate.js: -------------------------------------------------------------------------------- 1 | const commands = {}; 2 | 3 | /** 4 | * Terminate the given running application on Simulator. 5 | * It is required that Simulator is in _booted_ state. 6 | * 7 | * @this {import('../simctl').Simctl} 8 | * @param {string} bundleId - Bundle identifier of the application, 9 | * which is going to be terminated. 10 | * @throws {Error} If the corresponding simctl subcommand command 11 | * returns non-zero return code. 12 | * @throws {Error} If the `udid` instance property is unset 13 | */ 14 | commands.terminateApp = async function terminateApp (bundleId) { 15 | await this.exec('terminate', { 16 | args: [this.requireUdid('terminate'), bundleId], 17 | }); 18 | }; 19 | 20 | export default commands; 21 | -------------------------------------------------------------------------------- /lib/subcommands/ui.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | 4 | const commands = {}; 5 | 6 | /** 7 | * Retrieves the current UI appearance value from the given simulator 8 | * 9 | * @since Xcode 11.4 SDK 10 | * @this {import('../simctl').Simctl} 11 | * @return {Promise} the appearance value, for example 'light' or 'dark' 12 | * @throws {Error} if the current SDK version does not support the command 13 | * or there was an error while getting the value 14 | * @throws {Error} If the `udid` instance property is unset 15 | */ 16 | commands.getAppearance = async function getAppearance () { 17 | const {stdout} = await this.exec('ui', { 18 | args: [this.requireUdid('ui'), 'appearance'], 19 | }); 20 | return _.trim(stdout); 21 | }; 22 | 23 | /** 24 | * Sets the UI appearance to the given style 25 | * 26 | * @since Xcode 11.4 SDK 27 | * @this {import('../simctl').Simctl} 28 | * @param {string} appearance valid appearance value, for example 'light' or 'dark' 29 | * @throws {Error} if the current SDK version does not support the command 30 | * or there was an error while getting the value 31 | * @throws {Error} If the `udid` instance property is unset 32 | */ 33 | commands.setAppearance = async function setAppearance (appearance) { 34 | await this.exec('ui', { 35 | args: [this.requireUdid('ui'), 'appearance', appearance], 36 | }); 37 | }; 38 | 39 | /** 40 | * Retrieves the current increase contrast configuration value from the given simulator. 41 | * The value could be: 42 | * - enabled: Increase Contrast is enabled. 43 | * - disabled: Increase Contrast is disabled. 44 | * - unsupported: The platform or runtime version do not support the Increase Contrast setting. 45 | * - unknown: The current setting is unknown or there was an error detecting it. 46 | * 47 | * @since Xcode 15 (but lower xcode could have this command) 48 | * @this {import('../simctl').Simctl} 49 | * @return {Promise} the contrast configuration value. 50 | * Possible return value is 'enabled', 'disabled', 51 | * 'unsupported' or 'unknown' with Xcode 16.2. 52 | * @throws {Error} if the current SDK version does not support the command 53 | * or there was an error while getting the value. 54 | * @throws {Error} If the `udid` instance property is unset 55 | */ 56 | commands.getIncreaseContrast = async function getIncreaseContrast () { 57 | const {stdout} = await this.exec('ui', { 58 | args: [this.requireUdid('ui'), 'increase_contrast'], 59 | }); 60 | return _.trim(stdout); 61 | }; 62 | 63 | /** 64 | * Sets the increase constrast configuration for the given simulator. 65 | * Acceptable values (with Xcode 16.2, iOS 18.1) are 'enabled' or 'disabled' 66 | * They would change in the future version, so please validate the given value 67 | * in the caller side. 68 | * 69 | * @since Xcode 15 (but lower xcode could have this command) 70 | * @this {import('../simctl').Simctl} 71 | * @param {string} increaseContrast valid increase constrast configuration value. 72 | * Acceptable value is 'enabled' or 'disabled' with Xcode 16.2. 73 | * @throws {Error} if the current SDK version does not support the command 74 | * or the given value was invalid for the command. 75 | * @throws {Error} If the `udid` instance property is unset 76 | */ 77 | commands.setIncreaseContrast = async function setIncreaseContrast (increaseContrast) { 78 | await this.exec('ui', { 79 | args: [this.requireUdid('ui'), 'increase_contrast', increaseContrast], 80 | }); 81 | }; 82 | 83 | /** 84 | * Retrieves the current content size value from the given simulator. 85 | * The value could be: 86 | * Standard sizes: extra-small, small, medium, large, extra-large, 87 | * extra-extra-large, extra-extra-extra-large 88 | * Extended range sizes: accessibility-medium, accessibility-large, 89 | * accessibility-extra-large, accessibility-extra-extra-large, 90 | * accessibility-extra-extra-extra-large 91 | * Other values: unknown, unsupported. 92 | * 93 | * @since Xcode 15 (but lower xcode could have this command) 94 | * @this {import('../simctl').Simctl} 95 | * @return {Promise} the content size value. Possible return value is 96 | * extra-small, small, medium, large, extra-large, extra-extra-large, 97 | * extra-extra-extra-large, accessibility-medium, accessibility-large, 98 | * accessibility-extra-large, accessibility-extra-extra-large, 99 | * accessibility-extra-extra-extra-large, 100 | * unknown or unsupported with Xcode 16.2. 101 | * @throws {Error} if the current SDK version does not support the command 102 | * or there was an error while getting the value. 103 | * @throws {Error} If the `udid` instance property is unset 104 | */ 105 | commands.getContentSize = async function getContentSize () { 106 | const {stdout} = await this.exec('ui', { 107 | args: [this.requireUdid('ui'), 'content_size'], 108 | }); 109 | return _.trim(stdout); 110 | }; 111 | 112 | /** 113 | * Sets content size for the given simulator. 114 | * Acceptable values (with Xcode 16.2, iOS 18.1) are below: 115 | * Standard sizes: extra-small, small, medium, large, extra-large, 116 | * extra-extra-large, extra-extra-extra-large 117 | * Extended range sizes: accessibility-medium, accessibility-large, 118 | * accessibility-extra-large, accessibility-extra-extra-large, 119 | * accessibility-extra-extra-extra-large 120 | * Or 'increment' or 'decrement' 121 | * They would change in the future version, so please validate the given value 122 | * in the caller side. 123 | * 124 | * @since Xcode 15 (but lower xcode could have this command) 125 | * @this {import('../simctl').Simctl} 126 | * @param {string} contentSizeAction valid content size or action value. Acceptable value is 127 | * extra-small, small, medium, large, extra-large, extra-extra-large, 128 | * extra-extra-extra-large, accessibility-medium, accessibility-large, 129 | * accessibility-extra-large, accessibility-extra-extra-large, 130 | * accessibility-extra-extra-extra-large with Xcode 16.2. 131 | * @throws {Error} if the current SDK version does not support the command 132 | * or the given value was invalid for the command. 133 | * @throws {Error} If the `udid` instance property is unset 134 | */ 135 | commands.setContentSize = async function setContentSize (contentSizeAction) { 136 | await this.exec('ui', { 137 | args: [this.requireUdid('ui'), 'content_size', contentSizeAction], 138 | }); 139 | }; 140 | 141 | export default commands; 142 | -------------------------------------------------------------------------------- /lib/subcommands/uninstall.js: -------------------------------------------------------------------------------- 1 | const commands = {}; 2 | 3 | /** 4 | * Remove the particular application package from Simulator. 5 | * It is required that Simulator is in _booted_ state and 6 | * the application with given bundle identifier is already installed. 7 | * 8 | * @this {import('../simctl').Simctl} 9 | * @param {string} bundleId - Bundle identifier of the application, 10 | * which is going to be removed. 11 | * @throws {Error} If the corresponding simctl subcommand command 12 | * returns non-zero return code. 13 | * @throws {Error} If the `udid` instance property is unset 14 | */ 15 | commands.removeApp = async function removeApp (bundleId) { 16 | await this.exec('uninstall', { 17 | args: [this.requireUdid('uninstall'), bundleId], 18 | }); 19 | }; 20 | 21 | export default commands; 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-simctl", 3 | "description": "Wrapper around Apple's simctl binary", 4 | "tags": [ 5 | "apple", 6 | "ios", 7 | "simctl" 8 | ], 9 | "version": "8.0.5", 10 | "author": "Appium Contributors", 11 | "license": "Apache-2.0", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/appium/node-simctl.git" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/appium/node-simctl/issues" 18 | }, 19 | "engines": { 20 | "node": "^20.19.0 || ^22.12.0 || >=24.0.0", 21 | "npm": ">=10" 22 | }, 23 | "main": "./build/index.js", 24 | "bin": {}, 25 | "directories": { 26 | "lib": "./lib" 27 | }, 28 | "files": [ 29 | "index.js", 30 | "lib", 31 | "build/index.*", 32 | "build/lib", 33 | "CHANGELOG.md" 34 | ], 35 | "dependencies": { 36 | "@appium/logger": "^2.0.0-rc.1", 37 | "asyncbox": "^3.0.0", 38 | "bluebird": "^3.5.1", 39 | "lodash": "^4.2.1", 40 | "rimraf": "^6.0.1", 41 | "semver": "^7.0.0", 42 | "source-map-support": "^0.x", 43 | "teen_process": "^3.0.0", 44 | "uuid": "^13.0.0", 45 | "which": "^5.0.0" 46 | }, 47 | "scripts": { 48 | "build": "tsc -b", 49 | "clean": "npm run build -- --clean", 50 | "rebuild": "npm run clean; npm run build", 51 | "dev": "npm run build -- --watch", 52 | "lint": "eslint .", 53 | "lint:fix": "npm run lint -- --fix", 54 | "prepare": "npm run build", 55 | "test": "mocha --exit --timeout 1m \"./test/unit/**/*-specs.*js\"", 56 | "e2e-test": "mocha --exit --timeout 5m \"./test/e2e/**/*-specs.js\"" 57 | }, 58 | "prettier": { 59 | "bracketSpacing": false, 60 | "printWidth": 100, 61 | "singleQuote": true 62 | }, 63 | "devDependencies": { 64 | "@appium/eslint-config-appium-ts": "^2.0.0-rc.1", 65 | "@appium/tsconfig": "^1.0.0-rc.1", 66 | "@appium/types": "^1.0.0-rc.1", 67 | "@semantic-release/changelog": "^6.0.1", 68 | "@semantic-release/git": "^10.0.1", 69 | "@types/bluebird": "^3.5.38", 70 | "@types/lodash": "^4.14.196", 71 | "@types/mocha": "^10.0.1", 72 | "@types/node": "^24.0.0", 73 | "@types/teen_process": "^2.0.2", 74 | "appium-xcode": "^6.0.0", 75 | "chai": "^6.0.0", 76 | "chai-as-promised": "^8.0.0", 77 | "conventional-changelog-conventionalcommits": "^9.0.0", 78 | "mocha": "^11.0.1", 79 | "prettier": "^3.0.0", 80 | "proxyquire": "^2.1.3", 81 | "semantic-release": "^25.0.0", 82 | "sinon": "^21.0.0", 83 | "ts-node": "^10.9.1", 84 | "typescript": "^5.4.3" 85 | }, 86 | "types": "./build/index.d.ts" 87 | } 88 | -------------------------------------------------------------------------------- /test/e2e/simctl-e2e-specs.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { Simctl } from '../../lib/simctl.js'; 3 | import xcode from 'appium-xcode'; 4 | import { retryInterval } from 'asyncbox'; 5 | import { rimraf } from 'rimraf'; 6 | import { uuidV4 } from '../../lib/helpers'; 7 | import path from 'path'; 8 | import os from 'os'; 9 | import fs from 'fs/promises'; 10 | 11 | describe('simctl', function () { 12 | const DEVICE_NAME = process.env.DEVICE_NAME || 'iPhone X'; 13 | const MOCHA_TIMEOUT = 200000; 14 | this.timeout(MOCHA_TIMEOUT); 15 | 16 | let chai; 17 | let chaiAsPromised; 18 | let expect; 19 | let should; 20 | let randName; 21 | let validSdks = []; 22 | let sdk; 23 | let simctl; 24 | 25 | before(async function () { 26 | chai = await import('chai'); 27 | chaiAsPromised = await import('chai-as-promised'); 28 | 29 | chai.use(chaiAsPromised.default); 30 | expect = chai.expect; 31 | should = chai.should(); 32 | 33 | simctl = new Simctl(); 34 | const devices = await simctl.getDevices(); 35 | console.log(`Found devices: ${JSON.stringify(devices, null, 2)}`); // eslint-disable-line no-console 36 | validSdks = _.keys(devices) 37 | .filter((key) => !_.isEmpty(devices[key])) 38 | .sort((a, b) => a - b); 39 | if (!validSdks.length) { 40 | throw new Error('No valid SDKs'); 41 | } 42 | console.log(`Found valid SDKs: ${validSdks.join(', ')}`); // eslint-disable-line no-console 43 | sdk = `${process.env.IOS_SDK || _.last(validSdks)}`; 44 | 45 | // need to find a random name that does not already exist 46 | // give it 5 tries 47 | for (let i = 0; i < 5; i++) { 48 | let randNum = parseInt(Math.random() * 100, 10); 49 | randName = `device${randNum}`; 50 | 51 | let nameFound = false; 52 | for (let list of _.values(devices)) { 53 | if (_.includes(_.map(list, 'name'), randName)) { 54 | // need to find another random name 55 | nameFound = true; 56 | break; 57 | } 58 | } 59 | if (!nameFound) break; // eslint-disable-line curly 60 | } 61 | }); 62 | 63 | it('should retrieve a device with compatible properties', async function () { 64 | const devices = (await simctl.getDevices())[sdk]; 65 | const firstDevice = devices[0]; 66 | const expectedList = ['name', 'sdk', 'state', 'udid']; 67 | firstDevice.should.have.any.keys(...expectedList); 68 | }); 69 | 70 | describe('createDevice', function () { 71 | after(async function () { 72 | if (simctl.udid) { 73 | await simctl.deleteDevice(16000); 74 | simctl.udid = null; 75 | } 76 | }); 77 | 78 | it('should create a device', async function () { 79 | simctl.udid = await simctl.createDevice(randName, DEVICE_NAME, sdk); 80 | (typeof simctl.udid).should.equal('string'); 81 | simctl.udid.length.should.equal(36); 82 | }); 83 | 84 | it('should create a device and be able to see it in devices list right away', async function () { 85 | const numSimsBefore = (await simctl.getDevices())[sdk].length; 86 | simctl.udid = await simctl.createDevice('node-simctl test', DEVICE_NAME, sdk); 87 | const numSimsAfter = (await simctl.getDevices())[sdk].length; 88 | numSimsAfter.should.equal(numSimsBefore + 1); 89 | }); 90 | }); 91 | 92 | describe('device manipulation', function () { 93 | let simctl; 94 | const name = 'node-simctl test'; 95 | beforeEach(async function () { 96 | simctl = new Simctl(); 97 | simctl.udid = await simctl.createDevice('node-simctl test', DEVICE_NAME, sdk); 98 | }); 99 | afterEach(async function () { 100 | if (simctl.udid) { 101 | await simctl.deleteDevice(simctl.udid, 16000); 102 | simctl.udid = null; 103 | } 104 | }); 105 | it('should get devices', async function () { 106 | const sdkDevices = await simctl.getDevices(sdk); 107 | _.map(sdkDevices, 'name').should.include(name); 108 | }); 109 | 110 | it('should erase devices', async function () { 111 | await simctl.eraseDevice(16000); 112 | }); 113 | 114 | it('should delete devices', async function () { 115 | await simctl.deleteDevice(); 116 | const sdkDevices = await simctl.getDevices(sdk); 117 | _.map(sdkDevices, 'name').should.not.include(simctl.udid); 118 | 119 | // so we do not delete again 120 | simctl.udid = null; 121 | }); 122 | 123 | it('should not fail to shutdown a shutdown simulator', async function () { 124 | await simctl.shutdownDevice().should.eventually.not.be.rejected; 125 | }); 126 | }); 127 | 128 | it('should return a nice error for invalid usage', async function () { 129 | let err = null; 130 | try { 131 | await simctl.createDevice('foo', 'bar', 'baz'); 132 | } catch (e) { 133 | err = e; 134 | } 135 | should.exist(err); 136 | err.message.should.include(`Unable to parse version 'baz'`); 137 | }); 138 | 139 | describe('on running Simulator', function () { 140 | if (process.env.TRAVIS) { 141 | this.retries(3); 142 | } 143 | 144 | let major, minor; 145 | 146 | before(async function () { 147 | ({major, minor} = await xcode.getVersion(true)); 148 | if (major < 8 || (major === 8 && minor < 1)) { 149 | return this.skip(); 150 | } 151 | 152 | const sdk = process.env.IOS_SDK || _.last(validSdks); 153 | simctl.udid = await simctl.createDevice('runningSimTest', DEVICE_NAME, sdk); 154 | 155 | await simctl.bootDevice(); 156 | await simctl.startBootMonitor({timeout: MOCHA_TIMEOUT}); 157 | }); 158 | after(async function () { 159 | if (simctl.udid) { 160 | try { 161 | await simctl.shutdownDevice(); 162 | } catch {} 163 | await simctl.deleteDevice(); 164 | simctl.udid = null; 165 | } 166 | }); 167 | 168 | describe('startBootMonitor', function () { 169 | it('should be fulfilled if the simulator is already booted', async function () { 170 | if (major < 8 || (major === 8 && minor < 1)) { 171 | return this.skip(); 172 | } 173 | await simctl.startBootMonitor().should.eventually.be.fulfilled; 174 | }); 175 | it('should fail to monitor booting of non-existing simulator', async function () { 176 | if (major < 8 || (major === 8 && minor < 1)) { 177 | return this.skip(); 178 | } 179 | const udid = simctl.udid; 180 | try { 181 | simctl.udid = 'blabla'; 182 | await simctl.startBootMonitor({timeout: 1000}).should.eventually.be.rejected; 183 | } finally { 184 | simctl.udid = udid; 185 | } 186 | }); 187 | }); 188 | 189 | describe('pasteboard', function () { 190 | let pbRetries = 0; 191 | before(function () { 192 | if (major < 8 || (major === 8 && minor < 1)) { 193 | return this.skip(); 194 | } 195 | if (major === 9) { 196 | if (process.env.TRAVIS) { 197 | return this.skip(); 198 | } 199 | // TODO: recheck when full Xcode 9 comes out to see if pasteboard works better 200 | pbRetries = 200; 201 | this.timeout(200 * 1000 * 2); 202 | } 203 | }); 204 | it('should set and get the content of the pasteboard', async function () { 205 | const pbContent = 'blablabla'; 206 | const encoding = 'ascii'; 207 | 208 | await retryInterval(pbRetries, 1000, async () => { 209 | await simctl.setPasteboard(pbContent, encoding); 210 | (await simctl.getPasteboard(encoding)).should.eql(pbContent); 211 | }); 212 | }); 213 | }); 214 | 215 | describe('add media', function () { 216 | const BASE64_PNG = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; 217 | let picturePath; 218 | before(async function () { 219 | if (major < 8 || (major === 8 && minor < 1)) { 220 | return this.skip(); 221 | } 222 | picturePath = path.join(os.tmpdir(), `${await uuidV4()}.png`); 223 | await fs.writeFile(picturePath, Buffer.from(BASE64_PNG, 'base64').toString('binary'), 'binary'); 224 | }); 225 | after(async function () { 226 | if (picturePath) { 227 | await rimraf(picturePath); 228 | } 229 | }); 230 | it('should add media files', async function () { 231 | (await simctl.addMedia(picturePath)).code.should.eql(0); 232 | }); 233 | }); 234 | 235 | it('should extract applications information', async function () { 236 | (await simctl.appInfo('com.apple.springboard')).should.include('ApplicationType'); 237 | }); 238 | 239 | describe('getEnv', function () { 240 | it('should get env variable value', async function () { 241 | const udid = await simctl.getEnv('SIMULATOR_UDID'); 242 | udid.length.should.be.above(0); 243 | }); 244 | it('should return null if no var is found', async function () { 245 | const udid = await simctl.getEnv('SIMULATOR_UDD'); 246 | _.isNull(udid).should.be.true; 247 | }); 248 | }); 249 | 250 | describe('getDeviceTypes', function () { 251 | it('should get device types', async function () { 252 | const deviceTypes = await simctl.getDeviceTypes(); 253 | deviceTypes.should.have.length; 254 | deviceTypes.length.should.be.above(0); 255 | // at least one type, no matter the version of Xcode, should be an iPhone 256 | deviceTypes.filter((el) => el.includes('iPhone')).length.should.be.above(1); 257 | }); 258 | }); 259 | 260 | describe('list', function () { 261 | it('should get everything from xcrun simctl list', async function () { 262 | const fullList = await simctl.list(); 263 | fullList.should.have.property('devicetypes'); 264 | fullList.should.have.property('runtimes'); 265 | fullList.should.have.property('devices'); 266 | fullList.should.have.property('pairs'); 267 | fullList.devicetypes.length.should.be.above(1); 268 | // at least one type, no matter the version of Xcode, should be an iPhone 269 | fullList.devicetypes.filter((el) => el.identifier.includes('iPhone')).length.should.be.above(0); 270 | // at least one runtime should be iOS 271 | fullList.runtimes.filter((el) => el.identifier.includes('iOS')).length.should.be.above(0); 272 | }); 273 | }); 274 | 275 | describe('getScreenshot', function() { 276 | it('should get a base64 string', async function () { 277 | const image = await simctl.getScreenshot(); 278 | 279 | expect(Buffer.from(image, 'base64').toString('base64') === image).to.be.true; 280 | }); 281 | }); 282 | 283 | describe('pushNotification', function() { 284 | it('should not throw an error when sending a push notification', async function () { 285 | if (process.env.CI) { 286 | // This test is unstable in CI env 287 | return this.skip(); 288 | } 289 | 290 | const payload = { 291 | 'Simulator Target Bundle': 'com.apple.Preferences', 292 | 'aps': { 293 | 'alert': 'This is a simulated notification!', 294 | 'badge': 3, 295 | 'sound': 'default' 296 | } 297 | }; 298 | 299 | await simctl.pushNotification(payload).should.be.fulfilled; 300 | }); 301 | }); 302 | }); 303 | }); 304 | -------------------------------------------------------------------------------- /test/unit/fixtures/devices-simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "devices" : { 3 | "tvOS 12.1" : [ 4 | { 5 | "availability" : "(available)", 6 | "state" : "Shutdown", 7 | "isAvailable" : true, 8 | "name" : "Apple TV", 9 | "udid" : "FA628127-1D5C-45C3-9918-A47BF7E2AE14", 10 | "availabilityError" : "" 11 | }, 12 | { 13 | "availability" : "(available)", 14 | "state" : "Shutdown", 15 | "isAvailable" : true, 16 | "name" : "Apple TV 4K", 17 | "udid" : "2EF493BE-E7D5-45E7-9725-8D2706F7220E", 18 | "availabilityError" : "" 19 | }, 20 | { 21 | "availability" : "(available)", 22 | "state" : "Shutdown", 23 | "isAvailable" : true, 24 | "name" : "Apple TV 4K (at 1080p)", 25 | "udid" : "FFECD143-B523-4A3D-BA27-1F9706F814CB", 26 | "availabilityError" : "" 27 | } 28 | ], 29 | "iOS 12.1" : [ 30 | { 31 | "availability" : "(available)", 32 | "state" : "Shutdown", 33 | "isAvailable" : true, 34 | "name" : "iPhone 5s", 35 | "udid" : "E17597CE-71EE-4402-8B1C-1B526446A3A2", 36 | "availabilityError" : "" 37 | }, 38 | { 39 | "availability" : "(available)", 40 | "state" : "Shutdown", 41 | "isAvailable" : true, 42 | "name" : "iPhone 6", 43 | "udid" : "1C7AB8B9-94C3-4806-86D7-77C13B483902", 44 | "availabilityError" : "" 45 | }, 46 | { 47 | "availability" : "(available)", 48 | "state" : "Shutdown", 49 | "isAvailable" : true, 50 | "name" : "iPhone 6 Plus", 51 | "udid" : "EE76EA77-E975-4198-9859-69DFF74252D2", 52 | "availabilityError" : "" 53 | }, 54 | { 55 | "availability" : "(available)", 56 | "state" : "Shutdown", 57 | "isAvailable" : true, 58 | "name" : "iPad Air", 59 | "udid" : "225C4FDB-5132-423A-9CFF-89D4474395F9", 60 | "availabilityError" : "" 61 | }, 62 | { 63 | "availability" : "(available)", 64 | "state" : "Shutdown", 65 | "isAvailable" : true, 66 | "name" : "iPad Air 2", 67 | "udid" : "DDDF281C-F912-471C-B30D-994A2644DF03", 68 | "availabilityError" : "" 69 | }, 70 | { 71 | "availability" : "(available)", 72 | "state" : "Shutdown", 73 | "isAvailable" : true, 74 | "name" : "iPad Pro (12.9-inch)", 75 | "udid" : "1D953CFF-1FBC-413E-B2AB-B4BA4DD5EEC2", 76 | "availabilityError" : "" 77 | }, 78 | { 79 | "availability" : "(available)", 80 | "state" : "Shutdown", 81 | "isAvailable" : true, 82 | "name" : "iPad Pro (12.9-inch) (2nd generation)", 83 | "udid" : "A7D1A0D7-D67B-409F-A73D-0DB53EDD860F", 84 | "availabilityError" : "" 85 | } 86 | ], 87 | "watchOS 5.1" : [ 88 | { 89 | "availability" : "(available)", 90 | "state" : "Shutdown", 91 | "isAvailable" : true, 92 | "name" : "Apple Watch Series 2 - 38mm", 93 | "udid" : "F40113CC-2973-4EBD-87F4-31852A6FF09B", 94 | "availabilityError" : "" 95 | }, 96 | { 97 | "availability" : "(available)", 98 | "state" : "Shutdown", 99 | "isAvailable" : true, 100 | "name" : "Apple Watch Series 2 - 42mm", 101 | "udid" : "5A83C524-FFF6-45AA-936C-365D2F1126F3", 102 | "availabilityError" : "" 103 | }, 104 | { 105 | "availability" : "(available)", 106 | "state" : "Shutdown", 107 | "isAvailable" : true, 108 | "name" : "Apple Watch Series 3 - 38mm", 109 | "udid" : "C76C585D-96D1-4037-80ED-C73DBBDB3521", 110 | "availabilityError" : "" 111 | }, 112 | { 113 | "availability" : "(available)", 114 | "state" : "Shutdown", 115 | "isAvailable" : true, 116 | "name" : "Apple Watch Series 3 - 42mm", 117 | "udid" : "DA5E98A8-2D04-474F-A0C3-9A0234F44CC9", 118 | "availabilityError" : "" 119 | }, 120 | { 121 | "availability" : "(available)", 122 | "state" : "Shutdown", 123 | "isAvailable" : true, 124 | "name" : "Apple Watch Series 4 - 40mm", 125 | "udid" : "DCD321B8-F265-4213-B248-C89AB8B806E1", 126 | "availabilityError" : "" 127 | }, 128 | { 129 | "availability" : "(available)", 130 | "state" : "Shutdown", 131 | "isAvailable" : true, 132 | "name" : "Apple Watch Series 4 - 44mm", 133 | "udid" : "EADBBE94-E97C-4D13-9FF2-44A3524113C7", 134 | "availabilityError" : "" 135 | } 136 | ] 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /test/unit/fixtures/devices-with-unavailable-simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "devices" : { 3 | "tvOS 12.1" : [ 4 | { 5 | "availability" : "(available)", 6 | "state" : "Shutdown", 7 | "isAvailable" : true, 8 | "name" : "Apple TV", 9 | "udid" : "FA628127-1D5C-45C3-9918-A47BF7E2AE14", 10 | "availabilityError" : "" 11 | }, 12 | { 13 | "availability" : "(available)", 14 | "state" : "Shutdown", 15 | "isAvailable" : true, 16 | "name" : "Apple TV 4K", 17 | "udid" : "2EF493BE-E7D5-45E7-9725-8D2706F7220E", 18 | "availabilityError" : "" 19 | }, 20 | { 21 | "availability" : "(available)", 22 | "state" : "Shutdown", 23 | "isAvailable" : true, 24 | "name" : "Apple TV 4K (at 1080p)", 25 | "udid" : "FFECD143-B523-4A3D-BA27-1F9706F814CB", 26 | "availabilityError" : "" 27 | } 28 | ], 29 | "iOS 12.1" : [ 30 | { 31 | "availability" : "(available)", 32 | "state" : "Shutdown", 33 | "isAvailable" : true, 34 | "name" : "iPhone 5s", 35 | "udid" : "E17597CE-71EE-4402-8B1C-1B526446A3A2", 36 | "availabilityError" : "" 37 | }, 38 | { 39 | "availability" : "(available)", 40 | "state" : "Shutdown", 41 | "isAvailable" : true, 42 | "name" : "iPhone 6", 43 | "udid" : "1C7AB8B9-94C3-4806-86D7-77C13B483902", 44 | "availabilityError" : "" 45 | }, 46 | { 47 | "availability" : "(available)", 48 | "state" : "Shutdown", 49 | "isAvailable" : true, 50 | "name" : "iPhone 6 Plus", 51 | "udid" : "EE76EA77-E975-4198-9859-69DFF74252D2", 52 | "availabilityError" : "" 53 | }, 54 | { 55 | "availability" : "(available)", 56 | "state" : "Shutdown", 57 | "isAvailable" : true, 58 | "name" : "iPad Air", 59 | "udid" : "225C4FDB-5132-423A-9CFF-89D4474395F9", 60 | "availabilityError" : "" 61 | }, 62 | { 63 | "availability" : "(available)", 64 | "state" : "Shutdown", 65 | "isAvailable" : true, 66 | "name" : "iPad Air 2", 67 | "udid" : "DDDF281C-F912-471C-B30D-994A2644DF03", 68 | "availabilityError" : "" 69 | }, 70 | { 71 | "availability" : "(available)", 72 | "state" : "Shutdown", 73 | "isAvailable" : true, 74 | "name" : "iPad Pro (12.9-inch)", 75 | "udid" : "1D953CFF-1FBC-413E-B2AB-B4BA4DD5EEC2", 76 | "availabilityError" : "" 77 | }, 78 | { 79 | "availability" : "(available)", 80 | "state" : "Shutdown", 81 | "isAvailable" : true, 82 | "name" : "iPad Pro (12.9-inch) (2nd generation)", 83 | "udid" : "A7D1A0D7-D67B-409F-A73D-0DB53EDD860F", 84 | "availabilityError" : "" 85 | } 86 | ], 87 | "tvOS 12.2" : [ 88 | { 89 | "availability" : "(unavailable, runtime profile not found)", 90 | "state" : "Shutdown", 91 | "isAvailable" : false, 92 | "name" : "Apple TV", 93 | "udid" : "84793AD4-E1C9-49EE-B85A-0DCBB0806279", 94 | "availabilityError" : "runtime profile not found" 95 | }, 96 | { 97 | "availability" : "(unavailable, runtime profile not found)", 98 | "state" : "Shutdown", 99 | "isAvailable" : false, 100 | "name" : "Apple TV 4K", 101 | "udid" : "B3ABE34D-7F23-4107-B381-3DD0184134D1", 102 | "availabilityError" : "runtime profile not found" 103 | }, 104 | { 105 | "availability" : "(unavailable, runtime profile not found)", 106 | "state" : "Shutdown", 107 | "isAvailable" : false, 108 | "name" : "Apple TV 4K (at 1080p)", 109 | "udid" : "289113A3-52A3-4904-B7B6-E66DF666EDB5", 110 | "availabilityError" : "runtime profile not found" 111 | } 112 | ], 113 | "watchOS 5.2" : [ 114 | { 115 | "availability" : "(unavailable, runtime profile not found)", 116 | "state" : "Shutdown", 117 | "isAvailable" : false, 118 | "name" : "Apple Watch Series 2 - 38mm", 119 | "udid" : "76D3F91D-5C60-4B9B-A30A-A01A63C6FE73", 120 | "availabilityError" : "runtime profile not found" 121 | }, 122 | { 123 | "availability" : "(unavailable, runtime profile not found)", 124 | "state" : "Shutdown", 125 | "isAvailable" : false, 126 | "name" : "Apple Watch Series 2 - 42mm", 127 | "udid" : "26B36BEB-A564-4321-A108-72DEEB2D63F9", 128 | "availabilityError" : "runtime profile not found" 129 | }, 130 | { 131 | "availability" : "(unavailable, runtime profile not found)", 132 | "state" : "Shutdown", 133 | "isAvailable" : false, 134 | "name" : "Apple Watch Series 3 - 38mm", 135 | "udid" : "63D10205-08E3-4A8C-9334-DB0868D87554", 136 | "availabilityError" : "runtime profile not found" 137 | }, 138 | { 139 | "availability" : "(unavailable, runtime profile not found)", 140 | "state" : "Shutdown", 141 | "isAvailable" : false, 142 | "name" : "Apple Watch Series 3 - 42mm", 143 | "udid" : "420CD468-9CED-44F7-A8AE-F1FABB18BFB2", 144 | "availabilityError" : "runtime profile not found" 145 | }, 146 | { 147 | "availability" : "(unavailable, runtime profile not found)", 148 | "state" : "Shutdown", 149 | "isAvailable" : false, 150 | "name" : "Apple Watch Series 4 - 40mm", 151 | "udid" : "6E507921-8AE9-4562-93C6-52DFDA87EA40", 152 | "availabilityError" : "runtime profile not found" 153 | }, 154 | { 155 | "availability" : "(unavailable, runtime profile not found)", 156 | "state" : "Shutdown", 157 | "isAvailable" : false, 158 | "name" : "Apple Watch Series 4 - 44mm", 159 | "udid" : "E5DDFDFA-693A-46A7-B42A-866681E15B83", 160 | "availabilityError" : "runtime profile not found" 161 | } 162 | ], 163 | "watchOS 5.1" : [ 164 | { 165 | "availability" : "(available)", 166 | "state" : "Shutdown", 167 | "isAvailable" : true, 168 | "name" : "Apple Watch Series 2 - 38mm", 169 | "udid" : "F40113CC-2973-4EBD-87F4-31852A6FF09B", 170 | "availabilityError" : "" 171 | }, 172 | { 173 | "availability" : "(available)", 174 | "state" : "Shutdown", 175 | "isAvailable" : true, 176 | "name" : "Apple Watch Series 2 - 42mm", 177 | "udid" : "5A83C524-FFF6-45AA-936C-365D2F1126F3", 178 | "availabilityError" : "" 179 | }, 180 | { 181 | "availability" : "(available)", 182 | "state" : "Shutdown", 183 | "isAvailable" : true, 184 | "name" : "Apple Watch Series 3 - 38mm", 185 | "udid" : "C76C585D-96D1-4037-80ED-C73DBBDB3521", 186 | "availabilityError" : "" 187 | }, 188 | { 189 | "availability" : "(available)", 190 | "state" : "Shutdown", 191 | "isAvailable" : true, 192 | "name" : "Apple Watch Series 3 - 42mm", 193 | "udid" : "DA5E98A8-2D04-474F-A0C3-9A0234F44CC9", 194 | "availabilityError" : "" 195 | }, 196 | { 197 | "availability" : "(available)", 198 | "state" : "Shutdown", 199 | "isAvailable" : true, 200 | "name" : "Apple Watch Series 4 - 40mm", 201 | "udid" : "DCD321B8-F265-4213-B248-C89AB8B806E1", 202 | "availabilityError" : "" 203 | }, 204 | { 205 | "availability" : "(available)", 206 | "state" : "Shutdown", 207 | "isAvailable" : true, 208 | "name" : "Apple Watch Series 4 - 44mm", 209 | "udid" : "EADBBE94-E97C-4D13-9FF2-44A3524113C7", 210 | "availabilityError" : "" 211 | } 212 | ], 213 | "iOS 12.2" : [ 214 | { 215 | "availability" : "(unavailable, runtime profile not found)", 216 | "state" : "Shutdown", 217 | "isAvailable" : false, 218 | "name" : "iPhone 5s", 219 | "udid" : "DBC918BB-D519-408D-A5E7-EAD66D875A40", 220 | "availabilityError" : "runtime profile not found" 221 | }, 222 | { 223 | "availability" : "(unavailable, runtime profile not found)", 224 | "state" : "Shutdown", 225 | "isAvailable" : false, 226 | "name" : "iPhone 6", 227 | "udid" : "5CC1A69E-75B0-4109-8474-61C605C61493", 228 | "availabilityError" : "runtime profile not found" 229 | }, 230 | { 231 | "availability" : "(unavailable, runtime profile not found)", 232 | "state" : "Shutdown", 233 | "isAvailable" : false, 234 | "name" : "iPhone 6 Plus", 235 | "udid" : "99FE1BB4-66F4-47CA-A144-9198A5CBB9B6", 236 | "availabilityError" : "runtime profile not found" 237 | }, 238 | { 239 | "availability" : "(unavailable, runtime profile not found)", 240 | "state" : "Shutdown", 241 | "isAvailable" : false, 242 | "name" : "iPad Air", 243 | "udid" : "F78CD721-6DB1-4C19-9601-FEB84D2D0A12", 244 | "availabilityError" : "runtime profile not found" 245 | }, 246 | { 247 | "availability" : "(unavailable, runtime profile not found)", 248 | "state" : "Shutdown", 249 | "isAvailable" : false, 250 | "name" : "iPad Air 2", 251 | "udid" : "7E3B9E66-5995-4342-929C-43CCFF4E0819", 252 | "availabilityError" : "runtime profile not found" 253 | }, 254 | { 255 | "availability" : "(unavailable, runtime profile not found)", 256 | "state" : "Shutdown", 257 | "isAvailable" : false, 258 | "name" : "iPad Pro (12.9-inch)", 259 | "udid" : "D5483905-8384-4F9A-B337-AE5B7F6B6994", 260 | "availabilityError" : "runtime profile not found" 261 | }, 262 | { 263 | "availability" : "(unavailable, runtime profile not found)", 264 | "state" : "Shutdown", 265 | "isAvailable" : false, 266 | "name" : "iPad Pro (12.9-inch) (2nd generation)", 267 | "udid" : "DDC7CD7C-8E12-42AF-B39A-DB847CCD84FD", 268 | "availabilityError" : "runtime profile not found" 269 | } 270 | ] 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /test/unit/fixtures/devices-with-unavailable.json: -------------------------------------------------------------------------------- 1 | { 2 | "devices" : { 3 | "com.apple.CoreSimulator.SimRuntime.tvOS-12-1" : [ 4 | { 5 | "availability" : "(available)", 6 | "state" : "Shutdown", 7 | "isAvailable" : true, 8 | "name" : "Apple TV", 9 | "udid" : "FA628127-1D5C-45C3-9918-A47BF7E2AE14", 10 | "availabilityError" : "" 11 | }, 12 | { 13 | "availability" : "(available)", 14 | "state" : "Shutdown", 15 | "isAvailable" : true, 16 | "name" : "Apple TV 4K", 17 | "udid" : "2EF493BE-E7D5-45E7-9725-8D2706F7220E", 18 | "availabilityError" : "" 19 | }, 20 | { 21 | "availability" : "(available)", 22 | "state" : "Shutdown", 23 | "isAvailable" : true, 24 | "name" : "Apple TV 4K (at 1080p)", 25 | "udid" : "FFECD143-B523-4A3D-BA27-1F9706F814CB", 26 | "availabilityError" : "" 27 | } 28 | ], 29 | "com.apple.CoreSimulator.SimRuntime.iOS-12-1" : [ 30 | { 31 | "availability" : "(available)", 32 | "state" : "Shutdown", 33 | "isAvailable" : true, 34 | "name" : "iPhone 5s", 35 | "udid" : "E17597CE-71EE-4402-8B1C-1B526446A3A2", 36 | "availabilityError" : "" 37 | }, 38 | { 39 | "availability" : "(available)", 40 | "state" : "Shutdown", 41 | "isAvailable" : true, 42 | "name" : "iPhone 6", 43 | "udid" : "1C7AB8B9-94C3-4806-86D7-77C13B483902", 44 | "availabilityError" : "" 45 | }, 46 | { 47 | "availability" : "(available)", 48 | "state" : "Shutdown", 49 | "isAvailable" : true, 50 | "name" : "iPhone 6 Plus", 51 | "udid" : "EE76EA77-E975-4198-9859-69DFF74252D2", 52 | "availabilityError" : "" 53 | }, 54 | { 55 | "availability" : "(available)", 56 | "state" : "Shutdown", 57 | "isAvailable" : true, 58 | "name" : "iPad Air", 59 | "udid" : "225C4FDB-5132-423A-9CFF-89D4474395F9", 60 | "availabilityError" : "" 61 | }, 62 | { 63 | "availability" : "(available)", 64 | "state" : "Shutdown", 65 | "isAvailable" : true, 66 | "name" : "iPad Air 2", 67 | "udid" : "DDDF281C-F912-471C-B30D-994A2644DF03", 68 | "availabilityError" : "" 69 | }, 70 | { 71 | "availability" : "(available)", 72 | "state" : "Shutdown", 73 | "isAvailable" : true, 74 | "name" : "iPad Pro (12.9-inch)", 75 | "udid" : "1D953CFF-1FBC-413E-B2AB-B4BA4DD5EEC2", 76 | "availabilityError" : "" 77 | }, 78 | { 79 | "availability" : "(available)", 80 | "state" : "Shutdown", 81 | "isAvailable" : true, 82 | "name" : "iPad Pro (12.9-inch) (2nd generation)", 83 | "udid" : "A7D1A0D7-D67B-409F-A73D-0DB53EDD860F", 84 | "availabilityError" : "" 85 | } 86 | ], 87 | "com.apple.CoreSimulator.SimRuntime.tvOS-12-2" : [ 88 | { 89 | "availability" : "(unavailable, runtime profile not found)", 90 | "state" : "Shutdown", 91 | "isAvailable" : false, 92 | "name" : "Apple TV", 93 | "udid" : "84793AD4-E1C9-49EE-B85A-0DCBB0806279", 94 | "availabilityError" : "runtime profile not found" 95 | }, 96 | { 97 | "availability" : "(unavailable, runtime profile not found)", 98 | "state" : "Shutdown", 99 | "isAvailable" : false, 100 | "name" : "Apple TV 4K", 101 | "udid" : "B3ABE34D-7F23-4107-B381-3DD0184134D1", 102 | "availabilityError" : "runtime profile not found" 103 | }, 104 | { 105 | "availability" : "(unavailable, runtime profile not found)", 106 | "state" : "Shutdown", 107 | "isAvailable" : false, 108 | "name" : "Apple TV 4K (at 1080p)", 109 | "udid" : "289113A3-52A3-4904-B7B6-E66DF666EDB5", 110 | "availabilityError" : "runtime profile not found" 111 | } 112 | ], 113 | "com.apple.CoreSimulator.SimRuntime.watchOS-5-2" : [ 114 | { 115 | "availability" : "(unavailable, runtime profile not found)", 116 | "state" : "Shutdown", 117 | "isAvailable" : false, 118 | "name" : "Apple Watch Series 2 - 38mm", 119 | "udid" : "76D3F91D-5C60-4B9B-A30A-A01A63C6FE73", 120 | "availabilityError" : "runtime profile not found" 121 | }, 122 | { 123 | "availability" : "(unavailable, runtime profile not found)", 124 | "state" : "Shutdown", 125 | "isAvailable" : false, 126 | "name" : "Apple Watch Series 2 - 42mm", 127 | "udid" : "26B36BEB-A564-4321-A108-72DEEB2D63F9", 128 | "availabilityError" : "runtime profile not found" 129 | }, 130 | { 131 | "availability" : "(unavailable, runtime profile not found)", 132 | "state" : "Shutdown", 133 | "isAvailable" : false, 134 | "name" : "Apple Watch Series 3 - 38mm", 135 | "udid" : "63D10205-08E3-4A8C-9334-DB0868D87554", 136 | "availabilityError" : "runtime profile not found" 137 | }, 138 | { 139 | "availability" : "(unavailable, runtime profile not found)", 140 | "state" : "Shutdown", 141 | "isAvailable" : false, 142 | "name" : "Apple Watch Series 3 - 42mm", 143 | "udid" : "420CD468-9CED-44F7-A8AE-F1FABB18BFB2", 144 | "availabilityError" : "runtime profile not found" 145 | }, 146 | { 147 | "availability" : "(unavailable, runtime profile not found)", 148 | "state" : "Shutdown", 149 | "isAvailable" : false, 150 | "name" : "Apple Watch Series 4 - 40mm", 151 | "udid" : "6E507921-8AE9-4562-93C6-52DFDA87EA40", 152 | "availabilityError" : "runtime profile not found" 153 | }, 154 | { 155 | "availability" : "(unavailable, runtime profile not found)", 156 | "state" : "Shutdown", 157 | "isAvailable" : false, 158 | "name" : "Apple Watch Series 4 - 44mm", 159 | "udid" : "E5DDFDFA-693A-46A7-B42A-866681E15B83", 160 | "availabilityError" : "runtime profile not found" 161 | } 162 | ], 163 | "com.apple.CoreSimulator.SimRuntime.watchOS-5-1" : [ 164 | { 165 | "availability" : "(available)", 166 | "state" : "Shutdown", 167 | "isAvailable" : true, 168 | "name" : "Apple Watch Series 2 - 38mm", 169 | "udid" : "F40113CC-2973-4EBD-87F4-31852A6FF09B", 170 | "availabilityError" : "" 171 | }, 172 | { 173 | "availability" : "(available)", 174 | "state" : "Shutdown", 175 | "isAvailable" : true, 176 | "name" : "Apple Watch Series 2 - 42mm", 177 | "udid" : "5A83C524-FFF6-45AA-936C-365D2F1126F3", 178 | "availabilityError" : "" 179 | }, 180 | { 181 | "availability" : "(available)", 182 | "state" : "Shutdown", 183 | "isAvailable" : true, 184 | "name" : "Apple Watch Series 3 - 38mm", 185 | "udid" : "C76C585D-96D1-4037-80ED-C73DBBDB3521", 186 | "availabilityError" : "" 187 | }, 188 | { 189 | "availability" : "(available)", 190 | "state" : "Shutdown", 191 | "isAvailable" : true, 192 | "name" : "Apple Watch Series 3 - 42mm", 193 | "udid" : "DA5E98A8-2D04-474F-A0C3-9A0234F44CC9", 194 | "availabilityError" : "" 195 | }, 196 | { 197 | "availability" : "(available)", 198 | "state" : "Shutdown", 199 | "isAvailable" : true, 200 | "name" : "Apple Watch Series 4 - 40mm", 201 | "udid" : "DCD321B8-F265-4213-B248-C89AB8B806E1", 202 | "availabilityError" : "" 203 | }, 204 | { 205 | "availability" : "(available)", 206 | "state" : "Shutdown", 207 | "isAvailable" : true, 208 | "name" : "Apple Watch Series 4 - 44mm", 209 | "udid" : "EADBBE94-E97C-4D13-9FF2-44A3524113C7", 210 | "availabilityError" : "" 211 | } 212 | ], 213 | "com.apple.CoreSimulator.SimRuntime.iOS-12-2" : [ 214 | { 215 | "availability" : "(unavailable, runtime profile not found)", 216 | "state" : "Shutdown", 217 | "isAvailable" : false, 218 | "name" : "iPhone 5s", 219 | "udid" : "DBC918BB-D519-408D-A5E7-EAD66D875A40", 220 | "availabilityError" : "runtime profile not found" 221 | }, 222 | { 223 | "availability" : "(unavailable, runtime profile not found)", 224 | "state" : "Shutdown", 225 | "isAvailable" : false, 226 | "name" : "iPhone 6", 227 | "udid" : "5CC1A69E-75B0-4109-8474-61C605C61493", 228 | "availabilityError" : "runtime profile not found" 229 | }, 230 | { 231 | "availability" : "(unavailable, runtime profile not found)", 232 | "state" : "Shutdown", 233 | "isAvailable" : false, 234 | "name" : "iPhone 6 Plus", 235 | "udid" : "99FE1BB4-66F4-47CA-A144-9198A5CBB9B6", 236 | "availabilityError" : "runtime profile not found" 237 | }, 238 | { 239 | "availability" : "(unavailable, runtime profile not found)", 240 | "state" : "Shutdown", 241 | "isAvailable" : false, 242 | "name" : "iPad Air", 243 | "udid" : "F78CD721-6DB1-4C19-9601-FEB84D2D0A12", 244 | "availabilityError" : "runtime profile not found" 245 | }, 246 | { 247 | "availability" : "(unavailable, runtime profile not found)", 248 | "state" : "Shutdown", 249 | "isAvailable" : false, 250 | "name" : "iPad Air 2", 251 | "udid" : "7E3B9E66-5995-4342-929C-43CCFF4E0819", 252 | "availabilityError" : "runtime profile not found" 253 | }, 254 | { 255 | "availability" : "(unavailable, runtime profile not found)", 256 | "state" : "Shutdown", 257 | "isAvailable" : false, 258 | "name" : "iPad Pro (12.9-inch)", 259 | "udid" : "D5483905-8384-4F9A-B337-AE5B7F6B6994", 260 | "availabilityError" : "runtime profile not found" 261 | }, 262 | { 263 | "availability" : "(unavailable, runtime profile not found)", 264 | "state" : "Shutdown", 265 | "isAvailable" : false, 266 | "name" : "iPad Pro (12.9-inch) (2nd generation)", 267 | "udid" : "DDC7CD7C-8E12-42AF-B39A-DB847CCD84FD", 268 | "availabilityError" : "runtime profile not found" 269 | } 270 | ] 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /test/unit/fixtures/devices.json: -------------------------------------------------------------------------------- 1 | { 2 | "devices" : { 3 | "com.apple.CoreSimulator.SimRuntime.tvOS-12-1" : [ 4 | { 5 | "availability" : "(available)", 6 | "state" : "Shutdown", 7 | "isAvailable" : true, 8 | "name" : "Apple TV", 9 | "udid" : "FA628127-1D5C-45C3-9918-A47BF7E2AE14", 10 | "availabilityError" : "" 11 | }, 12 | { 13 | "availability" : "(available)", 14 | "state" : "Shutdown", 15 | "isAvailable" : true, 16 | "name" : "Apple TV 4K", 17 | "udid" : "2EF493BE-E7D5-45E7-9725-8D2706F7220E", 18 | "availabilityError" : "" 19 | }, 20 | { 21 | "availability" : "(available)", 22 | "state" : "Shutdown", 23 | "isAvailable" : true, 24 | "name" : "Apple TV 4K (at 1080p)", 25 | "udid" : "FFECD143-B523-4A3D-BA27-1F9706F814CB", 26 | "availabilityError" : "" 27 | } 28 | ], 29 | "com.apple.CoreSimulator.SimRuntime.iOS-12-1" : [ 30 | { 31 | "availability" : "(available)", 32 | "state" : "Shutdown", 33 | "isAvailable" : true, 34 | "name" : "iPhone 5s", 35 | "udid" : "E17597CE-71EE-4402-8B1C-1B526446A3A2", 36 | "availabilityError" : "" 37 | }, 38 | { 39 | "availability" : "(available)", 40 | "state" : "Shutdown", 41 | "isAvailable" : true, 42 | "name" : "iPhone 6", 43 | "udid" : "1C7AB8B9-94C3-4806-86D7-77C13B483902", 44 | "availabilityError" : "" 45 | }, 46 | { 47 | "availability" : "(available)", 48 | "state" : "Shutdown", 49 | "isAvailable" : true, 50 | "name" : "iPhone 6 Plus", 51 | "udid" : "EE76EA77-E975-4198-9859-69DFF74252D2", 52 | "availabilityError" : "" 53 | }, 54 | { 55 | "availability" : "(available)", 56 | "state" : "Shutdown", 57 | "isAvailable" : true, 58 | "name" : "iPad Air", 59 | "udid" : "225C4FDB-5132-423A-9CFF-89D4474395F9", 60 | "availabilityError" : "" 61 | }, 62 | { 63 | "availability" : "(available)", 64 | "state" : "Shutdown", 65 | "isAvailable" : true, 66 | "name" : "iPad Air 2", 67 | "udid" : "DDDF281C-F912-471C-B30D-994A2644DF03", 68 | "availabilityError" : "" 69 | }, 70 | { 71 | "availability" : "(available)", 72 | "state" : "Shutdown", 73 | "isAvailable" : true, 74 | "name" : "iPad Pro (12.9-inch)", 75 | "udid" : "1D953CFF-1FBC-413E-B2AB-B4BA4DD5EEC2", 76 | "availabilityError" : "" 77 | }, 78 | { 79 | "availability" : "(available)", 80 | "state" : "Shutdown", 81 | "isAvailable" : true, 82 | "name" : "iPad Pro (12.9-inch) (2nd generation)", 83 | "udid" : "A7D1A0D7-D67B-409F-A73D-0DB53EDD860F", 84 | "availabilityError" : "" 85 | } 86 | ], 87 | "com.apple.CoreSimulator.SimRuntime.watchOS-5-1" : [ 88 | { 89 | "availability" : "(available)", 90 | "state" : "Shutdown", 91 | "isAvailable" : true, 92 | "name" : "Apple Watch Series 2 - 38mm", 93 | "udid" : "F40113CC-2973-4EBD-87F4-31852A6FF09B", 94 | "availabilityError" : "" 95 | }, 96 | { 97 | "availability" : "(available)", 98 | "state" : "Shutdown", 99 | "isAvailable" : true, 100 | "name" : "Apple Watch Series 2 - 42mm", 101 | "udid" : "5A83C524-FFF6-45AA-936C-365D2F1126F3", 102 | "availabilityError" : "" 103 | }, 104 | { 105 | "availability" : "(available)", 106 | "state" : "Shutdown", 107 | "isAvailable" : true, 108 | "name" : "Apple Watch Series 3 - 38mm", 109 | "udid" : "C76C585D-96D1-4037-80ED-C73DBBDB3521", 110 | "availabilityError" : "" 111 | }, 112 | { 113 | "availability" : "(available)", 114 | "state" : "Shutdown", 115 | "isAvailable" : true, 116 | "name" : "Apple Watch Series 3 - 42mm", 117 | "udid" : "DA5E98A8-2D04-474F-A0C3-9A0234F44CC9", 118 | "availabilityError" : "" 119 | }, 120 | { 121 | "availability" : "(available)", 122 | "state" : "Shutdown", 123 | "isAvailable" : true, 124 | "name" : "Apple Watch Series 4 - 40mm", 125 | "udid" : "DCD321B8-F265-4213-B248-C89AB8B806E1", 126 | "availabilityError" : "" 127 | }, 128 | { 129 | "availability" : "(available)", 130 | "state" : "Shutdown", 131 | "isAvailable" : true, 132 | "name" : "Apple Watch Series 4 - 44mm", 133 | "udid" : "EADBBE94-E97C-4D13-9FF2-44A3524113C7", 134 | "availabilityError" : "" 135 | } 136 | ] 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /test/unit/simctl-specs.cjs: -------------------------------------------------------------------------------- 1 | import pq from 'proxyquire'; 2 | import sinon from 'sinon'; 3 | import * as TeenProcess from 'teen_process'; 4 | import _ from 'lodash'; 5 | import fs from 'node:fs'; 6 | 7 | const proxyquire = pq.noCallThru(); 8 | 9 | const devicePayloads = [ 10 | [ 11 | { 12 | stdout: fs.readFileSync(`${__dirname}/fixtures/devices.json`, 'utf-8'), 13 | }, 14 | { 15 | stdout: fs.readFileSync(`${__dirname}/fixtures/devices-with-unavailable.json`, 'utf-8'), 16 | }, 17 | ], 18 | [ 19 | { 20 | stdout: fs.readFileSync(`${__dirname}/fixtures/devices-simple.json`, 'utf-8'), 21 | }, 22 | { 23 | stdout: fs.readFileSync(`${__dirname}/fixtures/devices-with-unavailable-simple.json`, 'utf-8'), 24 | }, 25 | ], 26 | ]; 27 | 28 | describe('simctl', function () { 29 | let chai; 30 | let chaiAsPromised; 31 | 32 | const execStub = sinon.stub(TeenProcess, 'exec'); 33 | function stubSimctl (xcrun = {}) { 34 | const xcrunPath = process.env.XCRUN_BINARY || xcrun.path; 35 | const { Simctl } = proxyquire('../../lib/simctl', { 36 | 'which': sinon.stub().withArgs(xcrunPath).resolves(xcrunPath) 37 | }); 38 | 39 | return new Simctl({ xcrun }); 40 | } 41 | 42 | before(async function() { 43 | chai = await import('chai'); 44 | chaiAsPromised = await import('chai-as-promised'); 45 | 46 | chai.should(); 47 | chai.use(chaiAsPromised.default); 48 | }); 49 | 50 | 51 | describe('getDevices', function () { 52 | let simctl; 53 | 54 | beforeEach(function () { 55 | simctl = stubSimctl({ path: 'xcrun' }); 56 | }); 57 | afterEach(function () { 58 | execStub.resetHistory(); 59 | }); 60 | after(function () { 61 | execStub.reset(); 62 | }); 63 | 64 | for (const [devicesPayload, devicesWithUnavailablePayload] of devicePayloads) { 65 | describe('no forSdk defined', function () { 66 | describe('no platform defined', function () { 67 | it('should get all the devices in the JSON', async function () { 68 | execStub.returns(devicesPayload); 69 | 70 | const devices = await simctl.getDevices(); 71 | _.keys(devices).length.should.eql(2); 72 | 73 | devices['12.1'].length.should.eql(10); 74 | devices['5.1'].length.should.eql(6); 75 | }); 76 | it('should ignore unavailable devices', async function () { 77 | execStub.returns(devicesWithUnavailablePayload); 78 | 79 | const devices = await simctl.getDevices(); 80 | _.keys(devices).length.should.eql(4); 81 | 82 | devices['12.1'].length.should.eql(10); 83 | devices['5.1'].length.should.eql(6); 84 | devices['12.2'].length.should.eql(0); 85 | devices['5.2'].length.should.eql(0); 86 | }); 87 | }); 88 | describe('platform defined', function () { 89 | it('should get all the devices in the JSON', async function () { 90 | execStub.returns(devicesPayload); 91 | 92 | const devices = await simctl.getDevices(null, 'tvOS'); 93 | _.keys(devices).length.should.eql(1); 94 | 95 | devices['12.1'].length.should.eql(3); 96 | }); 97 | it('should ignore unavailable devices', async function () { 98 | execStub.returns(devicesWithUnavailablePayload); 99 | 100 | const devices = await simctl.getDevices(null, 'tvOS'); 101 | _.keys(devices).length.should.eql(2); 102 | 103 | devices['12.1'].length.should.eql(3); 104 | devices['12.2'].length.should.eql(0); 105 | }); 106 | }); 107 | }); 108 | 109 | describe('forSdk defined', function () { 110 | describe('no platform defined', function () { 111 | it('should get all the devices in the JSON', async function () { 112 | execStub.returns(devicesPayload); 113 | 114 | const devices = await simctl.getDevices('12.1'); 115 | _.keys(devices).length.should.eql(10); 116 | }); 117 | it('should ignore unavailable devices', async function () { 118 | execStub.returns(devicesWithUnavailablePayload); 119 | 120 | const devices = await simctl.getDevices('12.1'); 121 | _.keys(devices).length.should.eql(10); 122 | }); 123 | }); 124 | describe('platform defined', function () { 125 | it('should get all the devices in the JSON', async function () { 126 | execStub.returns(devicesPayload); 127 | 128 | const devices = await simctl.getDevices('5.1', 'watchOS'); 129 | _.keys(devices).length.should.eql(6); 130 | }); 131 | it('should ignore unavailable devices', async function () { 132 | execStub.returns(devicesWithUnavailablePayload); 133 | 134 | const devices = await simctl.getDevices('5.1', 'watchOS'); 135 | _.keys(devices).length.should.eql(6); 136 | }); 137 | }); 138 | }); 139 | } 140 | }); 141 | 142 | describe('#createDevice', function () { 143 | const devicesPayload = devicePayloads[0][0]; 144 | let simctl; 145 | 146 | beforeEach(function () { 147 | simctl = stubSimctl({ path: 'xcrun' }); 148 | }); 149 | afterEach(function () { 150 | execStub.resetHistory(); 151 | delete process.env.XCRUN_BINARY; 152 | }); 153 | after(function () { 154 | execStub.reset(); 155 | }); 156 | 157 | it('should create iOS simulator using xcrun path from env', async function () { 158 | process.env.XCRUN_BINARY = 'some/path'; 159 | simctl = stubSimctl({ path: undefined }); 160 | execStub.onCall(0).returns({stdout: 'not json'}) 161 | .onCall(1).returns({stdout: '12.1.1', stderr: ''}) 162 | .onCall(2).returns({stdout: 'EE76EA77-E975-4198-9859-69DFF74252D2', stderr: ''}) 163 | .onCall(3).returns(devicesPayload); 164 | 165 | const devices = await simctl.createDevice( 166 | 'name', 167 | 'iPhone 6 Plus', 168 | '12.1.1', 169 | { timeout: 20000 } 170 | ); 171 | execStub.getCall(2).args[1].should.eql([ 172 | 'simctl', 'create', 'name', 'iPhone 6 Plus', 'com.apple.CoreSimulator.SimRuntime.iOS-12-1' 173 | ]); 174 | execStub.getCall(0).args[0].should.eql('some/path'); 175 | devices.should.eql('EE76EA77-E975-4198-9859-69DFF74252D2'); 176 | }); 177 | 178 | it('should create iOS simulator using xcrun path from passed opts', async function () { 179 | process.env.XCRUN_BINARY = 'some/path'; 180 | simctl = stubSimctl({ path: 'other/path' }); 181 | execStub.onCall(0).returns({stdout: 'not json'}) 182 | .onCall(1).returns({stdout: '12.1.1', stderr: ''}) 183 | .onCall(2).returns({stdout: 'EE76EA77-E975-4198-9859-69DFF74252D2', stderr: ''}) 184 | .onCall(3).returns(devicesPayload); 185 | 186 | const devices = await simctl.createDevice( 187 | 'name', 188 | 'iPhone 6 Plus', 189 | '12.1.1', 190 | { timeout: 20000 } 191 | ); 192 | execStub.getCall(2).args[1].should.eql([ 193 | 'simctl', 'create', 'name', 'iPhone 6 Plus', 'com.apple.CoreSimulator.SimRuntime.iOS-12-1' 194 | ]); 195 | execStub.getCall(0).args[0].should.eql('other/path'); 196 | devices.should.eql('EE76EA77-E975-4198-9859-69DFF74252D2'); 197 | }); 198 | 199 | it('should create iOS simulator and use xcrun simctl "json" parsing', async function () { 200 | const runtimesJson = `{ 201 | "runtimes" : [ 202 | { 203 | "buildversion" : "15B87", 204 | "availability" : "(available)", 205 | "name" : "iOS 12.1.1", 206 | "identifier" : "com.apple.CoreSimulator.SimRuntime.iOS-12-1-1", 207 | "version" : "12.1.1" 208 | }, 209 | { 210 | "buildversion" : "15J580", 211 | "availability" : "(available)", 212 | "name" : "tvOS 11.1", 213 | "identifier" : "com.apple.CoreSimulator.SimRuntime.tvOS-11-1", 214 | "version" : "11.1" 215 | }, 216 | { 217 | "buildversion" : "15R844", 218 | "availability" : "(available)", 219 | "name" : "watchOS 4.1", 220 | "identifier" : "com.apple.CoreSimulator.SimRuntime.watchOS-4-1", 221 | "version" : "4.1" 222 | } 223 | ] 224 | }`; 225 | execStub.onCall(0).returns({stdout: runtimesJson}) 226 | .onCall(1).returns({stdout: 'FA628127-1D5C-45C3-9918-A47BF7E2AE14', stderr: ''}) 227 | .onCall(2).returns(devicesPayload); 228 | 229 | const devices = await simctl.createDevice( 230 | 'name', 231 | 'iPhone 6 Plus', 232 | '12.1.1', 233 | { timeout: 20000 } 234 | ); 235 | execStub.getCall(1).args[1].should.eql([ 236 | 'simctl', 'create', 'name', 'iPhone 6 Plus', 'com.apple.CoreSimulator.SimRuntime.iOS-12-1-1' 237 | ]); 238 | devices.should.eql('FA628127-1D5C-45C3-9918-A47BF7E2AE14'); 239 | }); 240 | 241 | it('should create tvOS simulator', async function () { 242 | execStub.onCall(0).returns({stdout: 'invalid json'}) 243 | .onCall(1).returns({stdout: 'com.apple.CoreSimulator.SimRuntime.tvOS-12-1', stderr: ''}) 244 | .onCall(2).returns({stdout: 'FA628127-1D5C-45C3-9918-A47BF7E2AE14', stderr: ''}) 245 | .onCall(3).returns(devicesPayload); 246 | 247 | const devices = await simctl.createDevice( 248 | 'name', 249 | 'Apple TV', 250 | '12.1', 251 | { timeout: 20000, platform: 'tvOS' } 252 | ); 253 | execStub.getCall(2).args[1].should.eql([ 254 | 'simctl', 'create', 'name', 'Apple TV', 'com.apple.CoreSimulator.SimRuntime.tvOS-12-1' 255 | ]); 256 | devices.should.eql('FA628127-1D5C-45C3-9918-A47BF7E2AE14'); 257 | }); 258 | 259 | it('should create iOS simulator with old runtime format', async function () { 260 | execStub.onCall(0).returns({stdout: 'invalid json'}) 261 | .onCall(1).returns({stdout: '12.1', stderr: ''}) 262 | .onCall(2).throws('Invalid runtime: com.apple.CoreSimulator.SimRuntime.iOS-12-1') 263 | .onCall(3).returns({stdout: 'EE76EA77-E975-4198-9859-69DFF74252D2', stderr: ''}) 264 | .onCall(4).returns(devicesPayload); 265 | 266 | const devices = await simctl.createDevice( 267 | 'name', 268 | 'iPhone 6 Plus', 269 | '12.1', 270 | { timeout: 20000 } 271 | ); 272 | execStub.getCall(3).args[1].should.eql([ 273 | 'simctl', 'create', 'name', 'iPhone 6 Plus', '12.1' 274 | ]); 275 | devices.should.eql('EE76EA77-E975-4198-9859-69DFF74252D2'); 276 | }); 277 | 278 | it('should create iOS simulator with old runtime format and three-part platform version', async function () { 279 | execStub.onCall(0).returns({stdout: 'invalid json'}) 280 | .onCall(1).returns({stdout: '12.1.1', stderr: ''}) 281 | .onCall(2).throws('Invalid runtime: com.apple.CoreSimulator.SimRuntime.iOS-12-1') 282 | .onCall(3).returns({stdout: 'EE76EA77-E975-4198-9859-69DFF74252D2', stderr: ''}) 283 | .onCall(4).returns(devicesPayload); 284 | 285 | const devices = await simctl.createDevice( 286 | 'name', 287 | 'iPhone 6 Plus', 288 | '12.1', 289 | { timeout: 20000 } 290 | ); 291 | execStub.getCall(3).args[1].should.eql([ 292 | 'simctl', 'create', 'name', 'iPhone 6 Plus', '12.1' 293 | ]); 294 | devices.should.eql('EE76EA77-E975-4198-9859-69DFF74252D2'); 295 | }); 296 | 297 | it('should create iOS simulator with three-part platform version and three-part runtime', async function () { 298 | execStub.onCall(0).returns({stdout: 'invalid json'}) 299 | .onCall(1).returns({stdout: '12.1.1', stderr: ''}) 300 | .onCall(2).throws('Invalid runtime: com.apple.CoreSimulator.SimRuntime.iOS-12-1') 301 | .onCall(3).returns({stdout: 'EE76EA77-E975-4198-9859-69DFF74252D2', stderr: ''}) 302 | .onCall(4).returns(devicesPayload); 303 | 304 | const devices = await simctl.createDevice( 305 | 'name', 306 | 'iPhone 6 Plus', 307 | '12.1.1', 308 | { timeout: 20000 } 309 | ); 310 | execStub.getCall(3).args[1].should.eql([ 311 | 'simctl', 'create', 'name', 'iPhone 6 Plus', 'com.apple.CoreSimulator.SimRuntime.iOS-12-1-1' 312 | ]); 313 | devices.should.eql('EE76EA77-E975-4198-9859-69DFF74252D2'); 314 | }); 315 | }); 316 | }); 317 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "@appium/tsconfig/tsconfig.json", 4 | "compilerOptions": { 5 | "strict": false, 6 | "esModuleInterop": true, 7 | "outDir": "build", 8 | "types": ["node"], 9 | "checkJs": true 10 | }, 11 | "include": [ 12 | "index.js", 13 | "lib" 14 | ] 15 | } 16 | --------------------------------------------------------------------------------