├── .github └── workflows │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── README.md ├── build.config.ts ├── commands.md ├── docs ├── .vitepress │ ├── cache │ │ └── deps │ │ │ ├── @theme_index.js │ │ │ ├── @theme_index.js.map │ │ │ ├── _metadata.json │ │ │ ├── chunk-JQJWEF5N.js │ │ │ ├── chunk-JQJWEF5N.js.map │ │ │ ├── chunk-VNFKW3Y4.js │ │ │ ├── chunk-VNFKW3Y4.js.map │ │ │ ├── package.json │ │ │ ├── vitepress___@vue_devtools-api.js │ │ │ ├── vitepress___@vue_devtools-api.js.map │ │ │ ├── vitepress___@vueuse_core.js │ │ │ ├── vitepress___@vueuse_core.js.map │ │ │ ├── vue.js │ │ │ └── vue.js.map │ └── config.mts ├── commands.md ├── getting-started.md ├── index.md ├── package.json ├── playground.md ├── pnpm-lock.yaml └── public │ └── maests.png ├── fixtures ├── sample-flow.ts └── utils │ ├── hello.ts │ ├── nest-script.ts │ ├── openApp.ts │ ├── script.ts │ └── type.ts ├── package.json ├── patches └── mkdist.patch ├── playground ├── .env ├── .gitignore ├── .npmrc ├── README.md ├── app.config.ts ├── app │ ├── _layout.tsx │ └── index.tsx ├── assets │ ├── fonts │ │ └── SpaceMono-Regular.ttf │ └── images │ │ ├── adaptive-icon.png │ │ ├── favicon.png │ │ ├── icon.png │ │ ├── partial-react-logo.png │ │ ├── react-logo.png │ │ ├── react-logo@2x.png │ │ ├── react-logo@3x.png │ │ └── splash.png ├── babel.config.js ├── credentials.json ├── debug.keystore ├── e2e │ ├── sample-flow.ts │ └── utils │ │ ├── hello.ts │ │ ├── nest-script.ts │ │ ├── openApp.ts │ │ ├── script.ts │ │ └── type.ts ├── package.json ├── pnpm-lock.yaml ├── scripts │ └── android.ts └── tsconfig.json ├── pnpm-lock.yaml ├── prettier.config.cjs ├── src ├── commands │ ├── addMedia.ts │ ├── assert.ts │ ├── commands.ts │ ├── init.ts │ ├── repeat.ts │ ├── run.ts │ ├── swipe.ts │ ├── tap.ts │ ├── text.ts │ ├── type.ts │ └── wait.ts ├── main.ts ├── out.ts ├── rewrite-code.ts ├── utils.ts └── write-yaml.ts ├── tsconfig.json └── vitest.config.ts /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run Unit Tests 2 | 3 | # Run this workflow on pull requests 4 | on: 5 | push: 6 | branches: 7 | - "main" 8 | pull_request: 9 | branches: 10 | - "**" 11 | workflow_dispatch: 12 | 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: pnpm/action-setup@v4 20 | - run: pnpm install && pnpm build && npx vitest 21 | 22 | android-test: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: actions/setup-node@v4 27 | - run: npm install -g pnpm 28 | - run: pnpm install && pnpm build && cd playground && pnpm install 29 | - uses: actions/setup-java@v4 30 | with: 31 | distribution: "oracle" 32 | java-version: 17 33 | 34 | - name: Enable KVM 35 | run: | 36 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules 37 | sudo udevadm control --reload-rules 38 | sudo udevadm trigger --name-match=kvm 39 | 40 | - run: curl -fsSL "https://get.maestro.mobile.dev" | bash 41 | 42 | - name: add path 43 | run: echo "$HOME/.maestro/bin" >> $GITHUB_PATH 44 | 45 | - name: run tests 46 | uses: reactivecircus/android-emulator-runner@62dbb605bba737720e10b196cb4220d374026a6d 47 | with: 48 | api-level: 33 49 | emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none 50 | disable-animations: true 51 | target: google_apis 52 | arch: x86_64 53 | emulator-port: 5584 54 | profile: Nexus 6 55 | force-avd-creation: false 56 | working-directory: ./playground 57 | script: npx tsx scripts/android.ts 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dev 3 | .DS_Store 4 | dist 5 | .vscode 6 | .idea 7 | maests 8 | clean.sh -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v2.8.6 2 | 3 | [compare changes](https://github.com/shoma-mano/maests/compare/v2.8.5...v2.8.6) 4 | 5 | ### 🩹 Fixes 6 | 7 | - EsmResolve in rewriteRunScriptPlugin throw error ([a89b3ad](https://github.com/shoma-mano/maests/commit/a89b3ad)) 8 | 9 | ### 💅 Refactors 10 | 11 | - Refactored utils ([2669227](https://github.com/shoma-mano/maests/commit/2669227)) 12 | 13 | ### 🏡 Chore 14 | 15 | - **test:** Fix failed unit tests ([30e399c](https://github.com/shoma-mano/maests/commit/30e399c)) 16 | 17 | ### ❤️ Contributors 18 | 19 | - Shoma-mano 20 | 21 | ## v2.8.5 22 | 23 | [compare changes](https://github.com/shoma-mano/maests/compare/v2.8.4...v2.8.5) 24 | 25 | ### 🩹 Fixes 26 | 27 | - Fix bug that happens when runScript is used in imported flow ([06f7aaf](https://github.com/shoma-mano/maests/commit/06f7aaf)) 28 | 29 | ### ❤️ Contributors 30 | 31 | - Shoma-mano 32 | 33 | ## v2.8.4 34 | 35 | [compare changes](https://github.com/shoma-mano/maests/compare/v2.8.3...v2.8.4) 36 | 37 | ### 🩹 Fixes 38 | 39 | - **runScript:** Regex for rewriting env ([7c3ce2f](https://github.com/shoma-mano/maests/commit/7c3ce2f)) 40 | 41 | ### ❤️ Contributors 42 | 43 | - Shoma-mano 44 | 45 | ## v2.8.3 46 | 47 | [compare changes](https://github.com/shoma-mano/maests/compare/v2.8.2...v2.8.3) 48 | 49 | ### 🩹 Fixes 50 | 51 | - **rewrite-code:** Fix relative import path bug ([4dcecb1](https://github.com/shoma-mano/maests/commit/4dcecb1)) 52 | 53 | ### 🏡 Chore 54 | 55 | - Update lock file ([2f87714](https://github.com/shoma-mano/maests/commit/2f87714)) 56 | 57 | ### ❤️ Contributors 58 | 59 | - Shoma-mano 60 | 61 | ## v2.8.2 62 | 63 | [compare changes](https://github.com/shoma-mano/maests/compare/v2.8.1...v2.8.2) 64 | 65 | ### 🩹 Fixes 66 | 67 | - Remove @types/babel__core from dependency ([b7f5cd2](https://github.com/shoma-mano/maests/commit/b7f5cd2)) 68 | 69 | ### ❤️ Contributors 70 | 71 | - Shoma-mano 72 | 73 | ## v2.8.1 74 | 75 | [compare changes](https://github.com/shoma-mano/maests/compare/v2.8.0...v2.8.1) 76 | 77 | ### 🩹 Fixes 78 | 79 | - Remove @types/babel__core from dependency ([a584e9c](https://github.com/shoma-mano/maests/commit/a584e9c)) 80 | 81 | ### 🏡 Chore 82 | 83 | - Update .gitignore ([6cc4c21](https://github.com/shoma-mano/maests/commit/6cc4c21)) 84 | - Update README.md ([11ffe97](https://github.com/shoma-mano/maests/commit/11ffe97)) 85 | - Update README.md ([9da2331](https://github.com/shoma-mano/maests/commit/9da2331)) 86 | 87 | ### ❤️ Contributors 88 | 89 | - Shoma-mano 90 | 91 | ## v2.8.0 92 | 93 | [compare changes](https://github.com/shoma-mano/maests/compare/v2.7.3...v2.8.0) 94 | 95 | ### 🚀 Enhancements 96 | 97 | - Support maestro.platform type ([9d33fae](https://github.com/shoma-mano/maests/commit/9d33fae)) 98 | - Support pass callback to runScript directly ([eb1a664](https://github.com/shoma-mano/maests/commit/eb1a664)) 99 | 100 | ### 🏡 Chore 101 | 102 | - Add type for maestro.platform ([f35093b](https://github.com/shoma-mano/maests/commit/f35093b)) 103 | - Update readme.md ([7319b41](https://github.com/shoma-mano/maests/commit/7319b41)) 104 | 105 | ### ✅ Tests 106 | 107 | - Fix failed tests ([cf93951](https://github.com/shoma-mano/maests/commit/cf93951)) 108 | - Fix failed tests ([4dce1d0](https://github.com/shoma-mano/maests/commit/4dce1d0)) 109 | - Fix failed tests ([568db1a](https://github.com/shoma-mano/maests/commit/568db1a)) 110 | 111 | ### ❤️ Contributors 112 | 113 | - Shoma-mano 114 | 115 | ## v2.7.3 116 | 117 | [compare changes](https://github.com/shoma-mano/maests/compare/v2.7.2...v2.7.3) 118 | 119 | ### 💅 Refactors 120 | 121 | - Remove unnecessary code ([cccad95](https://github.com/shoma-mano/maests/commit/cccad95)) 122 | - Split commands.ts ([ba09d00](https://github.com/shoma-mano/maests/commit/ba09d00)) 123 | - Delete tests folder ([b4f317e](https://github.com/shoma-mano/maests/commit/b4f317e)) 124 | 125 | ### 🏡 Chore 126 | 127 | - Improve error logging ([b37c169](https://github.com/shoma-mano/maests/commit/b37c169)) 128 | 129 | ### ✅ Tests 130 | 131 | - Android-emulator-runnner ([1f140f4](https://github.com/shoma-mano/maests/commit/1f140f4)) 132 | - Android-emulator-runnner ([c91a8f2](https://github.com/shoma-mano/maests/commit/c91a8f2)) 133 | - Android-emulator-runnner ([5656949](https://github.com/shoma-mano/maests/commit/5656949)) 134 | - Android-emulator-runnner ([7172df1](https://github.com/shoma-mano/maests/commit/7172df1)) 135 | - Android-emulator-runnner ([be488f1](https://github.com/shoma-mano/maests/commit/be488f1)) 136 | - Android-emulator-runnner ([e6b4094](https://github.com/shoma-mano/maests/commit/e6b4094)) 137 | - Android-emulator-runnner ([a88ef32](https://github.com/shoma-mano/maests/commit/a88ef32)) 138 | - Android-emulator-runnner ([3253a2b](https://github.com/shoma-mano/maests/commit/3253a2b)) 139 | - Android-emulator-runnner ([a82bda1](https://github.com/shoma-mano/maests/commit/a82bda1)) 140 | - Android-emulator-runnner ([acb9776](https://github.com/shoma-mano/maests/commit/acb9776)) 141 | - Android-emulator-runnner ([b7273e9](https://github.com/shoma-mano/maests/commit/b7273e9)) 142 | - Android-emulator-runnner ([aac76cc](https://github.com/shoma-mano/maests/commit/aac76cc)) 143 | - Android-emulator-runnner ([e1ff2cc](https://github.com/shoma-mano/maests/commit/e1ff2cc)) 144 | - Android-emulator-runnner ([e88ec05](https://github.com/shoma-mano/maests/commit/e88ec05)) 145 | - Android-emulator-runnner ([6401103](https://github.com/shoma-mano/maests/commit/6401103)) 146 | - Update e2e test example ([bcd935a](https://github.com/shoma-mano/maests/commit/bcd935a)) 147 | - Update e2e test example ([a3d3811](https://github.com/shoma-mano/maests/commit/a3d3811)) 148 | - Update e2e test example ([0ebe3f3](https://github.com/shoma-mano/maests/commit/0ebe3f3)) 149 | - Update e2e test example ([5c03713](https://github.com/shoma-mano/maests/commit/5c03713)) 150 | - Update e2e test example ([52b2e4e](https://github.com/shoma-mano/maests/commit/52b2e4e)) 151 | - Update e2e test example ([0cfc709](https://github.com/shoma-mano/maests/commit/0cfc709)) 152 | 153 | ### ❤️ Contributors 154 | 155 | - Shoma-mano 156 | 157 | ## v2.7.2 158 | 159 | [compare changes](https://github.com/shoma-mano/maests/compare/v2.7.1...v2.7.2) 160 | 161 | ### 🩹 Fixes 162 | 163 | - Broken type ([3658a92](https://github.com/shoma-mano/maests/commit/3658a92)) 164 | 165 | ### 🏡 Chore 166 | 167 | - Update pnpm-lock.yaml ([a6c57f9](https://github.com/shoma-mano/maests/commit/a6c57f9)) 168 | 169 | ### ❤️ Contributors 170 | 171 | - Shoma-mano 172 | 173 | ## v2.7.1 174 | 175 | [compare changes](https://github.com/shoma-mano/maests/compare/v2.7.0...v2.7.1) 176 | 177 | ### 🩹 Fixes 178 | 179 | - Deploy error ([4a01a6f](https://github.com/shoma-mano/maests/commit/4a01a6f)) 180 | - Consola should be included in dependency ([fcefe79](https://github.com/shoma-mano/maests/commit/fcefe79)) 181 | 182 | ### ✅ Tests 183 | 184 | - Android-emulator-runnner ([53ab891](https://github.com/shoma-mano/maests/commit/53ab891)) 185 | - Android-emulator-runnner ([3a09c15](https://github.com/shoma-mano/maests/commit/3a09c15)) 186 | - Android-emulator-runnner ([bc45e1a](https://github.com/shoma-mano/maests/commit/bc45e1a)) 187 | - Android-emulator-runnner ([7caed3a](https://github.com/shoma-mano/maests/commit/7caed3a)) 188 | - Android-emulator-runnner ([4874804](https://github.com/shoma-mano/maests/commit/4874804)) 189 | - Android-emulator-runnner ([1960dfe](https://github.com/shoma-mano/maests/commit/1960dfe)) 190 | - Android-emulator-runnner ([89f28af](https://github.com/shoma-mano/maests/commit/89f28af)) 191 | - Android-emulator-runnner ([d7dc190](https://github.com/shoma-mano/maests/commit/d7dc190)) 192 | - Android-emulator-runnner ([f808d52](https://github.com/shoma-mano/maests/commit/f808d52)) 193 | - Android-emulator-runnner ([173c542](https://github.com/shoma-mano/maests/commit/173c542)) 194 | - Android-emulator-runnner ([25f809b](https://github.com/shoma-mano/maests/commit/25f809b)) 195 | - Android-emulator-runnner ([4a7a002](https://github.com/shoma-mano/maests/commit/4a7a002)) 196 | - Android-emulator-runnner ([82f03a4](https://github.com/shoma-mano/maests/commit/82f03a4)) 197 | - Android-emulator-runnner ([b309a16](https://github.com/shoma-mano/maests/commit/b309a16)) 198 | - Android-emulator-runnner ([b5f3865](https://github.com/shoma-mano/maests/commit/b5f3865)) 199 | - Android-emulator-runnner ([7a5b0bb](https://github.com/shoma-mano/maests/commit/7a5b0bb)) 200 | - Android-emulator-runnner ([65b657f](https://github.com/shoma-mano/maests/commit/65b657f)) 201 | - Android-emulator-runnner ([54bc37d](https://github.com/shoma-mano/maests/commit/54bc37d)) 202 | - Android-emulator-runnner ([3cc0a37](https://github.com/shoma-mano/maests/commit/3cc0a37)) 203 | - Android-emulator-runnner ([8a4d778](https://github.com/shoma-mano/maests/commit/8a4d778)) 204 | - Android-emulator-runnner ([a827931](https://github.com/shoma-mano/maests/commit/a827931)) 205 | - Android-emulator-runnner ([dc4c244](https://github.com/shoma-mano/maests/commit/dc4c244)) 206 | - Android-emulator-runnner ([b2ce311](https://github.com/shoma-mano/maests/commit/b2ce311)) 207 | - Android-emulator-runnner ([fe81b58](https://github.com/shoma-mano/maests/commit/fe81b58)) 208 | - Android-emulator-runnner ([be76545](https://github.com/shoma-mano/maests/commit/be76545)) 209 | - Android-emulator-runnner ([ded9ba0](https://github.com/shoma-mano/maests/commit/ded9ba0)) 210 | - Android-emulator-runnner ([fd8bea6](https://github.com/shoma-mano/maests/commit/fd8bea6)) 211 | - Android-emulator-runnner ([5d7275a](https://github.com/shoma-mano/maests/commit/5d7275a)) 212 | - Android-emulator-runnner ([bc5c674](https://github.com/shoma-mano/maests/commit/bc5c674)) 213 | - Android-emulator-runnner ([e1233a7](https://github.com/shoma-mano/maests/commit/e1233a7)) 214 | - Android-emulator-runnner ([4d06341](https://github.com/shoma-mano/maests/commit/4d06341)) 215 | - Android-emulator-runnner ([7219b3c](https://github.com/shoma-mano/maests/commit/7219b3c)) 216 | - Android-emulator-runnner ([236ee2d](https://github.com/shoma-mano/maests/commit/236ee2d)) 217 | - Android-emulator-runnner ([367ddda](https://github.com/shoma-mano/maests/commit/367ddda)) 218 | - Android-emulator-runnner ([cb44266](https://github.com/shoma-mano/maests/commit/cb44266)) 219 | - Android-emulator-runnner ([b710a16](https://github.com/shoma-mano/maests/commit/b710a16)) 220 | - Android-emulator-runnner ([c0c67cc](https://github.com/shoma-mano/maests/commit/c0c67cc)) 221 | - Android-emulator-runnner ([49f2123](https://github.com/shoma-mano/maests/commit/49f2123)) 222 | - Android-emulator-runnner ([1b377ee](https://github.com/shoma-mano/maests/commit/1b377ee)) 223 | - Android-emulator-runnner ([9c7fa48](https://github.com/shoma-mano/maests/commit/9c7fa48)) 224 | 225 | ### ❤️ Contributors 226 | 227 | - Shoma-mano 228 | 229 | ## v2.7.0 230 | 231 | [compare changes](https://github.com/shoma-mano/maests/compare/v2.6.1...v2.7.0) 232 | 233 | ### 🚀 Enhancements 234 | 235 | - AddMedia command for uploading files ([d2e0839](https://github.com/shoma-mano/maests/commit/d2e0839)) 236 | 237 | ### ❤️ Contributors 238 | 239 | - Ian Ross Hamilton 240 | 241 | ## v2.6.1 242 | 243 | [compare changes](https://github.com/shoma-mano/maests/compare/v2.6.0...v2.6.1) 244 | 245 | ### 🩹 Fixes 246 | 247 | - Stop using evalScript for runScript ([3fee4f5](https://github.com/shoma-mano/maests/commit/3fee4f5)) 248 | 249 | ### ❤️ Contributors 250 | 251 | - Shoma-mano 252 | 253 | ## v2.6.0 254 | 255 | [compare changes](https://github.com/shoma-mano/maests/compare/v2.5.4...v2.6.0) 256 | 257 | ### 🚀 Enhancements 258 | 259 | - Support import.meta by updating jiti to v2 ([a96d496](https://github.com/shoma-mano/maests/commit/a96d496)) 260 | 261 | ### 🏡 Chore 262 | 263 | - Delete android-runnner action ([13c6e20](https://github.com/shoma-mano/maests/commit/13c6e20)) 264 | 265 | ### ❤️ Contributors 266 | 267 | - Shoma-mano 268 | 269 | ## v2.5.4 270 | 271 | [compare changes](https://github.com/shoma-mano/maests/compare/v2.5.3...v2.5.4) 272 | 273 | ### 🩹 Fixes 274 | 275 | - Incorrect command used for waitForAndTapOn ([09b6b1c](https://github.com/shoma-mano/maests/commit/09b6b1c)) 276 | 277 | ### ❤️ Contributors 278 | 279 | - Ian Ross Hamilton 280 | 281 | ## v2.5.3 282 | 283 | [compare changes](https://github.com/shoma-mano/maests/compare/v2.5.2...v2.5.3) 284 | 285 | ### 🏡 Chore 286 | 287 | - **log:** Improve log when maestro fails ([6d0f68c](https://github.com/shoma-mano/maests/commit/6d0f68c)) 288 | 289 | ### ❤️ Contributors 290 | 291 | - Shoma-mano 292 | 293 | ## v2.5.2 294 | 295 | [compare changes](https://github.com/shoma-mano/maests/compare/v2.5.1...v2.5.2) 296 | 297 | ### 🩹 Fixes 298 | 299 | - Fix bug when no import from maests exists ([62e5b46](https://github.com/shoma-mano/maests/commit/62e5b46)) 300 | 301 | ### ❤️ Contributors 302 | 303 | - Shoma-mano 304 | 305 | ## v2.5.1 306 | 307 | [compare changes](https://github.com/shoma-mano/maests/compare/v2.5.0...v2.5.1) 308 | 309 | ### 🏡 Chore 310 | 311 | - Update commands.md ([748a82e](https://github.com/shoma-mano/maests/commit/748a82e)) 312 | - Update README.md ([981c831](https://github.com/shoma-mano/maests/commit/981c831)) 313 | - **log:** Improve log when maestro fails ([a4ebf81](https://github.com/shoma-mano/maests/commit/a4ebf81)) 314 | 315 | ### ❤️ Contributors 316 | 317 | - Shoma-mano 318 | 319 | ## v2.5.0 320 | 321 | [compare changes](https://github.com/shoma-mano/maests/compare/v2.4.1...v2.5.0) 322 | 323 | ### 🚀 Enhancements 324 | 325 | - Support tsconfig paths ([ccbc5ed](https://github.com/shoma-mano/maests/commit/ccbc5ed)) 326 | 327 | ### ❤️ Contributors 328 | 329 | - Shoma-mano 330 | 331 | ## v2.4.1 332 | 333 | [compare changes](https://github.com/shoma-mano/maests/compare/v2.4.0...v2.4.1) 334 | 335 | ### 🩹 Fixes 336 | 337 | - Exit(1) when maestro test fails ([d95fcd3](https://github.com/shoma-mano/maests/commit/d95fcd3)) 338 | 339 | ### ❤️ Contributors 340 | 341 | - Shoma-mano 342 | 343 | ## v2.4.0 344 | 345 | [compare changes](https://github.com/shoma-mano/maests/compare/v2.3.0...v2.4.0) 346 | 347 | ### 🚀 Enhancements 348 | 349 | - Support absolute test path ([784977c](https://github.com/shoma-mano/maests/commit/784977c)) 350 | 351 | ### ❤️ Contributors 352 | 353 | - Shoma-mano 354 | 355 | ## v2.3.0 356 | 357 | [compare changes](https://github.com/shoma-mano/maests/compare/v2.2.1...v2.3.0) 358 | 359 | ### 🚀 Enhancements 360 | 361 | - Add times to repeat commands props ([cd289c2](https://github.com/shoma-mano/maests/commit/cd289c2)) 362 | 363 | ### ❤️ Contributors 364 | 365 | - Shoma-mano 366 | 367 | ## v2.2.1 368 | 369 | [compare changes](https://github.com/shoma-mano/maests/compare/v2.2.0...v2.2.1) 370 | 371 | ## v2.2.0 372 | 373 | [compare changes](https://github.com/shoma-mano/maests/compare/v2.1.2...v2.2.0) 374 | 375 | ### 🚀 Enhancements 376 | 377 | - Support matcher in repeat commands ([6c45563](https://github.com/shoma-mano/maests/commit/6c45563)) 378 | 379 | ### 💅 Refactors 380 | 381 | - Use yaml stringify as possible ([0ab0c3a](https://github.com/shoma-mano/maests/commit/0ab0c3a)) 382 | 383 | ### ❤️ Contributors 384 | 385 | - Shoma-mano 386 | 387 | ## v2.1.2 388 | 389 | [compare changes](https://github.com/shoma-mano/maests/compare/v2.1.1...v2.1.2) 390 | 391 | ### 🩹 Fixes 392 | 393 | - RetryTapIfNoChange by default ([75b1f04](https://github.com/shoma-mano/maests/commit/75b1f04)) 394 | 395 | ### ❤️ Contributors 396 | 397 | - Shoma-mano 398 | 399 | ## v2.1.1 400 | 401 | [compare changes](https://github.com/shoma-mano/maests/compare/v2.1.0...v2.1.1) 402 | 403 | ### 💅 Refactors 404 | 405 | - Add patch for mkdist and refactored code ([d55581c](https://github.com/shoma-mano/maests/commit/d55581c)) 406 | - Expose only methods users use ([4a0b717](https://github.com/shoma-mano/maests/commit/4a0b717)) 407 | 408 | ### ❤️ Contributors 409 | 410 | - Shoma-mano 411 | 412 | ## v2.1.0 413 | 414 | [compare changes](https://github.com/shoma-mano/maests/compare/v2.0.8...v2.1.0) 415 | 416 | ### 🚀 Enhancements 417 | 418 | - Enhance assert visible ([489ab49](https://github.com/shoma-mano/maests/commit/489ab49)) 419 | 420 | ### ❤️ Contributors 421 | 422 | - Shoma-mano 423 | 424 | ## v2.0.8 425 | 426 | [compare changes](https://github.com/shoma-mano/maests/compare/v2.0.7...v2.0.8) 427 | 428 | ### 🏡 Chore 429 | 430 | - Update README.md ([0a5f82b](https://github.com/shoma-mano/maests/commit/0a5f82b)) 431 | - Update README.md ([025245e](https://github.com/shoma-mano/maests/commit/025245e)) 432 | 433 | ### ❤️ Contributors 434 | 435 | - Shoma-mano 436 | 437 | ## v2.0.7 438 | 439 | [compare changes](https://github.com/shoma-mano/maests/compare/v2.0.6...v2.0.7) 440 | 441 | ### 🩹 Fixes 442 | 443 | - Test ([748d42a](https://github.com/shoma-mano/maests/commit/748d42a)) 444 | - Action ([31b9601](https://github.com/shoma-mano/maests/commit/31b9601)) 445 | 446 | ### 💅 Refactors 447 | 448 | - Breakdown commands to multiple files ([ed99c5f](https://github.com/shoma-mano/maests/commit/ed99c5f)) 449 | - Breakdown commands to multiple files ([722a99b](https://github.com/shoma-mano/maests/commit/722a99b)) 450 | 451 | ### 🏡 Chore 452 | 453 | - Edit CHANGELOG.md manually ([c10044d](https://github.com/shoma-mano/maests/commit/c10044d)) 454 | - Delete dist ([e10b1ae](https://github.com/shoma-mano/maests/commit/e10b1ae)) 455 | - Add dist to gitignore ([c562517](https://github.com/shoma-mano/maests/commit/c562517)) 456 | - Update README.md ([5f4dd76](https://github.com/shoma-mano/maests/commit/5f4dd76)) 457 | - Update README.md ([e9b039c](https://github.com/shoma-mano/maests/commit/e9b039c)) 458 | - Update README.md ([19e5e95](https://github.com/shoma-mano/maests/commit/19e5e95)) 459 | - Update README.md ([c81eb7f](https://github.com/shoma-mano/maests/commit/c81eb7f)) 460 | 461 | ### ❤️ Contributors 462 | 463 | - Shoma-mano 464 | 465 | ## v2.0.6 466 | 467 | [compare changes](https://github.com/shoma-mano/maests/compare/v2.0.5...v2.0.6) 468 | 469 | ### 🏡 Chore 470 | 471 | - Update README.md ([02a9ca9](https://github.com/shoma-mano/maests/commit/02a9ca9)) 472 | - Add dist to gitignore ([2a581dc](https://github.com/shoma-mano/maests/commit/2a581dc)) 473 | 474 | ### ⭐️ Feature 475 | 476 | - [Feature/meastro commands/unit tests/gh actions](https://github.com/shoma-mano/maests/pull/6) 477 | 478 | ### ❤️ Contributors 479 | 480 | - Ian Ross Hamilton 481 | - Shoma-mano 482 | 483 | ### ❤️ Contributors 484 | 485 | - Shoma-mano 486 | 487 | ## v2.0.5 488 | 489 | [compare changes](https://github.com/shoma-mano/maests/compare/v2.0.4...v2.0.5) 490 | 491 | ### 🩹 Fixes 492 | 493 | - Fix runScript bug ([c63dcbc](https://github.com/shoma-mano/maests/commit/c63dcbc)) 494 | 495 | ### ❤️ Contributors 496 | 497 | - Shoma-mano 498 | 499 | ## v2.0.4 500 | 501 | [compare changes](https://github.com/shoma-mano/maests/compare/v2.0.2...v2.0.4) 502 | 503 | ## v2.0.3 504 | 505 | [compare changes](https://github.com/shoma-mano/maests/compare/v2.0.2...v2.0.3) 506 | 507 | ## v2.0.2 508 | 509 | [compare changes](https://github.com/shoma-mano/maests/compare/v2.0.1...v2.0.2) 510 | 511 | ### 🏡 Chore 512 | 513 | - Update README.md ([fd3acb0](https://github.com/shoma-mano/maests/commit/fd3acb0)) 514 | - Update playground ([f854e39](https://github.com/shoma-mano/maests/commit/f854e39)) 515 | 516 | ### ❤️ Contributors 517 | 518 | - Shoma-mano 519 | 520 | ## v2.0.1 521 | 522 | [compare changes](https://github.com/shoma-mano/maests/compare/v2.0.0...v2.0.1) 523 | 524 | ### 🏡 Chore 525 | 526 | - Update README.md ([5d501a9](https://github.com/shoma-mano/maests/commit/5d501a9)) 527 | 528 | ### ❤️ Contributors 529 | 530 | - Shoma-mano 531 | 532 | ## v2.0.0 533 | 534 | [compare changes](https://github.com/shoma-mano/maests/compare/v1.0.6...v2.0.0) 535 | 536 | ### 🩹 Fixes 537 | 538 | - Support double level nesting ([c305dc8](https://github.com/shoma-mano/maests/commit/c305dc8)) 539 | 540 | ### 💅 Refactors 541 | 542 | - Change rewriteCode to import maests by module name ([4ac7323](https://github.com/shoma-mano/maests/commit/4ac7323)) 543 | 544 | ### 🏡 Chore 545 | 546 | - Update README.md ([b582292](https://github.com/shoma-mano/maests/commit/b582292)) 547 | 548 | ### ❤️ Contributors 549 | 550 | - Shoma-mano 551 | 552 | ## v1.0.6 553 | 554 | [compare changes](https://github.com/shoma-mano/maests/compare/v1.0.5...v1.0.6) 555 | 556 | ## v1.0.5 557 | 558 | [compare changes](https://github.com/shoma-mano/maests/compare/v1.0.4...v1.0.5) 559 | 560 | ### 🏡 Chore 561 | 562 | - Change file name ([b093b26](https://github.com/shoma-mano/maests/commit/b093b26)) 563 | 564 | ### ❤️ Contributors 565 | 566 | - Shoma-mano 567 | 568 | ## v1.0.4 569 | 570 | [compare changes](https://github.com/shoma-mano/maests/compare/v1.0.3...v1.0.4) 571 | 572 | ## v1.0.3 573 | 574 | [compare changes](https://github.com/shoma-mano/maests/compare/v1.0.2...v1.0.3) 575 | 576 | ## v1.0.2 577 | 578 | [compare changes](https://github.com/shoma-mano/maests/compare/v1.0.1...v1.0.2) 579 | 580 | ## v1.0.1 581 | 582 | ### 🏡 Chore 583 | 584 | - Update-readme.md ([23876f4](https://github.com/shoma-mano/maests/commit/23876f4)) 585 | 586 | ### ❤️ Contributors 587 | 588 | - Shoma-mano 589 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A TypeScript-based solution inspired by [maestro-ts](https://github.com/johkade/maestro-ts) for writing and running Maestro flows in a modular and reusable way. 2 | 3 | ## ✅ Features 4 | 5 | - You can write Maestro flows in TypeScript and execute directly. 6 | - Break down Flow into smaller, reusable modules 7 | - Automatically load environment variables from .env 8 | - Handling runScript with type. 9 | 10 | ## 🚀 Getting Started 11 | 12 | ### Requirement 13 | 14 | If you have not installed maestro yet, you have to [install](https://maestro.mobile.dev/getting-started/installing-maestro) it at first. 15 | 16 | ### Installation 17 | 18 | ```sh: 19 | pnpm -D add maests 20 | ``` 21 | 22 | ### 1: Create your first flow 23 | 24 | Create a new file my-first-flow.ts: 25 | 26 | ```typescript 27 | import { M } from "maests"; 28 | 29 | M.initFlow({ appId: "com.myTeam.myApp" }); 30 | M.tapOn("someTestId"); 31 | ``` 32 | 33 | ### 2: Execute Test 34 | 35 | If the Android Emulator or iOS Simulator is launched, you can execute the test with: 36 | 37 | ```sh 38 | npx maests my-first-flow.ts 39 | ``` 40 | 41 | If you don't have a project to try, the [Playground](https://maests.vercel.app/playground.html) is ready for you to use. 42 | 43 | ## 🚨 Trouble Shooting 44 | 45 | Most of the issues are caused by Maestro itself rather than this library. But you can still check the YAML files that are generated by maests in the "maests" folder. 46 | 47 | ## ⭐️ Contributing 48 | 49 | This package is currently under active development, and we welcome contributions from everyone! 50 | -------------------------------------------------------------------------------- /build.config.ts: -------------------------------------------------------------------------------- 1 | import { defineBuildConfig } from "unbuild"; 2 | 3 | export default defineBuildConfig({ 4 | entries: [ 5 | { 6 | input: "src/", 7 | format: "esm", 8 | builder: "mkdist", 9 | declaration: true, 10 | esbuild: { 11 | define: { 12 | "import.meta.vitest": "undefined", 13 | }, 14 | treeShaking: true, 15 | format: "esm", 16 | minifySyntax: true, 17 | }, 18 | }, 19 | ], 20 | }); 21 | -------------------------------------------------------------------------------- /commands.md: -------------------------------------------------------------------------------- 1 | # Supported Commands 2 | 3 | ## Table of Contents 4 | 5 | - [Initialize Flow](#initialize-flow) 6 | - [Run Script](#run-script) 7 | - [Taps](#taps) 8 | - [Swipes and Scrolls](#swipes-and-scrolls) 9 | - [Text Input](#text-input) 10 | - [Navigation and Links](#navigation-and-links) 11 | - [Assertions](#assertions) 12 | - [Waiting](#waiting) 13 | - [Keyboard Actions](#keyboard-actions) 14 | - [Device Volume Controls](#device-volume-controls) 15 | - [Miscellaneous Actions](#miscellaneous-actions) 16 | - [Repeat Actions](#repeat-actions) 17 | 18 | ### Initialize Flow 19 | 20 | - **Launch the Application** 21 | 22 | ```typescript 23 | M.initFlow({ appId: "com.example.app" }); 24 | ``` 25 | 26 | - **Clear Application State** 27 | 28 | ```typescript 29 | M.clearState({ appId: "com.example.app" }); 30 | ``` 31 | 32 | - **Clear Keychain** 33 | 34 | ```typescript 35 | M.clearKeychain(); 36 | ``` 37 | 38 | ### Run Script 39 | 40 | `flow.ts` 41 | 42 | ```typescript 43 | import { M } from "maests"; 44 | 45 | M.runScript("./script.ts"); 46 | ``` 47 | 48 | `script.ts` 49 | 50 | ```typescript 51 | import type { APIResult } from "./type"; 52 | import { hello } from "./hello"; 53 | 54 | // typed http request 55 | const body = http.get("https://jsonplaceholder.typicode.com/todos/1").body; 56 | const result = json(body); 57 | console.log(result.userId); 58 | 59 | // you can use environment variables 60 | console.log(process.env.MAESTRO_APP_ID); 61 | 62 | // you can use imported functions 63 | hello(); 64 | ``` 65 | 66 | ### Taps 67 | 68 | Tap Commands have optional properties available for configuring various Maestro commands, designed to enhance the flexibility and customization of user interactions. The properties are described below. 69 | The `TapOptions` interface defines optional properties for customizing tap actions in Maestro commands, such as `tapOn`, `tapOnText`, and `tapOnPoint`. 70 | 71 | | Property | Type | Description | 72 | | ----------------------- | ------- | ------------------------------------------------------------------------------------------------------------- | 73 | | `index` | number | index of the element you wish to interact with | 74 | | `retryTapIfNoChange` | boolean | If set to `true`, the tap action will be retried if no change is detected on the first attempt. | 75 | | `repeat` | number | Specifies the number of times to repeat the tap action. | 76 | | `waitToSettleTimeoutMs` | number | Time in milliseconds to wait for the element to settle after tapping, useful for handling UI transition time. | 77 | 78 | - **Tap on Id** 79 | 80 | ```typescript 81 | M.tapOn("submitButton", { 82 | retryTapIfNoChange: false, 83 | repeat: 2, 84 | waitToSettleTimeoutMs: 500, 85 | }); 86 | ``` 87 | 88 | - **Tap on Visible Text** 89 | 90 | ```typescript 91 | M.tapOnText("Submit", { retryTapIfNoChange: true }); 92 | ``` 93 | 94 | - **Tap on Point** 95 | 96 | ```typescript 97 | M.tapOnPoint({ x: "50%", y: "50%" }, { waitToSettleTimeoutMs: 200 }); 98 | ``` 99 | 100 | - **Long Press on Element by ID** 101 | 102 | ```typescript 103 | M.longPressOn("submitButton"); 104 | ``` 105 | 106 | - **Long Press on Text** 107 | 108 | ```typescript 109 | M.longPressOnText("Submit"); 110 | ``` 111 | 112 | ### Swipes and Scrolls 113 | 114 | - **Swipe in a Direction** 115 | 116 | ```typescript 117 | M.swipeLeft(); 118 | M.swipeRight(); 119 | M.swipeUp(); 120 | M.swipeDown(); 121 | ``` 122 | 123 | - **Swipe from Point to Point** 124 | 125 | ```typescript 126 | M.swipe({ x: "10%", y: "20%" }, { x: "90%", y: "80%" }); 127 | ``` 128 | 129 | - **Scroll Screen** 130 | 131 | ```typescript 132 | M.scroll(); 133 | ``` 134 | 135 | - **Scroll Until Element is Visible** 136 | 137 | ```typescript 138 | M.scrollUntilVisible("scrollTarget"); 139 | ``` 140 | 141 | ### Text Input 142 | 143 | - **Input Text into an Element** 144 | 145 | ```typescript 146 | M.inputText("Hello, World!", "textFieldId"); 147 | ``` 148 | 149 | - **Input Random Name** 150 | 151 | ```typescript 152 | M.inputRandomName("nameFieldId"); 153 | ``` 154 | 155 | - **Input Random Number** 156 | 157 | ```typescript 158 | M.inputRandomNumber("numberFieldId"); 159 | ``` 160 | 161 | - **Input Random Email** 162 | 163 | ```typescript 164 | M.inputRandomEmail("emailFieldId"); 165 | ``` 166 | 167 | - **Input Random Text** 168 | 169 | ```typescript 170 | M.inputRandomText("textFieldId"); 171 | ``` 172 | 173 | - **Input Random Text** 174 | 175 | ```typescript 176 | M.inputRandomText("textFieldId"); 177 | ``` 178 | 179 | - **Erase Text** 180 | 181 | ```typescript 182 | M.eraseText(10, "textFieldId"); 183 | ``` 184 | 185 | ### Navigation and Links 186 | 187 | - **Open a Link** 188 | 189 | ```typescript 190 | M.openLink("https://example.com"); 191 | ``` 192 | 193 | - **Navigate to Path Using Deep Link Base** 194 | 195 | ```typescript 196 | M.navigate("/home"); 197 | ``` 198 | 199 | ### Assertions 200 | 201 | - **Assert Element is Visible** 202 | 203 | ```typescript 204 | M.assertVisible("submitButton", true); 205 | ``` 206 | 207 | - **Assert Element is Not Visible** 208 | 209 | ```typescript 210 | M.assertNotVisible("submitButton"); 211 | ``` 212 | 213 | ### Waiting 214 | 215 | - **Wait for Animation End** 216 | 217 | ```typescript 218 | M.waitForAnimationEnd(3000); 219 | ``` 220 | 221 | - **Wait Until Element is Visible** 222 | 223 | ```typescript 224 | M.waitUntilVisible("submitButton", 5000); 225 | ``` 226 | 227 | - **Wait Until Element is Not Visiblee** 228 | 229 | ```typescript 230 | M.waitUntilNotVisible("loadingSpinner", 5000); 231 | ``` 232 | 233 | - **Wait a Specified Number of Milliseconds** 234 | 235 | ```typescript 236 | M.wait(1000); 237 | ``` 238 | 239 | ### Keyboard Actions 240 | 241 | - **Hide Keyboard** 242 | 243 | ```typescript 244 | M.hideKeyboard(); 245 | ``` 246 | 247 | - **Press Enter** 248 | 249 | ```typescript 250 | M.pressEnter(); 251 | ``` 252 | 253 | - **Press Home Button** 254 | 255 | ```typescript 256 | M.pressHomeButton(); 257 | ``` 258 | 259 | - **Press Lock Button** 260 | 261 | ```typescript 262 | M.pressLockButton(); 263 | ``` 264 | 265 | - **Press Back Button** 266 | 267 | ```typescript 268 | M.back(); 269 | ``` 270 | 271 | ### Device Volume Controls 272 | 273 | - **Increase Volume** 274 | 275 | ```typescript 276 | M.volumeUp(); 277 | ``` 278 | 279 | - **Decrease Volume** 280 | 281 | ```typescript 282 | M.volumeDown(); 283 | ``` 284 | 285 | ### Miscellaneous Actions 286 | 287 | - **Take Screenshot** 288 | 289 | ```typescript 290 | M.screenshot("my-screenshot.png"); 291 | ``` 292 | 293 | - **Copy Text from an Element** 294 | 295 | ```typescript 296 | M.copyTextFrom("textElementId"); 297 | ``` 298 | 299 | - **Assert Condition is True** 300 | 301 | ```typescript 302 | M.assertTrue("2 + 2 === 4"); 303 | ``` 304 | 305 | ### Repeat Actions 306 | 307 | - **Repeat Actions a Specified Number of Times** 308 | 309 | ```typescript 310 | M.repeat(3, () => { 311 | M.tapOn("submitButton"); 312 | M.wait(500); 313 | }); 314 | ``` 315 | 316 | - **Repeat Actions While Element is Visibles** 317 | 318 | ```typescript 319 | M.repeatWhileVisible("loadingSpinner", () => { 320 | M.wait(1000); 321 | }); 322 | ``` 323 | 324 | - **Repeat Actions While Element is Not Visible** 325 | 326 | ```typescript 327 | M.repeatWhileNotVisible("submitButton", () => { 328 | M.wait(1000); 329 | }); 330 | ``` 331 | -------------------------------------------------------------------------------- /docs/.vitepress/cache/deps/@theme_index.js: -------------------------------------------------------------------------------- 1 | import { 2 | useMediaQuery 3 | } from "./chunk-VNFKW3Y4.js"; 4 | import { 5 | computed, 6 | ref, 7 | shallowRef, 8 | watch 9 | } from "./chunk-JQJWEF5N.js"; 10 | 11 | // node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/index.js 12 | import "/Users/mano/my-oss/maests/docs/node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/styles/fonts.css"; 13 | 14 | // node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/without-fonts.js 15 | import "/Users/mano/my-oss/maests/docs/node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/styles/vars.css"; 16 | import "/Users/mano/my-oss/maests/docs/node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/styles/base.css"; 17 | import "/Users/mano/my-oss/maests/docs/node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/styles/icons.css"; 18 | import "/Users/mano/my-oss/maests/docs/node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/styles/utils.css"; 19 | import "/Users/mano/my-oss/maests/docs/node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/styles/components/custom-block.css"; 20 | import "/Users/mano/my-oss/maests/docs/node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/styles/components/vp-code.css"; 21 | import "/Users/mano/my-oss/maests/docs/node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/styles/components/vp-code-group.css"; 22 | import "/Users/mano/my-oss/maests/docs/node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/styles/components/vp-doc.css"; 23 | import "/Users/mano/my-oss/maests/docs/node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/styles/components/vp-sponsor.css"; 24 | import VPBadge from "/Users/mano/my-oss/maests/docs/node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/components/VPBadge.vue"; 25 | import Layout from "/Users/mano/my-oss/maests/docs/node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/Layout.vue"; 26 | import { default as default2 } from "/Users/mano/my-oss/maests/docs/node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/components/VPBadge.vue"; 27 | import { default as default3 } from "/Users/mano/my-oss/maests/docs/node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/components/VPImage.vue"; 28 | import { default as default4 } from "/Users/mano/my-oss/maests/docs/node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/components/VPButton.vue"; 29 | import { default as default5 } from "/Users/mano/my-oss/maests/docs/node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/components/VPHomeContent.vue"; 30 | import { default as default6 } from "/Users/mano/my-oss/maests/docs/node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/components/VPHomeHero.vue"; 31 | import { default as default7 } from "/Users/mano/my-oss/maests/docs/node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/components/VPHomeFeatures.vue"; 32 | import { default as default8 } from "/Users/mano/my-oss/maests/docs/node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/components/VPHomeSponsors.vue"; 33 | import { default as default9 } from "/Users/mano/my-oss/maests/docs/node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/components/VPLink.vue"; 34 | import { default as default10 } from "/Users/mano/my-oss/maests/docs/node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/components/VPDocAsideSponsors.vue"; 35 | import { default as default11 } from "/Users/mano/my-oss/maests/docs/node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/components/VPSocialLink.vue"; 36 | import { default as default12 } from "/Users/mano/my-oss/maests/docs/node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/components/VPSocialLinks.vue"; 37 | import { default as default13 } from "/Users/mano/my-oss/maests/docs/node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/components/VPSponsors.vue"; 38 | import { default as default14 } from "/Users/mano/my-oss/maests/docs/node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/components/VPTeamPage.vue"; 39 | import { default as default15 } from "/Users/mano/my-oss/maests/docs/node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/components/VPTeamPageTitle.vue"; 40 | import { default as default16 } from "/Users/mano/my-oss/maests/docs/node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/components/VPTeamPageSection.vue"; 41 | import { default as default17 } from "/Users/mano/my-oss/maests/docs/node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/components/VPTeamMembers.vue"; 42 | 43 | // node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/support/utils.js 44 | import { withBase } from "vitepress"; 45 | 46 | // node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/composables/data.js 47 | import { useData as useData$ } from "vitepress"; 48 | var useData = useData$; 49 | 50 | // node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/support/utils.js 51 | function ensureStartingSlash(path) { 52 | return /^\//.test(path) ? path : `/${path}`; 53 | } 54 | 55 | // node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/support/sidebar.js 56 | function getSidebar(_sidebar, path) { 57 | if (Array.isArray(_sidebar)) 58 | return addBase(_sidebar); 59 | if (_sidebar == null) 60 | return []; 61 | path = ensureStartingSlash(path); 62 | const dir = Object.keys(_sidebar).sort((a, b) => { 63 | return b.split("/").length - a.split("/").length; 64 | }).find((dir2) => { 65 | return path.startsWith(ensureStartingSlash(dir2)); 66 | }); 67 | const sidebar = dir ? _sidebar[dir] : []; 68 | return Array.isArray(sidebar) ? addBase(sidebar) : addBase(sidebar.items, sidebar.base); 69 | } 70 | function getSidebarGroups(sidebar) { 71 | const groups = []; 72 | let lastGroupIndex = 0; 73 | for (const index in sidebar) { 74 | const item = sidebar[index]; 75 | if (item.items) { 76 | lastGroupIndex = groups.push(item); 77 | continue; 78 | } 79 | if (!groups[lastGroupIndex]) { 80 | groups.push({ items: [] }); 81 | } 82 | groups[lastGroupIndex].items.push(item); 83 | } 84 | return groups; 85 | } 86 | function addBase(items, _base) { 87 | return [...items].map((_item) => { 88 | const item = { ..._item }; 89 | const base = item.base || _base; 90 | if (base && item.link) 91 | item.link = base + item.link; 92 | if (item.items) 93 | item.items = addBase(item.items, base); 94 | return item; 95 | }); 96 | } 97 | 98 | // node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/composables/sidebar.js 99 | function useSidebar() { 100 | const { frontmatter, page, theme: theme2 } = useData(); 101 | const is960 = useMediaQuery("(min-width: 960px)"); 102 | const isOpen = ref(false); 103 | const _sidebar = computed(() => { 104 | const sidebarConfig = theme2.value.sidebar; 105 | const relativePath = page.value.relativePath; 106 | return sidebarConfig ? getSidebar(sidebarConfig, relativePath) : []; 107 | }); 108 | const sidebar = ref(_sidebar.value); 109 | watch(_sidebar, (next, prev) => { 110 | if (JSON.stringify(next) !== JSON.stringify(prev)) 111 | sidebar.value = _sidebar.value; 112 | }); 113 | const hasSidebar = computed(() => { 114 | return frontmatter.value.sidebar !== false && sidebar.value.length > 0 && frontmatter.value.layout !== "home"; 115 | }); 116 | const leftAside = computed(() => { 117 | if (hasAside) 118 | return frontmatter.value.aside == null ? theme2.value.aside === "left" : frontmatter.value.aside === "left"; 119 | return false; 120 | }); 121 | const hasAside = computed(() => { 122 | if (frontmatter.value.layout === "home") 123 | return false; 124 | if (frontmatter.value.aside != null) 125 | return !!frontmatter.value.aside; 126 | return theme2.value.aside !== false; 127 | }); 128 | const isSidebarEnabled = computed(() => hasSidebar.value && is960.value); 129 | const sidebarGroups = computed(() => { 130 | return hasSidebar.value ? getSidebarGroups(sidebar.value) : []; 131 | }); 132 | function open() { 133 | isOpen.value = true; 134 | } 135 | function close() { 136 | isOpen.value = false; 137 | } 138 | function toggle() { 139 | isOpen.value ? close() : open(); 140 | } 141 | return { 142 | isOpen, 143 | sidebar, 144 | sidebarGroups, 145 | hasSidebar, 146 | hasAside, 147 | leftAside, 148 | isSidebarEnabled, 149 | open, 150 | close, 151 | toggle 152 | }; 153 | } 154 | 155 | // node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/composables/local-nav.js 156 | import { onContentUpdated } from "vitepress"; 157 | 158 | // node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/composables/outline.js 159 | import { getScrollOffset } from "vitepress"; 160 | var resolvedHeaders = []; 161 | function getHeaders(range) { 162 | const headers = [ 163 | ...document.querySelectorAll(".VPDoc :where(h1,h2,h3,h4,h5,h6)") 164 | ].filter((el) => el.id && el.hasChildNodes()).map((el) => { 165 | const level = Number(el.tagName[1]); 166 | return { 167 | element: el, 168 | title: serializeHeader(el), 169 | link: "#" + el.id, 170 | level 171 | }; 172 | }); 173 | return resolveHeaders(headers, range); 174 | } 175 | function serializeHeader(h) { 176 | let ret = ""; 177 | for (const node of h.childNodes) { 178 | if (node.nodeType === 1) { 179 | if (node.classList.contains("VPBadge") || node.classList.contains("header-anchor") || node.classList.contains("ignore-header")) { 180 | continue; 181 | } 182 | ret += node.textContent; 183 | } else if (node.nodeType === 3) { 184 | ret += node.textContent; 185 | } 186 | } 187 | return ret.trim(); 188 | } 189 | function resolveHeaders(headers, range) { 190 | if (range === false) { 191 | return []; 192 | } 193 | const levelsRange = (typeof range === "object" && !Array.isArray(range) ? range.level : range) || 2; 194 | const [high, low] = typeof levelsRange === "number" ? [levelsRange, levelsRange] : levelsRange === "deep" ? [2, 6] : levelsRange; 195 | return buildTree(headers, high, low); 196 | } 197 | function buildTree(data, min, max) { 198 | resolvedHeaders.length = 0; 199 | const result = []; 200 | const stack = []; 201 | data.forEach((item) => { 202 | const node = { ...item, children: [] }; 203 | let parent = stack[stack.length - 1]; 204 | while (parent && parent.level >= node.level) { 205 | stack.pop(); 206 | parent = stack[stack.length - 1]; 207 | } 208 | if (node.element.classList.contains("ignore-header") || parent && "shouldIgnore" in parent) { 209 | stack.push({ level: node.level, shouldIgnore: true }); 210 | return; 211 | } 212 | if (node.level > max || node.level < min) 213 | return; 214 | resolvedHeaders.push({ element: node.element, link: node.link }); 215 | if (parent) 216 | parent.children.push(node); 217 | else 218 | result.push(node); 219 | stack.push(node); 220 | }); 221 | return result; 222 | } 223 | 224 | // node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/composables/local-nav.js 225 | function useLocalNav() { 226 | const { theme: theme2, frontmatter } = useData(); 227 | const headers = shallowRef([]); 228 | const hasLocalNav = computed(() => { 229 | return headers.value.length > 0; 230 | }); 231 | onContentUpdated(() => { 232 | headers.value = getHeaders(frontmatter.value.outline ?? theme2.value.outline); 233 | }); 234 | return { 235 | headers, 236 | hasLocalNav 237 | }; 238 | } 239 | 240 | // node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/without-fonts.js 241 | var theme = { 242 | Layout, 243 | enhanceApp: ({ app }) => { 244 | app.component("Badge", VPBadge); 245 | } 246 | }; 247 | var without_fonts_default = theme; 248 | export { 249 | default2 as VPBadge, 250 | default4 as VPButton, 251 | default10 as VPDocAsideSponsors, 252 | default5 as VPHomeContent, 253 | default7 as VPHomeFeatures, 254 | default6 as VPHomeHero, 255 | default8 as VPHomeSponsors, 256 | default3 as VPImage, 257 | default9 as VPLink, 258 | default11 as VPSocialLink, 259 | default12 as VPSocialLinks, 260 | default13 as VPSponsors, 261 | default17 as VPTeamMembers, 262 | default14 as VPTeamPage, 263 | default16 as VPTeamPageSection, 264 | default15 as VPTeamPageTitle, 265 | without_fonts_default as default, 266 | useLocalNav, 267 | useSidebar 268 | }; 269 | //# sourceMappingURL=@theme_index.js.map 270 | -------------------------------------------------------------------------------- /docs/.vitepress/cache/deps/@theme_index.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "sources": ["../../../node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/index.js", "../../../node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/without-fonts.js", "../../../node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/support/utils.js", "../../../node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/composables/data.js", "../../../node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/support/sidebar.js", "../../../node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/composables/sidebar.js", "../../../node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/composables/local-nav.js", "../../../node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/composables/outline.js"], 4 | "sourcesContent": ["import './styles/fonts.css';\nexport * from './without-fonts';\nexport { default as default } from './without-fonts';\n", "import './styles/vars.css';\nimport './styles/base.css';\nimport './styles/icons.css';\nimport './styles/utils.css';\nimport './styles/components/custom-block.css';\nimport './styles/components/vp-code.css';\nimport './styles/components/vp-code-group.css';\nimport './styles/components/vp-doc.css';\nimport './styles/components/vp-sponsor.css';\nimport VPBadge from './components/VPBadge.vue';\nimport Layout from './Layout.vue';\nexport { default as VPBadge } from './components/VPBadge.vue';\nexport { default as VPImage } from './components/VPImage.vue';\nexport { default as VPButton } from './components/VPButton.vue';\nexport { default as VPHomeContent } from './components/VPHomeContent.vue';\nexport { default as VPHomeHero } from './components/VPHomeHero.vue';\nexport { default as VPHomeFeatures } from './components/VPHomeFeatures.vue';\nexport { default as VPHomeSponsors } from './components/VPHomeSponsors.vue';\nexport { default as VPLink } from './components/VPLink.vue';\nexport { default as VPDocAsideSponsors } from './components/VPDocAsideSponsors.vue';\nexport { default as VPSocialLink } from './components/VPSocialLink.vue';\nexport { default as VPSocialLinks } from './components/VPSocialLinks.vue';\nexport { default as VPSponsors } from './components/VPSponsors.vue';\nexport { default as VPTeamPage } from './components/VPTeamPage.vue';\nexport { default as VPTeamPageTitle } from './components/VPTeamPageTitle.vue';\nexport { default as VPTeamPageSection } from './components/VPTeamPageSection.vue';\nexport { default as VPTeamMembers } from './components/VPTeamMembers.vue';\nexport { useSidebar } from './composables/sidebar';\nexport { useLocalNav } from './composables/local-nav';\nconst theme = {\n Layout,\n enhanceApp: ({ app }) => {\n app.component('Badge', VPBadge);\n }\n};\nexport default theme;\n", "import { withBase } from 'vitepress';\nimport { useData } from '../composables/data';\nimport { isExternal, treatAsHtml } from '../../shared';\nexport function throttleAndDebounce(fn, delay) {\n let timeoutId;\n let called = false;\n return () => {\n if (timeoutId)\n clearTimeout(timeoutId);\n if (!called) {\n fn();\n (called = true) && setTimeout(() => (called = false), delay);\n }\n else\n timeoutId = setTimeout(fn, delay);\n };\n}\nexport function ensureStartingSlash(path) {\n return /^\\//.test(path) ? path : `/${path}`;\n}\nexport function normalizeLink(url) {\n const { pathname, search, hash, protocol } = new URL(url, 'http://a.com');\n if (isExternal(url) ||\n url.startsWith('#') ||\n !protocol.startsWith('http') ||\n !treatAsHtml(pathname))\n return url;\n const { site } = useData();\n const normalizedPath = pathname.endsWith('/') || pathname.endsWith('.html')\n ? url\n : url.replace(/(?:(^\\.+)\\/)?.*$/, `$1${pathname.replace(/(\\.md)?$/, site.value.cleanUrls ? '' : '.html')}${search}${hash}`);\n return withBase(normalizedPath);\n}\n", "import { useData as useData$ } from 'vitepress';\nexport const useData = useData$;\n", "import { ensureStartingSlash } from './utils';\nimport { isActive } from '../../shared';\n/**\n * Get the `Sidebar` from sidebar option. This method will ensure to get correct\n * sidebar config from `MultiSideBarConfig` with various path combinations such\n * as matching `guide/` and `/guide/`. If no matching config was found, it will\n * return empty array.\n */\nexport function getSidebar(_sidebar, path) {\n if (Array.isArray(_sidebar))\n return addBase(_sidebar);\n if (_sidebar == null)\n return [];\n path = ensureStartingSlash(path);\n const dir = Object.keys(_sidebar)\n .sort((a, b) => {\n return b.split('/').length - a.split('/').length;\n })\n .find((dir) => {\n // make sure the multi sidebar key starts with slash too\n return path.startsWith(ensureStartingSlash(dir));\n });\n const sidebar = dir ? _sidebar[dir] : [];\n return Array.isArray(sidebar)\n ? addBase(sidebar)\n : addBase(sidebar.items, sidebar.base);\n}\n/**\n * Get or generate sidebar group from the given sidebar items.\n */\nexport function getSidebarGroups(sidebar) {\n const groups = [];\n let lastGroupIndex = 0;\n for (const index in sidebar) {\n const item = sidebar[index];\n if (item.items) {\n lastGroupIndex = groups.push(item);\n continue;\n }\n if (!groups[lastGroupIndex]) {\n groups.push({ items: [] });\n }\n groups[lastGroupIndex].items.push(item);\n }\n return groups;\n}\nexport function getFlatSideBarLinks(sidebar) {\n const links = [];\n function recursivelyExtractLinks(items) {\n for (const item of items) {\n if (item.text && item.link) {\n links.push({\n text: item.text,\n link: item.link,\n docFooterText: item.docFooterText\n });\n }\n if (item.items) {\n recursivelyExtractLinks(item.items);\n }\n }\n }\n recursivelyExtractLinks(sidebar);\n return links;\n}\n/**\n * Check if the given sidebar item contains any active link.\n */\nexport function hasActiveLink(path, items) {\n if (Array.isArray(items)) {\n return items.some((item) => hasActiveLink(path, item));\n }\n return isActive(path, items.link)\n ? true\n : items.items\n ? hasActiveLink(path, items.items)\n : false;\n}\nfunction addBase(items, _base) {\n return [...items].map((_item) => {\n const item = { ..._item };\n const base = item.base || _base;\n if (base && item.link)\n item.link = base + item.link;\n if (item.items)\n item.items = addBase(item.items, base);\n return item;\n });\n}\n", "import { useMediaQuery } from '@vueuse/core';\nimport { computed, onMounted, onUnmounted, ref, watch, watchEffect, watchPostEffect } from 'vue';\nimport { isActive } from '../../shared';\nimport { hasActiveLink as containsActiveLink, getSidebar, getSidebarGroups } from '../support/sidebar';\nimport { useData } from './data';\nexport function useSidebar() {\n const { frontmatter, page, theme } = useData();\n const is960 = useMediaQuery('(min-width: 960px)');\n const isOpen = ref(false);\n const _sidebar = computed(() => {\n const sidebarConfig = theme.value.sidebar;\n const relativePath = page.value.relativePath;\n return sidebarConfig ? getSidebar(sidebarConfig, relativePath) : [];\n });\n const sidebar = ref(_sidebar.value);\n watch(_sidebar, (next, prev) => {\n if (JSON.stringify(next) !== JSON.stringify(prev))\n sidebar.value = _sidebar.value;\n });\n const hasSidebar = computed(() => {\n return (frontmatter.value.sidebar !== false &&\n sidebar.value.length > 0 &&\n frontmatter.value.layout !== 'home');\n });\n const leftAside = computed(() => {\n if (hasAside)\n return frontmatter.value.aside == null\n ? theme.value.aside === 'left'\n : frontmatter.value.aside === 'left';\n return false;\n });\n const hasAside = computed(() => {\n if (frontmatter.value.layout === 'home')\n return false;\n if (frontmatter.value.aside != null)\n return !!frontmatter.value.aside;\n return theme.value.aside !== false;\n });\n const isSidebarEnabled = computed(() => hasSidebar.value && is960.value);\n const sidebarGroups = computed(() => {\n return hasSidebar.value ? getSidebarGroups(sidebar.value) : [];\n });\n function open() {\n isOpen.value = true;\n }\n function close() {\n isOpen.value = false;\n }\n function toggle() {\n isOpen.value ? close() : open();\n }\n return {\n isOpen,\n sidebar,\n sidebarGroups,\n hasSidebar,\n hasAside,\n leftAside,\n isSidebarEnabled,\n open,\n close,\n toggle\n };\n}\n/**\n * a11y: cache the element that opened the Sidebar (the menu button) then\n * focus that button again when Menu is closed with Escape key.\n */\nexport function useCloseSidebarOnEscape(isOpen, close) {\n let triggerElement;\n watchEffect(() => {\n triggerElement = isOpen.value\n ? document.activeElement\n : undefined;\n });\n onMounted(() => {\n window.addEventListener('keyup', onEscape);\n });\n onUnmounted(() => {\n window.removeEventListener('keyup', onEscape);\n });\n function onEscape(e) {\n if (e.key === 'Escape' && isOpen.value) {\n close();\n triggerElement?.focus();\n }\n }\n}\nexport function useSidebarControl(item) {\n const { page, hash } = useData();\n const collapsed = ref(false);\n const collapsible = computed(() => {\n return item.value.collapsed != null;\n });\n const isLink = computed(() => {\n return !!item.value.link;\n });\n const isActiveLink = ref(false);\n const updateIsActiveLink = () => {\n isActiveLink.value = isActive(page.value.relativePath, item.value.link);\n };\n watch([page, item, hash], updateIsActiveLink);\n onMounted(updateIsActiveLink);\n const hasActiveLink = computed(() => {\n if (isActiveLink.value) {\n return true;\n }\n return item.value.items\n ? containsActiveLink(page.value.relativePath, item.value.items)\n : false;\n });\n const hasChildren = computed(() => {\n return !!(item.value.items && item.value.items.length);\n });\n watchEffect(() => {\n collapsed.value = !!(collapsible.value && item.value.collapsed);\n });\n watchPostEffect(() => {\n ;\n (isActiveLink.value || hasActiveLink.value) && (collapsed.value = false);\n });\n function toggle() {\n if (collapsible.value) {\n collapsed.value = !collapsed.value;\n }\n }\n return {\n collapsed,\n collapsible,\n isLink,\n isActiveLink,\n hasActiveLink,\n hasChildren,\n toggle\n };\n}\n", "import { onContentUpdated } from 'vitepress';\nimport { computed, shallowRef } from 'vue';\nimport { getHeaders } from '../composables/outline';\nimport { useData } from './data';\nexport function useLocalNav() {\n const { theme, frontmatter } = useData();\n const headers = shallowRef([]);\n const hasLocalNav = computed(() => {\n return headers.value.length > 0;\n });\n onContentUpdated(() => {\n headers.value = getHeaders(frontmatter.value.outline ?? theme.value.outline);\n });\n return {\n headers,\n hasLocalNav\n };\n}\n", "import { getScrollOffset } from 'vitepress';\nimport { onMounted, onUnmounted, onUpdated } from 'vue';\nimport { throttleAndDebounce } from '../support/utils';\nimport { useAside } from './aside';\n// cached list of anchor elements from resolveHeaders\nconst resolvedHeaders = [];\nexport function resolveTitle(theme) {\n return ((typeof theme.outline === 'object' &&\n !Array.isArray(theme.outline) &&\n theme.outline.label) ||\n theme.outlineTitle ||\n 'On this page');\n}\nexport function getHeaders(range) {\n const headers = [\n ...document.querySelectorAll('.VPDoc :where(h1,h2,h3,h4,h5,h6)')\n ]\n .filter((el) => el.id && el.hasChildNodes())\n .map((el) => {\n const level = Number(el.tagName[1]);\n return {\n element: el,\n title: serializeHeader(el),\n link: '#' + el.id,\n level\n };\n });\n return resolveHeaders(headers, range);\n}\nfunction serializeHeader(h) {\n let ret = '';\n for (const node of h.childNodes) {\n if (node.nodeType === 1) {\n if (node.classList.contains('VPBadge') ||\n node.classList.contains('header-anchor') ||\n node.classList.contains('ignore-header')) {\n continue;\n }\n ret += node.textContent;\n }\n else if (node.nodeType === 3) {\n ret += node.textContent;\n }\n }\n return ret.trim();\n}\nexport function resolveHeaders(headers, range) {\n if (range === false) {\n return [];\n }\n const levelsRange = (typeof range === 'object' && !Array.isArray(range)\n ? range.level\n : range) || 2;\n const [high, low] = typeof levelsRange === 'number'\n ? [levelsRange, levelsRange]\n : levelsRange === 'deep'\n ? [2, 6]\n : levelsRange;\n return buildTree(headers, high, low);\n}\nexport function useActiveAnchor(container, marker) {\n const { isAsideEnabled } = useAside();\n const onScroll = throttleAndDebounce(setActiveLink, 100);\n let prevActiveLink = null;\n onMounted(() => {\n requestAnimationFrame(setActiveLink);\n window.addEventListener('scroll', onScroll);\n });\n onUpdated(() => {\n // sidebar update means a route change\n activateLink(location.hash);\n });\n onUnmounted(() => {\n window.removeEventListener('scroll', onScroll);\n });\n function setActiveLink() {\n if (!isAsideEnabled.value) {\n return;\n }\n const scrollY = window.scrollY;\n const innerHeight = window.innerHeight;\n const offsetHeight = document.body.offsetHeight;\n const isBottom = Math.abs(scrollY + innerHeight - offsetHeight) < 1;\n // resolvedHeaders may be repositioned, hidden or fix positioned\n const headers = resolvedHeaders\n .map(({ element, link }) => ({\n link,\n top: getAbsoluteTop(element)\n }))\n .filter(({ top }) => !Number.isNaN(top))\n .sort((a, b) => a.top - b.top);\n // no headers available for active link\n if (!headers.length) {\n activateLink(null);\n return;\n }\n // page top\n if (scrollY < 1) {\n activateLink(null);\n return;\n }\n // page bottom - highlight last link\n if (isBottom) {\n activateLink(headers[headers.length - 1].link);\n return;\n }\n // find the last header above the top of viewport\n let activeLink = null;\n for (const { link, top } of headers) {\n if (top > scrollY + getScrollOffset() + 4) {\n break;\n }\n activeLink = link;\n }\n activateLink(activeLink);\n }\n function activateLink(hash) {\n if (prevActiveLink) {\n prevActiveLink.classList.remove('active');\n }\n if (hash == null) {\n prevActiveLink = null;\n }\n else {\n prevActiveLink = container.value.querySelector(`a[href=\"${decodeURIComponent(hash)}\"]`);\n }\n const activeLink = prevActiveLink;\n if (activeLink) {\n activeLink.classList.add('active');\n marker.value.style.top = activeLink.offsetTop + 39 + 'px';\n marker.value.style.opacity = '1';\n }\n else {\n marker.value.style.top = '33px';\n marker.value.style.opacity = '0';\n }\n }\n}\nfunction getAbsoluteTop(element) {\n let offsetTop = 0;\n while (element !== document.body) {\n if (element === null) {\n // child element is:\n // - not attached to the DOM (display: none)\n // - set to fixed position (not scrollable)\n // - body or html element (null offsetParent)\n return NaN;\n }\n offsetTop += element.offsetTop;\n element = element.offsetParent;\n }\n return offsetTop;\n}\nfunction buildTree(data, min, max) {\n resolvedHeaders.length = 0;\n const result = [];\n const stack = [];\n data.forEach((item) => {\n const node = { ...item, children: [] };\n let parent = stack[stack.length - 1];\n while (parent && parent.level >= node.level) {\n stack.pop();\n parent = stack[stack.length - 1];\n }\n if (node.element.classList.contains('ignore-header') ||\n (parent && 'shouldIgnore' in parent)) {\n stack.push({ level: node.level, shouldIgnore: true });\n return;\n }\n if (node.level > max || node.level < min)\n return;\n resolvedHeaders.push({ element: node.element, link: node.link });\n if (parent)\n parent.children.push(node);\n else\n result.push(node);\n stack.push(node);\n });\n return result;\n}\n"], 5 | "mappings": ";;;;;;;;;;;AAAA,OAAO;;;ACAP,OAAO;AACP,OAAO;AACP,OAAO;AACP,OAAO;AACP,OAAO;AACP,OAAO;AACP,OAAO;AACP,OAAO;AACP,OAAO;AACP,OAAO,aAAa;AACpB,OAAO,YAAY;AACnB,SAAoB,WAAXA,gBAA0B;AACnC,SAAoB,WAAXA,gBAA0B;AACnC,SAAoB,WAAXA,gBAA2B;AACpC,SAAoB,WAAXA,gBAAgC;AACzC,SAAoB,WAAXA,gBAA6B;AACtC,SAAoB,WAAXA,gBAAiC;AAC1C,SAAoB,WAAXA,gBAAiC;AAC1C,SAAoB,WAAXA,gBAAyB;AAClC,SAAoB,WAAXA,iBAAqC;AAC9C,SAAoB,WAAXA,iBAA+B;AACxC,SAAoB,WAAXA,iBAAgC;AACzC,SAAoB,WAAXA,iBAA6B;AACtC,SAAoB,WAAXA,iBAA6B;AACtC,SAAoB,WAAXA,iBAAkC;AAC3C,SAAoB,WAAXA,iBAAoC;AAC7C,SAAoB,WAAXA,iBAAgC;;;AC1BzC,SAAS,gBAAgB;;;ACAzB,SAAS,WAAW,gBAAgB;AAC7B,IAAM,UAAU;;;ADgBhB,SAAS,oBAAoB,MAAM;AACtC,SAAO,MAAM,KAAK,IAAI,IAAI,OAAO,IAAI,IAAI;AAC7C;;;AEXO,SAAS,WAAW,UAAU,MAAM;AACvC,MAAI,MAAM,QAAQ,QAAQ;AACtB,WAAO,QAAQ,QAAQ;AAC3B,MAAI,YAAY;AACZ,WAAO,CAAC;AACZ,SAAO,oBAAoB,IAAI;AAC/B,QAAM,MAAM,OAAO,KAAK,QAAQ,EAC3B,KAAK,CAAC,GAAG,MAAM;AAChB,WAAO,EAAE,MAAM,GAAG,EAAE,SAAS,EAAE,MAAM,GAAG,EAAE;AAAA,EAC9C,CAAC,EACI,KAAK,CAACC,SAAQ;AAEf,WAAO,KAAK,WAAW,oBAAoBA,IAAG,CAAC;AAAA,EACnD,CAAC;AACD,QAAM,UAAU,MAAM,SAAS,GAAG,IAAI,CAAC;AACvC,SAAO,MAAM,QAAQ,OAAO,IACtB,QAAQ,OAAO,IACf,QAAQ,QAAQ,OAAO,QAAQ,IAAI;AAC7C;AAIO,SAAS,iBAAiB,SAAS;AACtC,QAAM,SAAS,CAAC;AAChB,MAAI,iBAAiB;AACrB,aAAW,SAAS,SAAS;AACzB,UAAM,OAAO,QAAQ,KAAK;AAC1B,QAAI,KAAK,OAAO;AACZ,uBAAiB,OAAO,KAAK,IAAI;AACjC;AAAA,IACJ;AACA,QAAI,CAAC,OAAO,cAAc,GAAG;AACzB,aAAO,KAAK,EAAE,OAAO,CAAC,EAAE,CAAC;AAAA,IAC7B;AACA,WAAO,cAAc,EAAE,MAAM,KAAK,IAAI;AAAA,EAC1C;AACA,SAAO;AACX;AAiCA,SAAS,QAAQ,OAAO,OAAO;AAC3B,SAAO,CAAC,GAAG,KAAK,EAAE,IAAI,CAAC,UAAU;AAC7B,UAAM,OAAO,EAAE,GAAG,MAAM;AACxB,UAAM,OAAO,KAAK,QAAQ;AAC1B,QAAI,QAAQ,KAAK;AACb,WAAK,OAAO,OAAO,KAAK;AAC5B,QAAI,KAAK;AACL,WAAK,QAAQ,QAAQ,KAAK,OAAO,IAAI;AACzC,WAAO;AAAA,EACX,CAAC;AACL;;;ACnFO,SAAS,aAAa;AACzB,QAAM,EAAE,aAAa,MAAM,OAAAC,OAAM,IAAI,QAAQ;AAC7C,QAAM,QAAQ,cAAc,oBAAoB;AAChD,QAAM,SAAS,IAAI,KAAK;AACxB,QAAM,WAAW,SAAS,MAAM;AAC5B,UAAM,gBAAgBA,OAAM,MAAM;AAClC,UAAM,eAAe,KAAK,MAAM;AAChC,WAAO,gBAAgB,WAAW,eAAe,YAAY,IAAI,CAAC;AAAA,EACtE,CAAC;AACD,QAAM,UAAU,IAAI,SAAS,KAAK;AAClC,QAAM,UAAU,CAAC,MAAM,SAAS;AAC5B,QAAI,KAAK,UAAU,IAAI,MAAM,KAAK,UAAU,IAAI;AAC5C,cAAQ,QAAQ,SAAS;AAAA,EACjC,CAAC;AACD,QAAM,aAAa,SAAS,MAAM;AAC9B,WAAQ,YAAY,MAAM,YAAY,SAClC,QAAQ,MAAM,SAAS,KACvB,YAAY,MAAM,WAAW;AAAA,EACrC,CAAC;AACD,QAAM,YAAY,SAAS,MAAM;AAC7B,QAAI;AACA,aAAO,YAAY,MAAM,SAAS,OAC5BA,OAAM,MAAM,UAAU,SACtB,YAAY,MAAM,UAAU;AACtC,WAAO;AAAA,EACX,CAAC;AACD,QAAM,WAAW,SAAS,MAAM;AAC5B,QAAI,YAAY,MAAM,WAAW;AAC7B,aAAO;AACX,QAAI,YAAY,MAAM,SAAS;AAC3B,aAAO,CAAC,CAAC,YAAY,MAAM;AAC/B,WAAOA,OAAM,MAAM,UAAU;AAAA,EACjC,CAAC;AACD,QAAM,mBAAmB,SAAS,MAAM,WAAW,SAAS,MAAM,KAAK;AACvE,QAAM,gBAAgB,SAAS,MAAM;AACjC,WAAO,WAAW,QAAQ,iBAAiB,QAAQ,KAAK,IAAI,CAAC;AAAA,EACjE,CAAC;AACD,WAAS,OAAO;AACZ,WAAO,QAAQ;AAAA,EACnB;AACA,WAAS,QAAQ;AACb,WAAO,QAAQ;AAAA,EACnB;AACA,WAAS,SAAS;AACd,WAAO,QAAQ,MAAM,IAAI,KAAK;AAAA,EAClC;AACA,SAAO;AAAA,IACH;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACJ;AACJ;;;AC/DA,SAAS,wBAAwB;;;ACAjC,SAAS,uBAAuB;AAKhC,IAAM,kBAAkB,CAAC;AAQlB,SAAS,WAAW,OAAO;AAC9B,QAAM,UAAU;AAAA,IACZ,GAAG,SAAS,iBAAiB,kCAAkC;AAAA,EACnE,EACK,OAAO,CAAC,OAAO,GAAG,MAAM,GAAG,cAAc,CAAC,EAC1C,IAAI,CAAC,OAAO;AACb,UAAM,QAAQ,OAAO,GAAG,QAAQ,CAAC,CAAC;AAClC,WAAO;AAAA,MACH,SAAS;AAAA,MACT,OAAO,gBAAgB,EAAE;AAAA,MACzB,MAAM,MAAM,GAAG;AAAA,MACf;AAAA,IACJ;AAAA,EACJ,CAAC;AACD,SAAO,eAAe,SAAS,KAAK;AACxC;AACA,SAAS,gBAAgB,GAAG;AACxB,MAAI,MAAM;AACV,aAAW,QAAQ,EAAE,YAAY;AAC7B,QAAI,KAAK,aAAa,GAAG;AACrB,UAAI,KAAK,UAAU,SAAS,SAAS,KACjC,KAAK,UAAU,SAAS,eAAe,KACvC,KAAK,UAAU,SAAS,eAAe,GAAG;AAC1C;AAAA,MACJ;AACA,aAAO,KAAK;AAAA,IAChB,WACS,KAAK,aAAa,GAAG;AAC1B,aAAO,KAAK;AAAA,IAChB;AAAA,EACJ;AACA,SAAO,IAAI,KAAK;AACpB;AACO,SAAS,eAAe,SAAS,OAAO;AAC3C,MAAI,UAAU,OAAO;AACjB,WAAO,CAAC;AAAA,EACZ;AACA,QAAM,eAAe,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,IAChE,MAAM,QACN,UAAU;AAChB,QAAM,CAAC,MAAM,GAAG,IAAI,OAAO,gBAAgB,WACrC,CAAC,aAAa,WAAW,IACzB,gBAAgB,SACZ,CAAC,GAAG,CAAC,IACL;AACV,SAAO,UAAU,SAAS,MAAM,GAAG;AACvC;AA8FA,SAAS,UAAU,MAAM,KAAK,KAAK;AAC/B,kBAAgB,SAAS;AACzB,QAAM,SAAS,CAAC;AAChB,QAAM,QAAQ,CAAC;AACf,OAAK,QAAQ,CAAC,SAAS;AACnB,UAAM,OAAO,EAAE,GAAG,MAAM,UAAU,CAAC,EAAE;AACrC,QAAI,SAAS,MAAM,MAAM,SAAS,CAAC;AACnC,WAAO,UAAU,OAAO,SAAS,KAAK,OAAO;AACzC,YAAM,IAAI;AACV,eAAS,MAAM,MAAM,SAAS,CAAC;AAAA,IACnC;AACA,QAAI,KAAK,QAAQ,UAAU,SAAS,eAAe,KAC9C,UAAU,kBAAkB,QAAS;AACtC,YAAM,KAAK,EAAE,OAAO,KAAK,OAAO,cAAc,KAAK,CAAC;AACpD;AAAA,IACJ;AACA,QAAI,KAAK,QAAQ,OAAO,KAAK,QAAQ;AACjC;AACJ,oBAAgB,KAAK,EAAE,SAAS,KAAK,SAAS,MAAM,KAAK,KAAK,CAAC;AAC/D,QAAI;AACA,aAAO,SAAS,KAAK,IAAI;AAAA;AAEzB,aAAO,KAAK,IAAI;AACpB,UAAM,KAAK,IAAI;AAAA,EACnB,CAAC;AACD,SAAO;AACX;;;AD/KO,SAAS,cAAc;AAC1B,QAAM,EAAE,OAAAC,QAAO,YAAY,IAAI,QAAQ;AACvC,QAAM,UAAU,WAAW,CAAC,CAAC;AAC7B,QAAM,cAAc,SAAS,MAAM;AAC/B,WAAO,QAAQ,MAAM,SAAS;AAAA,EAClC,CAAC;AACD,mBAAiB,MAAM;AACnB,YAAQ,QAAQ,WAAW,YAAY,MAAM,WAAWA,OAAM,MAAM,OAAO;AAAA,EAC/E,CAAC;AACD,SAAO;AAAA,IACH;AAAA,IACA;AAAA,EACJ;AACJ;;;ALYA,IAAM,QAAQ;AAAA,EACV;AAAA,EACA,YAAY,CAAC,EAAE,IAAI,MAAM;AACrB,QAAI,UAAU,SAAS,OAAO;AAAA,EAClC;AACJ;AACA,IAAO,wBAAQ;", 6 | "names": ["default", "dir", "theme", "theme"] 7 | } 8 | -------------------------------------------------------------------------------- /docs/.vitepress/cache/deps/_metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "hash": "66092bbb", 3 | "configHash": "bdd1b83f", 4 | "lockfileHash": "48d38ce4", 5 | "browserHash": "b6bf6bdb", 6 | "optimized": { 7 | "vue": { 8 | "src": "../../../node_modules/.pnpm/vue@3.5.12/node_modules/vue/dist/vue.runtime.esm-bundler.js", 9 | "file": "vue.js", 10 | "fileHash": "b8f677bb", 11 | "needsInterop": false 12 | }, 13 | "vitepress > @vue/devtools-api": { 14 | "src": "../../../node_modules/.pnpm/@vue+devtools-api@7.6.4/node_modules/@vue/devtools-api/dist/index.js", 15 | "file": "vitepress___@vue_devtools-api.js", 16 | "fileHash": "f1fbf4b5", 17 | "needsInterop": false 18 | }, 19 | "vitepress > @vueuse/core": { 20 | "src": "../../../node_modules/.pnpm/@vueuse+core@11.2.0_vue@3.5.12/node_modules/@vueuse/core/index.mjs", 21 | "file": "vitepress___@vueuse_core.js", 22 | "fileHash": "42838a78", 23 | "needsInterop": false 24 | }, 25 | "@theme/index": { 26 | "src": "../../../node_modules/.pnpm/vitepress@1.5.0_@algolia+client-search@5.13.0_postcss@8.4.49_search-insights@2.17.2/node_modules/vitepress/dist/client/theme-default/index.js", 27 | "file": "@theme_index.js", 28 | "fileHash": "6fb3f6a2", 29 | "needsInterop": false 30 | } 31 | }, 32 | "chunks": { 33 | "chunk-VNFKW3Y4": { 34 | "file": "chunk-VNFKW3Y4.js" 35 | }, 36 | "chunk-JQJWEF5N": { 37 | "file": "chunk-JQJWEF5N.js" 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /docs/.vitepress/cache/deps/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /docs/.vitepress/cache/deps/vitepress___@vueuse_core.js: -------------------------------------------------------------------------------- 1 | import { 2 | DefaultMagicKeysAliasMap, 3 | StorageSerializers, 4 | TransitionPresets, 5 | assert, 6 | breakpointsAntDesign, 7 | breakpointsBootstrapV5, 8 | breakpointsElement, 9 | breakpointsMasterCss, 10 | breakpointsPrimeFlex, 11 | breakpointsQuasar, 12 | breakpointsSematic, 13 | breakpointsTailwind, 14 | breakpointsVuetify, 15 | breakpointsVuetifyV2, 16 | breakpointsVuetifyV3, 17 | bypassFilter, 18 | camelize, 19 | clamp, 20 | cloneFnJSON, 21 | computedAsync, 22 | computedEager, 23 | computedInject, 24 | computedWithControl, 25 | containsProp, 26 | controlledRef, 27 | createEventHook, 28 | createFetch, 29 | createFilterWrapper, 30 | createGlobalState, 31 | createInjectionState, 32 | createReusableTemplate, 33 | createSharedComposable, 34 | createSingletonPromise, 35 | createTemplatePromise, 36 | createUnrefFn, 37 | customStorageEventName, 38 | debounceFilter, 39 | defaultDocument, 40 | defaultLocation, 41 | defaultNavigator, 42 | defaultWindow, 43 | directiveHooks, 44 | executeTransition, 45 | extendRef, 46 | formatDate, 47 | formatTimeAgo, 48 | get, 49 | getLifeCycleTarget, 50 | getSSRHandler, 51 | hasOwn, 52 | hyphenate, 53 | identity, 54 | increaseWithUnit, 55 | injectLocal, 56 | invoke, 57 | isClient, 58 | isDef, 59 | isDefined, 60 | isIOS, 61 | isObject, 62 | isWorker, 63 | makeDestructurable, 64 | mapGamepadToXbox360Controller, 65 | noop, 66 | normalizeDate, 67 | notNullish, 68 | now, 69 | objectEntries, 70 | objectOmit, 71 | objectPick, 72 | onClickOutside, 73 | onKeyDown, 74 | onKeyPressed, 75 | onKeyStroke, 76 | onKeyUp, 77 | onLongPress, 78 | onStartTyping, 79 | pausableFilter, 80 | promiseTimeout, 81 | provideLocal, 82 | rand, 83 | reactify, 84 | reactifyObject, 85 | reactiveComputed, 86 | reactiveOmit, 87 | reactivePick, 88 | refAutoReset, 89 | refDebounced, 90 | refDefault, 91 | refThrottled, 92 | refWithControl, 93 | resolveRef, 94 | resolveUnref, 95 | set, 96 | setSSRHandler, 97 | syncRef, 98 | syncRefs, 99 | templateRef, 100 | throttleFilter, 101 | timestamp, 102 | toReactive, 103 | toRef, 104 | toRefs, 105 | toValue, 106 | tryOnBeforeMount, 107 | tryOnBeforeUnmount, 108 | tryOnMounted, 109 | tryOnScopeDispose, 110 | tryOnUnmounted, 111 | unrefElement, 112 | until, 113 | useActiveElement, 114 | useAnimate, 115 | useArrayDifference, 116 | useArrayEvery, 117 | useArrayFilter, 118 | useArrayFind, 119 | useArrayFindIndex, 120 | useArrayFindLast, 121 | useArrayIncludes, 122 | useArrayJoin, 123 | useArrayMap, 124 | useArrayReduce, 125 | useArraySome, 126 | useArrayUnique, 127 | useAsyncQueue, 128 | useAsyncState, 129 | useBase64, 130 | useBattery, 131 | useBluetooth, 132 | useBreakpoints, 133 | useBroadcastChannel, 134 | useBrowserLocation, 135 | useCached, 136 | useClipboard, 137 | useClipboardItems, 138 | useCloned, 139 | useColorMode, 140 | useConfirmDialog, 141 | useCounter, 142 | useCssVar, 143 | useCurrentElement, 144 | useCycleList, 145 | useDark, 146 | useDateFormat, 147 | useDebounceFn, 148 | useDebouncedRefHistory, 149 | useDeviceMotion, 150 | useDeviceOrientation, 151 | useDevicePixelRatio, 152 | useDevicesList, 153 | useDisplayMedia, 154 | useDocumentVisibility, 155 | useDraggable, 156 | useDropZone, 157 | useElementBounding, 158 | useElementByPoint, 159 | useElementHover, 160 | useElementSize, 161 | useElementVisibility, 162 | useEventBus, 163 | useEventListener, 164 | useEventSource, 165 | useEyeDropper, 166 | useFavicon, 167 | useFetch, 168 | useFileDialog, 169 | useFileSystemAccess, 170 | useFocus, 171 | useFocusWithin, 172 | useFps, 173 | useFullscreen, 174 | useGamepad, 175 | useGeolocation, 176 | useIdle, 177 | useImage, 178 | useInfiniteScroll, 179 | useIntersectionObserver, 180 | useInterval, 181 | useIntervalFn, 182 | useKeyModifier, 183 | useLastChanged, 184 | useLocalStorage, 185 | useMagicKeys, 186 | useManualRefHistory, 187 | useMediaControls, 188 | useMediaQuery, 189 | useMemoize, 190 | useMemory, 191 | useMounted, 192 | useMouse, 193 | useMouseInElement, 194 | useMousePressed, 195 | useMutationObserver, 196 | useNavigatorLanguage, 197 | useNetwork, 198 | useNow, 199 | useObjectUrl, 200 | useOffsetPagination, 201 | useOnline, 202 | usePageLeave, 203 | useParallax, 204 | useParentElement, 205 | usePerformanceObserver, 206 | usePermission, 207 | usePointer, 208 | usePointerLock, 209 | usePointerSwipe, 210 | usePreferredColorScheme, 211 | usePreferredContrast, 212 | usePreferredDark, 213 | usePreferredLanguages, 214 | usePreferredReducedMotion, 215 | usePrevious, 216 | useRafFn, 217 | useRefHistory, 218 | useResizeObserver, 219 | useScreenOrientation, 220 | useScreenSafeArea, 221 | useScriptTag, 222 | useScroll, 223 | useScrollLock, 224 | useSessionStorage, 225 | useShare, 226 | useSorted, 227 | useSpeechRecognition, 228 | useSpeechSynthesis, 229 | useStepper, 230 | useStorage, 231 | useStorageAsync, 232 | useStyleTag, 233 | useSupported, 234 | useSwipe, 235 | useTemplateRefsList, 236 | useTextDirection, 237 | useTextSelection, 238 | useTextareaAutosize, 239 | useThrottleFn, 240 | useThrottledRefHistory, 241 | useTimeAgo, 242 | useTimeout, 243 | useTimeoutFn, 244 | useTimeoutPoll, 245 | useTimestamp, 246 | useTitle, 247 | useToNumber, 248 | useToString, 249 | useToggle, 250 | useTransition, 251 | useUrlSearchParams, 252 | useUserMedia, 253 | useVModel, 254 | useVModels, 255 | useVibrate, 256 | useVirtualList, 257 | useWakeLock, 258 | useWebNotification, 259 | useWebSocket, 260 | useWebWorker, 261 | useWebWorkerFn, 262 | useWindowFocus, 263 | useWindowScroll, 264 | useWindowSize, 265 | watchArray, 266 | watchAtMost, 267 | watchDebounced, 268 | watchDeep, 269 | watchIgnorable, 270 | watchImmediate, 271 | watchOnce, 272 | watchPausable, 273 | watchThrottled, 274 | watchTriggerable, 275 | watchWithFilter, 276 | whenever 277 | } from "./chunk-VNFKW3Y4.js"; 278 | import "./chunk-JQJWEF5N.js"; 279 | export { 280 | DefaultMagicKeysAliasMap, 281 | StorageSerializers, 282 | TransitionPresets, 283 | assert, 284 | computedAsync as asyncComputed, 285 | refAutoReset as autoResetRef, 286 | breakpointsAntDesign, 287 | breakpointsBootstrapV5, 288 | breakpointsElement, 289 | breakpointsMasterCss, 290 | breakpointsPrimeFlex, 291 | breakpointsQuasar, 292 | breakpointsSematic, 293 | breakpointsTailwind, 294 | breakpointsVuetify, 295 | breakpointsVuetifyV2, 296 | breakpointsVuetifyV3, 297 | bypassFilter, 298 | camelize, 299 | clamp, 300 | cloneFnJSON, 301 | computedAsync, 302 | computedEager, 303 | computedInject, 304 | computedWithControl, 305 | containsProp, 306 | computedWithControl as controlledComputed, 307 | controlledRef, 308 | createEventHook, 309 | createFetch, 310 | createFilterWrapper, 311 | createGlobalState, 312 | createInjectionState, 313 | reactify as createReactiveFn, 314 | createReusableTemplate, 315 | createSharedComposable, 316 | createSingletonPromise, 317 | createTemplatePromise, 318 | createUnrefFn, 319 | customStorageEventName, 320 | debounceFilter, 321 | refDebounced as debouncedRef, 322 | watchDebounced as debouncedWatch, 323 | defaultDocument, 324 | defaultLocation, 325 | defaultNavigator, 326 | defaultWindow, 327 | directiveHooks, 328 | computedEager as eagerComputed, 329 | executeTransition, 330 | extendRef, 331 | formatDate, 332 | formatTimeAgo, 333 | get, 334 | getLifeCycleTarget, 335 | getSSRHandler, 336 | hasOwn, 337 | hyphenate, 338 | identity, 339 | watchIgnorable as ignorableWatch, 340 | increaseWithUnit, 341 | injectLocal, 342 | invoke, 343 | isClient, 344 | isDef, 345 | isDefined, 346 | isIOS, 347 | isObject, 348 | isWorker, 349 | makeDestructurable, 350 | mapGamepadToXbox360Controller, 351 | noop, 352 | normalizeDate, 353 | notNullish, 354 | now, 355 | objectEntries, 356 | objectOmit, 357 | objectPick, 358 | onClickOutside, 359 | onKeyDown, 360 | onKeyPressed, 361 | onKeyStroke, 362 | onKeyUp, 363 | onLongPress, 364 | onStartTyping, 365 | pausableFilter, 366 | watchPausable as pausableWatch, 367 | promiseTimeout, 368 | provideLocal, 369 | rand, 370 | reactify, 371 | reactifyObject, 372 | reactiveComputed, 373 | reactiveOmit, 374 | reactivePick, 375 | refAutoReset, 376 | refDebounced, 377 | refDefault, 378 | refThrottled, 379 | refWithControl, 380 | resolveRef, 381 | resolveUnref, 382 | set, 383 | setSSRHandler, 384 | syncRef, 385 | syncRefs, 386 | templateRef, 387 | throttleFilter, 388 | refThrottled as throttledRef, 389 | watchThrottled as throttledWatch, 390 | timestamp, 391 | toReactive, 392 | toRef, 393 | toRefs, 394 | toValue, 395 | tryOnBeforeMount, 396 | tryOnBeforeUnmount, 397 | tryOnMounted, 398 | tryOnScopeDispose, 399 | tryOnUnmounted, 400 | unrefElement, 401 | until, 402 | useActiveElement, 403 | useAnimate, 404 | useArrayDifference, 405 | useArrayEvery, 406 | useArrayFilter, 407 | useArrayFind, 408 | useArrayFindIndex, 409 | useArrayFindLast, 410 | useArrayIncludes, 411 | useArrayJoin, 412 | useArrayMap, 413 | useArrayReduce, 414 | useArraySome, 415 | useArrayUnique, 416 | useAsyncQueue, 417 | useAsyncState, 418 | useBase64, 419 | useBattery, 420 | useBluetooth, 421 | useBreakpoints, 422 | useBroadcastChannel, 423 | useBrowserLocation, 424 | useCached, 425 | useClipboard, 426 | useClipboardItems, 427 | useCloned, 428 | useColorMode, 429 | useConfirmDialog, 430 | useCounter, 431 | useCssVar, 432 | useCurrentElement, 433 | useCycleList, 434 | useDark, 435 | useDateFormat, 436 | refDebounced as useDebounce, 437 | useDebounceFn, 438 | useDebouncedRefHistory, 439 | useDeviceMotion, 440 | useDeviceOrientation, 441 | useDevicePixelRatio, 442 | useDevicesList, 443 | useDisplayMedia, 444 | useDocumentVisibility, 445 | useDraggable, 446 | useDropZone, 447 | useElementBounding, 448 | useElementByPoint, 449 | useElementHover, 450 | useElementSize, 451 | useElementVisibility, 452 | useEventBus, 453 | useEventListener, 454 | useEventSource, 455 | useEyeDropper, 456 | useFavicon, 457 | useFetch, 458 | useFileDialog, 459 | useFileSystemAccess, 460 | useFocus, 461 | useFocusWithin, 462 | useFps, 463 | useFullscreen, 464 | useGamepad, 465 | useGeolocation, 466 | useIdle, 467 | useImage, 468 | useInfiniteScroll, 469 | useIntersectionObserver, 470 | useInterval, 471 | useIntervalFn, 472 | useKeyModifier, 473 | useLastChanged, 474 | useLocalStorage, 475 | useMagicKeys, 476 | useManualRefHistory, 477 | useMediaControls, 478 | useMediaQuery, 479 | useMemoize, 480 | useMemory, 481 | useMounted, 482 | useMouse, 483 | useMouseInElement, 484 | useMousePressed, 485 | useMutationObserver, 486 | useNavigatorLanguage, 487 | useNetwork, 488 | useNow, 489 | useObjectUrl, 490 | useOffsetPagination, 491 | useOnline, 492 | usePageLeave, 493 | useParallax, 494 | useParentElement, 495 | usePerformanceObserver, 496 | usePermission, 497 | usePointer, 498 | usePointerLock, 499 | usePointerSwipe, 500 | usePreferredColorScheme, 501 | usePreferredContrast, 502 | usePreferredDark, 503 | usePreferredLanguages, 504 | usePreferredReducedMotion, 505 | usePrevious, 506 | useRafFn, 507 | useRefHistory, 508 | useResizeObserver, 509 | useScreenOrientation, 510 | useScreenSafeArea, 511 | useScriptTag, 512 | useScroll, 513 | useScrollLock, 514 | useSessionStorage, 515 | useShare, 516 | useSorted, 517 | useSpeechRecognition, 518 | useSpeechSynthesis, 519 | useStepper, 520 | useStorage, 521 | useStorageAsync, 522 | useStyleTag, 523 | useSupported, 524 | useSwipe, 525 | useTemplateRefsList, 526 | useTextDirection, 527 | useTextSelection, 528 | useTextareaAutosize, 529 | refThrottled as useThrottle, 530 | useThrottleFn, 531 | useThrottledRefHistory, 532 | useTimeAgo, 533 | useTimeout, 534 | useTimeoutFn, 535 | useTimeoutPoll, 536 | useTimestamp, 537 | useTitle, 538 | useToNumber, 539 | useToString, 540 | useToggle, 541 | useTransition, 542 | useUrlSearchParams, 543 | useUserMedia, 544 | useVModel, 545 | useVModels, 546 | useVibrate, 547 | useVirtualList, 548 | useWakeLock, 549 | useWebNotification, 550 | useWebSocket, 551 | useWebWorker, 552 | useWebWorkerFn, 553 | useWindowFocus, 554 | useWindowScroll, 555 | useWindowSize, 556 | watchArray, 557 | watchAtMost, 558 | watchDebounced, 559 | watchDeep, 560 | watchIgnorable, 561 | watchImmediate, 562 | watchOnce, 563 | watchPausable, 564 | watchThrottled, 565 | watchTriggerable, 566 | watchWithFilter, 567 | whenever 568 | }; 569 | //# sourceMappingURL=vitepress___@vueuse_core.js.map 570 | -------------------------------------------------------------------------------- /docs/.vitepress/cache/deps/vitepress___@vueuse_core.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "sources": [], 4 | "sourcesContent": [], 5 | "mappings": "", 6 | "names": [] 7 | } 8 | -------------------------------------------------------------------------------- /docs/.vitepress/cache/deps/vue.js: -------------------------------------------------------------------------------- 1 | import { 2 | BaseTransition, 3 | BaseTransitionPropsValidators, 4 | Comment, 5 | DeprecationTypes, 6 | EffectScope, 7 | ErrorCodes, 8 | ErrorTypeStrings, 9 | Fragment, 10 | KeepAlive, 11 | ReactiveEffect, 12 | Static, 13 | Suspense, 14 | Teleport, 15 | Text, 16 | TrackOpTypes, 17 | Transition, 18 | TransitionGroup, 19 | TriggerOpTypes, 20 | VueElement, 21 | assertNumber, 22 | callWithAsyncErrorHandling, 23 | callWithErrorHandling, 24 | camelize, 25 | capitalize, 26 | cloneVNode, 27 | compatUtils, 28 | compile, 29 | computed, 30 | createApp, 31 | createBaseVNode, 32 | createBlock, 33 | createCommentVNode, 34 | createElementBlock, 35 | createHydrationRenderer, 36 | createPropsRestProxy, 37 | createRenderer, 38 | createSSRApp, 39 | createSlots, 40 | createStaticVNode, 41 | createTextVNode, 42 | createVNode, 43 | customRef, 44 | defineAsyncComponent, 45 | defineComponent, 46 | defineCustomElement, 47 | defineEmits, 48 | defineExpose, 49 | defineModel, 50 | defineOptions, 51 | defineProps, 52 | defineSSRCustomElement, 53 | defineSlots, 54 | devtools, 55 | effect, 56 | effectScope, 57 | getCurrentInstance, 58 | getCurrentScope, 59 | getCurrentWatcher, 60 | getTransitionRawChildren, 61 | guardReactiveProps, 62 | h, 63 | handleError, 64 | hasInjectionContext, 65 | hydrate, 66 | hydrateOnIdle, 67 | hydrateOnInteraction, 68 | hydrateOnMediaQuery, 69 | hydrateOnVisible, 70 | initCustomFormatter, 71 | initDirectivesForSSR, 72 | inject, 73 | isMemoSame, 74 | isProxy, 75 | isReactive, 76 | isReadonly, 77 | isRef, 78 | isRuntimeOnly, 79 | isShallow, 80 | isVNode, 81 | markRaw, 82 | mergeDefaults, 83 | mergeModels, 84 | mergeProps, 85 | nextTick, 86 | normalizeClass, 87 | normalizeProps, 88 | normalizeStyle, 89 | onActivated, 90 | onBeforeMount, 91 | onBeforeUnmount, 92 | onBeforeUpdate, 93 | onDeactivated, 94 | onErrorCaptured, 95 | onMounted, 96 | onRenderTracked, 97 | onRenderTriggered, 98 | onScopeDispose, 99 | onServerPrefetch, 100 | onUnmounted, 101 | onUpdated, 102 | onWatcherCleanup, 103 | openBlock, 104 | popScopeId, 105 | provide, 106 | proxyRefs, 107 | pushScopeId, 108 | queuePostFlushCb, 109 | reactive, 110 | readonly, 111 | ref, 112 | registerRuntimeCompiler, 113 | render, 114 | renderList, 115 | renderSlot, 116 | resolveComponent, 117 | resolveDirective, 118 | resolveDynamicComponent, 119 | resolveFilter, 120 | resolveTransitionHooks, 121 | setBlockTracking, 122 | setDevtoolsHook, 123 | setTransitionHooks, 124 | shallowReactive, 125 | shallowReadonly, 126 | shallowRef, 127 | ssrContextKey, 128 | ssrUtils, 129 | stop, 130 | toDisplayString, 131 | toHandlerKey, 132 | toHandlers, 133 | toRaw, 134 | toRef, 135 | toRefs, 136 | toValue, 137 | transformVNodeArgs, 138 | triggerRef, 139 | unref, 140 | useAttrs, 141 | useCssModule, 142 | useCssVars, 143 | useHost, 144 | useId, 145 | useModel, 146 | useSSRContext, 147 | useShadowRoot, 148 | useSlots, 149 | useTemplateRef, 150 | useTransitionState, 151 | vModelCheckbox, 152 | vModelDynamic, 153 | vModelRadio, 154 | vModelSelect, 155 | vModelText, 156 | vShow, 157 | version, 158 | warn, 159 | watch, 160 | watchEffect, 161 | watchPostEffect, 162 | watchSyncEffect, 163 | withAsyncContext, 164 | withCtx, 165 | withDefaults, 166 | withDirectives, 167 | withKeys, 168 | withMemo, 169 | withModifiers, 170 | withScopeId 171 | } from "./chunk-JQJWEF5N.js"; 172 | export { 173 | BaseTransition, 174 | BaseTransitionPropsValidators, 175 | Comment, 176 | DeprecationTypes, 177 | EffectScope, 178 | ErrorCodes, 179 | ErrorTypeStrings, 180 | Fragment, 181 | KeepAlive, 182 | ReactiveEffect, 183 | Static, 184 | Suspense, 185 | Teleport, 186 | Text, 187 | TrackOpTypes, 188 | Transition, 189 | TransitionGroup, 190 | TriggerOpTypes, 191 | VueElement, 192 | assertNumber, 193 | callWithAsyncErrorHandling, 194 | callWithErrorHandling, 195 | camelize, 196 | capitalize, 197 | cloneVNode, 198 | compatUtils, 199 | compile, 200 | computed, 201 | createApp, 202 | createBlock, 203 | createCommentVNode, 204 | createElementBlock, 205 | createBaseVNode as createElementVNode, 206 | createHydrationRenderer, 207 | createPropsRestProxy, 208 | createRenderer, 209 | createSSRApp, 210 | createSlots, 211 | createStaticVNode, 212 | createTextVNode, 213 | createVNode, 214 | customRef, 215 | defineAsyncComponent, 216 | defineComponent, 217 | defineCustomElement, 218 | defineEmits, 219 | defineExpose, 220 | defineModel, 221 | defineOptions, 222 | defineProps, 223 | defineSSRCustomElement, 224 | defineSlots, 225 | devtools, 226 | effect, 227 | effectScope, 228 | getCurrentInstance, 229 | getCurrentScope, 230 | getCurrentWatcher, 231 | getTransitionRawChildren, 232 | guardReactiveProps, 233 | h, 234 | handleError, 235 | hasInjectionContext, 236 | hydrate, 237 | hydrateOnIdle, 238 | hydrateOnInteraction, 239 | hydrateOnMediaQuery, 240 | hydrateOnVisible, 241 | initCustomFormatter, 242 | initDirectivesForSSR, 243 | inject, 244 | isMemoSame, 245 | isProxy, 246 | isReactive, 247 | isReadonly, 248 | isRef, 249 | isRuntimeOnly, 250 | isShallow, 251 | isVNode, 252 | markRaw, 253 | mergeDefaults, 254 | mergeModels, 255 | mergeProps, 256 | nextTick, 257 | normalizeClass, 258 | normalizeProps, 259 | normalizeStyle, 260 | onActivated, 261 | onBeforeMount, 262 | onBeforeUnmount, 263 | onBeforeUpdate, 264 | onDeactivated, 265 | onErrorCaptured, 266 | onMounted, 267 | onRenderTracked, 268 | onRenderTriggered, 269 | onScopeDispose, 270 | onServerPrefetch, 271 | onUnmounted, 272 | onUpdated, 273 | onWatcherCleanup, 274 | openBlock, 275 | popScopeId, 276 | provide, 277 | proxyRefs, 278 | pushScopeId, 279 | queuePostFlushCb, 280 | reactive, 281 | readonly, 282 | ref, 283 | registerRuntimeCompiler, 284 | render, 285 | renderList, 286 | renderSlot, 287 | resolveComponent, 288 | resolveDirective, 289 | resolveDynamicComponent, 290 | resolveFilter, 291 | resolveTransitionHooks, 292 | setBlockTracking, 293 | setDevtoolsHook, 294 | setTransitionHooks, 295 | shallowReactive, 296 | shallowReadonly, 297 | shallowRef, 298 | ssrContextKey, 299 | ssrUtils, 300 | stop, 301 | toDisplayString, 302 | toHandlerKey, 303 | toHandlers, 304 | toRaw, 305 | toRef, 306 | toRefs, 307 | toValue, 308 | transformVNodeArgs, 309 | triggerRef, 310 | unref, 311 | useAttrs, 312 | useCssModule, 313 | useCssVars, 314 | useHost, 315 | useId, 316 | useModel, 317 | useSSRContext, 318 | useShadowRoot, 319 | useSlots, 320 | useTemplateRef, 321 | useTransitionState, 322 | vModelCheckbox, 323 | vModelDynamic, 324 | vModelRadio, 325 | vModelSelect, 326 | vModelText, 327 | vShow, 328 | version, 329 | warn, 330 | watch, 331 | watchEffect, 332 | watchPostEffect, 333 | watchSyncEffect, 334 | withAsyncContext, 335 | withCtx, 336 | withDefaults, 337 | withDirectives, 338 | withKeys, 339 | withMemo, 340 | withModifiers, 341 | withScopeId 342 | }; 343 | //# sourceMappingURL=vue.js.map 344 | -------------------------------------------------------------------------------- /docs/.vitepress/cache/deps/vue.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "sources": [], 4 | "sourcesContent": [], 5 | "mappings": "", 6 | "names": [] 7 | } 8 | -------------------------------------------------------------------------------- /docs/.vitepress/config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitepress"; 2 | 3 | // https://vitepress.dev/reference/site-config 4 | export default defineConfig({ 5 | title: "Maests", 6 | description: "Maests Docs", 7 | head: [["link", { rel: "icon", type: "image/png", href: "/maests.png" }]], 8 | themeConfig: { 9 | // https://vitepress.dev/reference/default-theme-config 10 | 11 | logo: "/maests.png", 12 | 13 | sidebar: [ 14 | { 15 | text: "Introduction", 16 | items: [ 17 | { text: "Getting Started", link: "/getting-started" }, 18 | { text: "Playground", link: "/playground" }, 19 | ], 20 | }, 21 | { 22 | text: "API", 23 | items: [{ text: "Commands", link: "/commands" }], 24 | }, 25 | ], 26 | 27 | socialLinks: [ 28 | { icon: "github", link: "https://github.com/shoma-mano/maests" }, 29 | ], 30 | }, 31 | }); 32 | -------------------------------------------------------------------------------- /docs/commands.md: -------------------------------------------------------------------------------- 1 | # Supported Commands 2 | 3 | :::warning 4 | This page is under construction. 5 | ::: 6 | 7 | ## Table of Contents 8 | 9 | - [Initialize Flow](#initialize-flow) 10 | - [Run Script](#run-script) 11 | - [Taps](#taps) 12 | - [Swipes and Scrolls](#swipes-and-scrolls) 13 | - [Text Input](#text-input) 14 | - [Navigation and Links](#navigation-and-links) 15 | - [Assertions](#assertions) 16 | - [Waiting](#waiting) 17 | - [Keyboard Actions](#keyboard-actions) 18 | - [Device Volume Controls](#device-volume-controls) 19 | - [Miscellaneous Actions](#miscellaneous-actions) 20 | - [Repeat Actions](#repeat-actions) 21 | 22 | ### Initialize Flow 23 | 24 | - **Launch the Application** 25 | 26 | ```typescript 27 | M.initFlow({ appId: "com.example.app" }); 28 | ``` 29 | 30 | - **Clear Application State** 31 | 32 | ```typescript 33 | M.clearState({ appId: "com.example.app" }); 34 | ``` 35 | 36 | - **Clear Keychain** 37 | 38 | ```typescript 39 | M.clearKeychain(); 40 | ``` 41 | 42 | ### Run Script 43 | 44 | `flow.ts` 45 | 46 | ```typescript 47 | import { M } from "maests"; 48 | 49 | M.runScript("./script.ts"); 50 | ``` 51 | 52 | `script.ts` 53 | 54 | ```typescript 55 | import type { APIResult } from "./type"; 56 | import { hello } from "./hello"; 57 | 58 | // typed http request 59 | const body = http.get("https://jsonplaceholder.typicode.com/todos/1").body; 60 | const result = json(body); 61 | console.log(result.userId); 62 | 63 | // you can use environment variables 64 | console.log(process.env.MAESTRO_APP_ID); 65 | 66 | // you can use imported functions 67 | hello(); 68 | ``` 69 | 70 | ### Taps 71 | 72 | Tap Commands have optional properties available for configuring various Maestro commands, designed to enhance the flexibility and customization of user interactions. The properties are described below. 73 | The `TapOptions` interface defines optional properties for customizing tap actions in Maestro commands, such as `tapOn`, `tapOnText`, and `tapOnPoint`. 74 | 75 | | Property | Type | Description | 76 | | ----------------------- | ------- | ------------------------------------------------------------------------------------------------------------- | 77 | | `index` | number | index of the element you wish to interact with | 78 | | `retryTapIfNoChange` | boolean | If set to `true`, the tap action will be retried if no change is detected on the first attempt. | 79 | | `repeat` | number | Specifies the number of times to repeat the tap action. | 80 | | `waitToSettleTimeoutMs` | number | Time in milliseconds to wait for the element to settle after tapping, useful for handling UI transition time. | 81 | 82 | - **Tap on Id** 83 | 84 | ```typescript 85 | M.tapOn("submitButton", { 86 | retryTapIfNoChange: false, 87 | repeat: 2, 88 | waitToSettleTimeoutMs: 500, 89 | }); 90 | ``` 91 | 92 | - **Tap on Visible Text** 93 | 94 | ```typescript 95 | M.tapOnText("Submit", { retryTapIfNoChange: true }); 96 | ``` 97 | 98 | - **Tap on Point** 99 | 100 | ```typescript 101 | M.tapOnPoint({ x: "50%", y: "50%" }, { waitToSettleTimeoutMs: 200 }); 102 | ``` 103 | 104 | - **Long Press on Element by ID** 105 | 106 | ```typescript 107 | M.longPressOn("submitButton"); 108 | ``` 109 | 110 | - **Long Press on Text** 111 | 112 | ```typescript 113 | M.longPressOnText("Submit"); 114 | ``` 115 | 116 | ### Swipes and Scrolls 117 | 118 | - **Swipe in a Direction** 119 | 120 | ```typescript 121 | M.swipeLeft(); 122 | M.swipeRight(); 123 | M.swipeUp(); 124 | M.swipeDown(); 125 | ``` 126 | 127 | - **Swipe from Point to Point** 128 | 129 | ```typescript 130 | M.swipe({ x: "10%", y: "20%" }, { x: "90%", y: "80%" }); 131 | ``` 132 | 133 | - **Scroll Screen** 134 | 135 | ```typescript 136 | M.scroll(); 137 | ``` 138 | 139 | - **Scroll Until Element is Visible** 140 | 141 | ```typescript 142 | M.scrollUntilVisible("scrollTarget"); 143 | ``` 144 | 145 | ### Text Input 146 | 147 | - **Input Text into an Element** 148 | 149 | ```typescript 150 | M.inputText("Hello, World!", "textFieldId"); 151 | ``` 152 | 153 | - **Input Random Name** 154 | 155 | ```typescript 156 | M.inputRandomName("nameFieldId"); 157 | ``` 158 | 159 | - **Input Random Number** 160 | 161 | ```typescript 162 | M.inputRandomNumber("numberFieldId"); 163 | ``` 164 | 165 | - **Input Random Email** 166 | 167 | ```typescript 168 | M.inputRandomEmail("emailFieldId"); 169 | ``` 170 | 171 | - **Input Random Text** 172 | 173 | ```typescript 174 | M.inputRandomText("textFieldId"); 175 | ``` 176 | 177 | - **Input Random Text** 178 | 179 | ```typescript 180 | M.inputRandomText("textFieldId"); 181 | ``` 182 | 183 | - **Erase Text** 184 | 185 | ```typescript 186 | M.eraseText(10, "textFieldId"); 187 | ``` 188 | 189 | ### Navigation and Links 190 | 191 | - **Open a Link** 192 | 193 | ```typescript 194 | M.openLink("https://example.com"); 195 | ``` 196 | 197 | - **Navigate to Path Using Deep Link Base** 198 | 199 | ```typescript 200 | M.navigate("/home"); 201 | ``` 202 | 203 | ### Assertions 204 | 205 | - **Assert Element is Visible** 206 | 207 | ```typescript 208 | M.assertVisible("submitButton", true); 209 | ``` 210 | 211 | - **Assert Element is Not Visible** 212 | 213 | ```typescript 214 | M.assertNotVisible("submitButton"); 215 | ``` 216 | 217 | ### Waiting 218 | 219 | - **Wait for Animation End** 220 | 221 | ```typescript 222 | M.waitForAnimationEnd(3000); 223 | ``` 224 | 225 | - **Wait Until Element is Visible** 226 | 227 | ```typescript 228 | M.waitUntilVisible("submitButton", 5000); 229 | ``` 230 | 231 | - **Wait Until Element is Not Visiblee** 232 | 233 | ```typescript 234 | M.waitUntilNotVisible("loadingSpinner", 5000); 235 | ``` 236 | 237 | - **Wait a Specified Number of Milliseconds** 238 | 239 | ```typescript 240 | M.wait(1000); 241 | ``` 242 | 243 | ### Keyboard Actions 244 | 245 | - **Hide Keyboard** 246 | 247 | ```typescript 248 | M.hideKeyboard(); 249 | ``` 250 | 251 | - **Press Enter** 252 | 253 | ```typescript 254 | M.pressEnter(); 255 | ``` 256 | 257 | - **Press Home Button** 258 | 259 | ```typescript 260 | M.pressHomeButton(); 261 | ``` 262 | 263 | - **Press Lock Button** 264 | 265 | ```typescript 266 | M.pressLockButton(); 267 | ``` 268 | 269 | - **Press Back Button** 270 | 271 | ```typescript 272 | M.back(); 273 | ``` 274 | 275 | ### Device Volume Controls 276 | 277 | - **Increase Volume** 278 | 279 | ```typescript 280 | M.volumeUp(); 281 | ``` 282 | 283 | - **Decrease Volume** 284 | 285 | ```typescript 286 | M.volumeDown(); 287 | ``` 288 | 289 | ### Miscellaneous Actions 290 | 291 | - **Take Screenshot** 292 | 293 | ```typescript 294 | M.screenshot("my-screenshot.png"); 295 | ``` 296 | 297 | - **Copy Text from an Element** 298 | 299 | ```typescript 300 | M.copyTextFrom("textElementId"); 301 | ``` 302 | 303 | - **Assert Condition is True** 304 | 305 | ```typescript 306 | M.assertTrue("2 + 2 === 4"); 307 | ``` 308 | 309 | ### Repeat Actions 310 | 311 | - **Repeat Actions a Specified Number of Times** 312 | 313 | ```typescript 314 | M.repeat(3, () => { 315 | M.tapOn("submitButton"); 316 | M.wait(500); 317 | }); 318 | ``` 319 | 320 | - **Repeat Actions While Element is Visibles** 321 | 322 | ```typescript 323 | M.repeatWhileVisible("loadingSpinner", () => { 324 | M.wait(1000); 325 | }); 326 | ``` 327 | 328 | - **Repeat Actions While Element is Not Visible** 329 | 330 | ```typescript 331 | M.repeatWhileNotVisible("submitButton", () => { 332 | M.wait(1000); 333 | }); 334 | ``` 335 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | # 🚀 Getting Started 2 | 3 | ## Getting Stared 4 | 5 | ::: warning Requirement 6 | If you have not installed maestro yet, you have to [install](https://maestro.mobile.dev/getting-started/installing-maestro) it at first. 7 | ::: 8 | 9 | ### Installation 10 | 11 | ```sh: 12 | pnpm -D add maests 13 | ``` 14 | 15 | ### 1 : Create your first flow 16 | 17 | Create a new file `my-first-flow.ts` 18 | 19 | ```typescript 20 | import { M } from "maests"; 21 | 22 | M.initFlow({ appId: "com.myTeam.myApp" }); 23 | M.tapOn("someTestId"); 24 | ``` 25 | 26 | ### 2 : Execute Test 27 | 28 | If the Android Emulator or iOS Simulator is launched, you can execute the test with: 29 | 30 | ```sh 31 | npx maests my-first-flow.ts 32 | ``` 33 | 34 | If you don't have a project to try, the [Playground](./playground) is ready for you to use. 35 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: "Maests" 7 | text: "Run Maestro with TypeScript" 8 | tagline: Write Maestro flows in TypeScript and execute directly. 9 | actions: 10 | - theme: brand 11 | text: Getting Started 12 | link: /getting-started 13 | 14 | image: 15 | src: /maests.png 16 | 17 | features: 18 | - icon: 🚀 19 | title: Easily Break Down Flows 20 | details: Break down flows into smaller, reusable modules moere easily than yaml. 21 | - icon: 🛹 22 | title: Write Flows Faster with IDE Support 23 | details: Write flows faster with the power of an IDE. 24 | - icon: 🎸 25 | title: Handle runScript with Type Safety 26 | details: Write scripts in TypeScript and execute them by passing them as callbacks. 27 | --- 28 | 29 | 50 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "docs:dev": "vitepress dev", 9 | "docs:build": "vitepress build", 10 | "docs:preview": "vitepress preview" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "devDependencies": { 16 | "vitepress": "^1.5.0" 17 | } 18 | } -------------------------------------------------------------------------------- /docs/playground.md: -------------------------------------------------------------------------------- 1 | # 🛹 Playground 2 | 3 | ## Sample Flow 4 | 5 | There is sample flow you can try actually in [playground](https://github.com/shoma-mano/maests/tree/main/playground). 6 | 7 | ```typescript 8 | import { getOutput, M } from "maests"; 9 | import { openApp } from "@/e2e/utils/openApp"; 10 | import { someScript } from "./utils/script"; 11 | 12 | // use composable flow easiliy 13 | openApp(); 14 | 15 | // run script like this 16 | M.runScript(someScript); 17 | 18 | // use variables set in someScript 19 | M.assertVisible({ id: getOutput("id") }); 20 | 21 | // use runFlow to run a flow with a condition 22 | M.runFlow({ 23 | flow: () => { 24 | M.repeatWhileNotVisible( 25 | { 26 | text: "4", 27 | }, 28 | () => { 29 | M.tapOnText("Increment"); 30 | } 31 | ); 32 | }, 33 | condition: { 34 | visible: "Increment", 35 | }, 36 | }); 37 | ``` 38 | 39 | ## Run Sample Flow 40 | 41 | You can try maests by above sample flow with simulator. 42 | 43 | ### 1.Clone repo 44 | 45 | ```shell 46 | git clone https://github.com/shoma-mano/maests 47 | cd maests 48 | ``` 49 | 50 | ### 2.Build maests 51 | 52 | ```shell 53 | pnpm install 54 | pnpm build 55 | ``` 56 | 57 | ### 3.Try maests in playground 58 | 59 | ```shell 60 | cd playground 61 | pnpm install 62 | pnpm test 63 | ``` 64 | -------------------------------------------------------------------------------- /docs/public/maests.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoma-mano/maests/b3048690d3ac8f3cfec0869df3770b113b90849b/docs/public/maests.png -------------------------------------------------------------------------------- /fixtures/sample-flow.ts: -------------------------------------------------------------------------------- 1 | import { getOutput, M } from "maests"; 2 | import { openApp } from "@/fixtures/utils/openApp"; 3 | import { someScript } from "./utils/script"; 4 | 5 | // use composable flow easiliy 6 | openApp(); 7 | 8 | // run script like this 9 | M.runScript(someScript); 10 | 11 | // use variables set in someScript 12 | M.assertVisible({ id: getOutput("id") }); 13 | 14 | // use runFlow to run some flow with condition 15 | M.runFlow({ 16 | flow: () => { 17 | M.repeatWhileNotVisible( 18 | { 19 | text: "4", 20 | }, 21 | () => { 22 | M.tapOnText("Increment"); 23 | } 24 | ); 25 | }, 26 | condition: { 27 | visible: "Increment", 28 | }, 29 | }); 30 | -------------------------------------------------------------------------------- /fixtures/utils/hello.ts: -------------------------------------------------------------------------------- 1 | export const hello = () => "Hello, World!"; 2 | -------------------------------------------------------------------------------- /fixtures/utils/nest-script.ts: -------------------------------------------------------------------------------- 1 | export const nestScript = () => { 2 | console.log("nestScript"); 3 | }; 4 | -------------------------------------------------------------------------------- /fixtures/utils/openApp.ts: -------------------------------------------------------------------------------- 1 | import { M } from "maests"; 2 | import { nestScript } from "./nest-script"; 3 | 4 | export const openApp = () => { 5 | M.initFlow({ appId: "com.my.app" }); 6 | M.launchApp({ appId: "com.my.app" }); 7 | M.runScript(nestScript); 8 | }; 9 | -------------------------------------------------------------------------------- /fixtures/utils/script.ts: -------------------------------------------------------------------------------- 1 | import type { APIResult } from "./type"; 2 | import { hello } from "./hello"; 3 | 4 | export const someScript = () => { 5 | // typed http request 6 | const body = http.get("https://jsonplaceholder.typicode.com/todos/1").body; 7 | const result = json(body); 8 | console.log("id " + result.userId); 9 | 10 | // you can use environment variables 11 | console.log(`appId from env: ${process.env.APP_ID}`); 12 | 13 | // you can use imported functions 14 | console.log("imported file " + hello()); 15 | 16 | if (maestro.platform === "android") { 17 | console.log("platform is android"); 18 | } 19 | 20 | // set a variable to output to use in flow 21 | output.id = "com.my.app:id/action_bar_root"; 22 | }; 23 | -------------------------------------------------------------------------------- /fixtures/utils/type.ts: -------------------------------------------------------------------------------- 1 | export type APIResult = { 2 | userId: number; 3 | id: number; 4 | title: string; 5 | completed: boolean; 6 | }; 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "maests", 3 | "version": "2.8.6", 4 | "description": "An executable compiler for creating Maestro's yaml-flows with typescript.", 5 | "type": "module", 6 | "main": "dist/commands/commands.mjs", 7 | "types": "dist/commands/commands.d.ts", 8 | "exports": { 9 | ".": { 10 | "import": "./dist/commands/commands.mjs", 11 | "types": "./dist/commands/commands.d.ts" 12 | }, 13 | "./write-yaml": "./dist/write-yaml.mjs" 14 | }, 15 | "bin": { 16 | "maests": "./dist/main.mjs" 17 | }, 18 | "scripts": { 19 | "build": "npx unbuild && chmod 755 dist/main.mjs", 20 | "release": "pnpm build && changelogen --release --publish --push", 21 | "type-check": "tsc --noEmit", 22 | "test": "vitest run" 23 | }, 24 | "author": "ms2geki@gmail.com", 25 | "devDependencies": { 26 | "@types/node": "^18.14.0", 27 | "changelogen": "^0.5.7", 28 | "unbuild": "^2.0.0", 29 | "vitest": "^2.1.3" 30 | }, 31 | "peerDependencies": { 32 | "consola": "^3" 33 | }, 34 | "repository": { 35 | "type": "git", 36 | "url": "git+https://github.com/shoma-mano/maests.git" 37 | }, 38 | "keywords": [ 39 | "maestro", 40 | "e2e", 41 | "typescript", 42 | "testing", 43 | "React Native" 44 | ], 45 | "bugs": { 46 | "url": "https://github.com/shoma-mano/maests/issues" 47 | }, 48 | "homepage": "https://github.com/shoma-mano/maests#readme", 49 | "license": "MIT", 50 | "dependencies": { 51 | "citty": "^0.1.6", 52 | "consola": "^3.2.3", 53 | "dotenv": "^16.4.5", 54 | "esbuild": "^0.24.0", 55 | "typescript": "^5.5.4", 56 | "get-tsconfig": "^4.8.1", 57 | "jiti": "2.4.0", 58 | "magicast": "^0.3.5", 59 | "yaml": "^2.6.0" 60 | }, 61 | "packageManager": "pnpm@9.1.0+sha512.67f5879916a9293e5cf059c23853d571beaf4f753c707f40cb22bed5fb1578c6aad3b6c4107ccb3ba0b35be003eb621a16471ac836c87beb53f9d54bb4612724", 62 | "pnpm": { 63 | "patchedDependencies": { 64 | "mkdist": "patches/mkdist.patch" 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /patches/mkdist.patch: -------------------------------------------------------------------------------- 1 | diff --git a/dist/index.mjs b/dist/index.mjs 2 | index 59678285189a10c3acd7eddbaeed663ca94f679b..23977afe8662f3289679f5fb36bc5cc61eb826ac 100644 3 | --- a/dist/index.mjs 4 | +++ b/dist/index.mjs 5 | @@ -609,7 +609,7 @@ async function mkdist(options = {}) { 6 | (o) => o.extension === ".mjs" || o.extension === ".js" 7 | )) { 8 | output.contents = output.contents.replace( 9 | - /(import|export)(\s+(?:.+|{[\s\w,]+})\s+from\s+["'])(.*)(["'])/g, 10 | + /(import|export)(\s+(?:.*|{[\s\w,]+})?\s*(?:from)?\s*["'])(.*)(["'])/g, 11 | (_, type, head, id, tail) => type + head + resolveId(output.path, id, esmResolveExtensions) + tail 12 | ).replace( 13 | /import\((["'])(.*)(["'])\)/g, 14 | -------------------------------------------------------------------------------- /playground/.env: -------------------------------------------------------------------------------- 1 | APP_ID=com.my.app -------------------------------------------------------------------------------- /playground/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | android/ 13 | maests/ 14 | 15 | # macOS 16 | .DS_Store 17 | 18 | # @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb 19 | # The following patterns were generated by expo-cli 20 | 21 | expo-env.d.ts 22 | # @end expo-cli -------------------------------------------------------------------------------- /playground/.npmrc: -------------------------------------------------------------------------------- 1 | node-linker=hoisted 2 | -------------------------------------------------------------------------------- /playground/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to your Expo app 👋 2 | 3 | This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app). 4 | 5 | ## Get started 6 | 7 | 1. Install dependencies 8 | 9 | ```bash 10 | npm install 11 | ``` 12 | 13 | 2. Start the app 14 | 15 | ```bash 16 | npx expo start 17 | ``` 18 | 19 | In the output, you'll find options to open the app in a 20 | 21 | - [development build](https://docs.expo.dev/develop/development-builds/introduction/) 22 | - [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/) 23 | - [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/) 24 | - [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo 25 | 26 | You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction). 27 | 28 | ## Get a fresh project 29 | 30 | When you're ready, run: 31 | 32 | ```bash 33 | npm run reset-project 34 | ``` 35 | 36 | This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing. 37 | 38 | ## Learn more 39 | 40 | To learn more about developing your project with Expo, look at the following resources: 41 | 42 | - [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides). 43 | - [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web. 44 | 45 | ## Join the community 46 | 47 | Join our community of developers creating universal apps. 48 | 49 | - [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute. 50 | - [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions. 51 | -------------------------------------------------------------------------------- /playground/app.config.ts: -------------------------------------------------------------------------------- 1 | import type { ConfigContext, ExpoConfig } from "expo/config"; 2 | 3 | export default ({ config }: ConfigContext): ExpoConfig => ({ 4 | name: "playground", 5 | slug: "playground", 6 | version: "1.0.0", 7 | orientation: "portrait", 8 | icon: "./assets/images/icon.png", 9 | scheme: "myapp", 10 | userInterfaceStyle: "automatic", 11 | splash: { 12 | image: "./assets/images/splash.png", 13 | resizeMode: "contain", 14 | backgroundColor: "#ffffff", 15 | }, 16 | ios: { 17 | supportsTablet: true, 18 | }, 19 | android: { 20 | adaptiveIcon: { 21 | foregroundImage: "./assets/images/adaptive-icon.png", 22 | backgroundColor: "#ffffff", 23 | }, 24 | package: "com.my.app", 25 | }, 26 | web: { 27 | bundler: "metro", 28 | output: "static", 29 | favicon: "./assets/images/favicon.png", 30 | }, 31 | plugins: ["expo-router"], 32 | experiments: { 33 | typedRoutes: true, 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /playground/app/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { Stack } from "expo-router"; 2 | 3 | export default function RootLayout() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /playground/app/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Text, View } from "react-native"; 3 | 4 | export default function Index() { 5 | const [count, setCount] = useState(0); 6 | return ( 7 | 14 | Edit app/index.tsx to edit this screen. 15 | {count} 16 | setCount(count + 1)}>Increment 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /playground/assets/fonts/SpaceMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoma-mano/maests/b3048690d3ac8f3cfec0869df3770b113b90849b/playground/assets/fonts/SpaceMono-Regular.ttf -------------------------------------------------------------------------------- /playground/assets/images/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoma-mano/maests/b3048690d3ac8f3cfec0869df3770b113b90849b/playground/assets/images/adaptive-icon.png -------------------------------------------------------------------------------- /playground/assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoma-mano/maests/b3048690d3ac8f3cfec0869df3770b113b90849b/playground/assets/images/favicon.png -------------------------------------------------------------------------------- /playground/assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoma-mano/maests/b3048690d3ac8f3cfec0869df3770b113b90849b/playground/assets/images/icon.png -------------------------------------------------------------------------------- /playground/assets/images/partial-react-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoma-mano/maests/b3048690d3ac8f3cfec0869df3770b113b90849b/playground/assets/images/partial-react-logo.png -------------------------------------------------------------------------------- /playground/assets/images/react-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoma-mano/maests/b3048690d3ac8f3cfec0869df3770b113b90849b/playground/assets/images/react-logo.png -------------------------------------------------------------------------------- /playground/assets/images/react-logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoma-mano/maests/b3048690d3ac8f3cfec0869df3770b113b90849b/playground/assets/images/react-logo@2x.png -------------------------------------------------------------------------------- /playground/assets/images/react-logo@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoma-mano/maests/b3048690d3ac8f3cfec0869df3770b113b90849b/playground/assets/images/react-logo@3x.png -------------------------------------------------------------------------------- /playground/assets/images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoma-mano/maests/b3048690d3ac8f3cfec0869df3770b113b90849b/playground/assets/images/splash.png -------------------------------------------------------------------------------- /playground/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /playground/credentials.json: -------------------------------------------------------------------------------- 1 | { 2 | "android": { 3 | "keystore": { 4 | "keyAlias": "androiddebugkey", 5 | "keyPassword": "android", 6 | "keystorePassword": "android", 7 | "keystorePath": "./debug.keystore" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /playground/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shoma-mano/maests/b3048690d3ac8f3cfec0869df3770b113b90849b/playground/debug.keystore -------------------------------------------------------------------------------- /playground/e2e/sample-flow.ts: -------------------------------------------------------------------------------- 1 | import { getOutput, M } from "maests"; 2 | import { openApp } from "@/e2e/utils/openApp"; 3 | import { someScript } from "./utils/script"; 4 | 5 | // use composable flow easiliy 6 | openApp(); 7 | 8 | // run script like this 9 | M.runScript(someScript); 10 | 11 | // use variables set in someScript 12 | M.assertVisible({ id: getOutput("id") }); 13 | 14 | // use runFlow to run some flow with condition 15 | M.runFlow({ 16 | flow: () => { 17 | M.repeatWhileNotVisible( 18 | { 19 | text: "4", 20 | }, 21 | () => { 22 | M.tapOnText("Increment"); 23 | } 24 | ); 25 | }, 26 | condition: { 27 | visible: "Increment", 28 | }, 29 | }); 30 | -------------------------------------------------------------------------------- /playground/e2e/utils/hello.ts: -------------------------------------------------------------------------------- 1 | export const hello = () => "Hello, World!"; 2 | -------------------------------------------------------------------------------- /playground/e2e/utils/nest-script.ts: -------------------------------------------------------------------------------- 1 | export const nestScript = () => { 2 | console.log("nestScript"); 3 | }; 4 | -------------------------------------------------------------------------------- /playground/e2e/utils/openApp.ts: -------------------------------------------------------------------------------- 1 | import { M } from "maests"; 2 | import { nestScript } from "./nest-script"; 3 | 4 | export const openApp = () => { 5 | M.initFlow({ appId: "com.my.app" }); 6 | M.launchApp({ appId: "com.my.app" }); 7 | M.runScript(nestScript); 8 | }; 9 | -------------------------------------------------------------------------------- /playground/e2e/utils/script.ts: -------------------------------------------------------------------------------- 1 | import type { APIResult } from "./type"; 2 | import { hello } from "./hello"; 3 | 4 | export const someScript = () => { 5 | // typed http request 6 | const body = http.get("https://jsonplaceholder.typicode.com/todos/1").body; 7 | const result = json(body); 8 | console.log("id " + result.userId); 9 | 10 | // you can use environment variables 11 | console.log(`appId from env: ${process.env.APP_ID}`); 12 | 13 | // you can use imported functions 14 | console.log("imported file " + hello()); 15 | 16 | if (maestro.platform === "android") { 17 | console.log("platform is android"); 18 | } 19 | 20 | // set a variable to output to use in flow 21 | output.id = "com.my.app:id/action_bar_root"; 22 | }; 23 | -------------------------------------------------------------------------------- /playground/e2e/utils/type.ts: -------------------------------------------------------------------------------- 1 | export type APIResult = { 2 | userId: number; 3 | id: number; 4 | title: string; 5 | completed: boolean; 6 | }; 7 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground2", 3 | "main": "expo-router/entry", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "test": "npx tsx scripts/android.ts", 7 | "android": "expo run:android", 8 | "ios": "expo run:ios" 9 | }, 10 | "jest": { 11 | "preset": "jest-expo" 12 | }, 13 | "dependencies": { 14 | "@expo/vector-icons": "^14.0.2", 15 | "@react-navigation/native": "^6.0.2", 16 | "eas-cli": "^13.1.0", 17 | "expo": "~51.0.28", 18 | "expo-constants": "~16.0.2", 19 | "expo-font": "~12.0.9", 20 | "expo-linking": "~6.3.1", 21 | "expo-router": "~3.5.23", 22 | "expo-splash-screen": "~0.27.5", 23 | "expo-status-bar": "~1.12.1", 24 | "expo-system-ui": "~3.0.7", 25 | "expo-web-browser": "~13.0.3", 26 | "react": "18.2.0", 27 | "react-dom": "18.2.0", 28 | "react-native": "0.74.5", 29 | "react-native-gesture-handler": "~2.16.1", 30 | "react-native-reanimated": "~3.10.1", 31 | "react-native-safe-area-context": "4.10.5", 32 | "react-native-screens": "3.31.1", 33 | "react-native-web": "~0.19.10", 34 | "tsx": "^4.19.2" 35 | }, 36 | "devDependencies": { 37 | "@babel/core": "^7.20.0", 38 | "maests": "link:..", 39 | "@types/jest": "^29.5.12", 40 | "@types/react": "~18.2.45", 41 | "@types/react-test-renderer": "^18.0.7", 42 | "jest": "^29.2.1", 43 | "jest-expo": "~51.0.3", 44 | "react-test-renderer": "18.2.0", 45 | "typescript": "~5.3.3" 46 | }, 47 | "private": true 48 | } 49 | -------------------------------------------------------------------------------- /playground/scripts/android.ts: -------------------------------------------------------------------------------- 1 | import { execSync, spawn } from "child_process"; 2 | 3 | const test = async () => { 4 | let proc; 5 | if (!process.env.CI) { 6 | const names = getEmulatorNames(); 7 | // use 5584 to avoid conflicts with simulator for development 8 | if (!names.includes("emulator-5584")) { 9 | const avd = getFirstAVD(); 10 | proc = spawn("emulator", ["-avd", avd, "-port", "5584"], { 11 | stdio: "inherit", 12 | }); 13 | } 14 | } 15 | execSync("npx expo prebuild -p android", { 16 | stdio: "inherit", 17 | }); 18 | execSync("./android/gradlew -p ./android assembleRelease", { 19 | stdio: "inherit", 20 | }); 21 | execSync( 22 | "adb -s emulator-5584 install -r ./android/app/build/outputs/apk/release/app-release.apk", 23 | { stdio: "inherit" } 24 | ); 25 | execSync("npx maests e2e/sample-flow.ts", { 26 | stdio: "inherit", 27 | }); 28 | if (proc) proc.kill(); 29 | }; 30 | test(); 31 | 32 | function getBootedDevices() { 33 | const booted = execSync("adb devices -l") 34 | .toString() 35 | .split("\n") 36 | .slice(1) 37 | .filter((line) => line.includes("device")); 38 | 39 | return booted; 40 | } 41 | 42 | function getFirstAVD() { 43 | return execSync("emulator -list-avds") 44 | .toString() 45 | .split("\n") 46 | .filter((line) => line && !line.includes("INFO"))[0]; 47 | } 48 | 49 | function getEmulatorNames() { 50 | const booted = getBootedDevices(); 51 | return booted.map((line) => { 52 | console.log("line:", line); 53 | line = line.trim(); 54 | 55 | if (line.includes("device:panther")) { 56 | return line.split(" ")[0].trim(); 57 | } 58 | 59 | const emulatorMatch = line.match(/^(emulator-\d+)/); 60 | return emulatorMatch ? emulatorMatch[1] : ""; 61 | }); 62 | } 63 | -------------------------------------------------------------------------------- /playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true, 5 | "paths": { 6 | "@/*": ["./*"] 7 | } 8 | }, 9 | "include": [ 10 | "**/*.ts", 11 | "**/*.tsx", 12 | ".expo/types/**/*.ts", 13 | "expo-env.d.ts", 14 | "test.js", 15 | "e2e/sample-flow.mts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /prettier.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports={} -------------------------------------------------------------------------------- /src/commands/addMedia.ts: -------------------------------------------------------------------------------- 1 | import { stringify } from "yaml"; 2 | import { addOut, getOut } from "../out"; 3 | 4 | // List of MIME types supported by Maestro for adding media to the device's gallery 5 | const allowedMimeTypes = [ 6 | "image/png", 7 | "image/jpeg", 8 | "image/jpg", 9 | "image/gif", 10 | "video/mp4", 11 | ] as const; 12 | type AllowedMimeTypes = (typeof allowedMimeTypes)[number]; 13 | 14 | /** 15 | * Adds a media file to the device's gallery, making it accessible for use in application flows. 16 | * Supports both Android and iOS platforms. 17 | * 18 | * @param {string} mimeType - The MIME type of the media file to add. 19 | * Must be one of the following supported types: 20 | * "image/png", "image/jpeg", "image/jpg", "image/gif", "video/mp4". 21 | * @param {string} filePath - The relative path to the media file. 22 | * 23 | * @throws {Error} Throws an error if the provided MIME type is not in the allowed list. 24 | * 25 | * @example 26 | * addMedia("image/png", "../assets/test.png"); // Adds a PNG file located at the specified path 27 | */ 28 | export const addMedia = (mimeType: AllowedMimeTypes, filePath: string) => { 29 | // Validate if the provided MIME type is in the list of allowed types 30 | if (!allowedMimeTypes.includes(mimeType)) { 31 | throw new Error( 32 | `Unsupported MIME type. Allowed types are: ${allowedMimeTypes.join(", ")}` 33 | ); 34 | } 35 | 36 | // Command structure for adding media to the gallery 37 | const commands = [ 38 | { 39 | addMedia: [`${filePath}`], 40 | }, 41 | ]; 42 | addOut(stringify(commands)); 43 | }; 44 | 45 | // Tests for the addMedia function using Vitest 46 | if (import.meta.vitest) { 47 | it("should match snapshot for addMedia with a valid mimeType and filePath", () => { 48 | addMedia("image/png", "../assets/test.png"); 49 | expect(getOut()).toMatchInlineSnapshot(` 50 | "- addMedia: 51 | - ../assets/test.png 52 | " 53 | `); 54 | }); 55 | 56 | it("should throw an error for an unsupported mimeType", () => { 57 | expect(() => 58 | // @ts-expect-error 59 | addMedia("application/pdf", "../assets/test.pdf") 60 | ).toThrowError( 61 | "Unsupported MIME type. Allowed types are: image/png, image/jpeg, image/jpg, image/gif, video/mp4" 62 | ); 63 | }); 64 | } 65 | -------------------------------------------------------------------------------- /src/commands/assert.ts: -------------------------------------------------------------------------------- 1 | import { stringify } from "yaml"; 2 | import { addOut, getOut } from "../out"; 3 | 4 | type AssertProps = { 5 | text?: string; 6 | id?: string; 7 | enabled?: boolean; 8 | checked?: boolean; 9 | focused?: boolean; 10 | selected?: boolean; 11 | }; 12 | 13 | export const assertVisible = (props: AssertProps) => { 14 | const commands = [{ assertVisible: { ...props } }]; 15 | addOut(stringify(commands)); 16 | }; 17 | 18 | if (import.meta.vitest) { 19 | it("assertVisible", () => { 20 | assertVisible({ id: "com.android.systemui:id/battery" }); 21 | expect(getOut()).toMatchInlineSnapshot(` 22 | "- assertVisible: 23 | id: com.android.systemui:id/battery 24 | " 25 | `); 26 | }); 27 | } 28 | 29 | export const assertNotVisible = (props: AssertProps) => { 30 | const commands = [{ assertNotVisible: { ...props } }]; 31 | addOut(stringify(commands)); 32 | }; 33 | 34 | if (import.meta.vitest) { 35 | it("assertNotVisible", () => { 36 | assertNotVisible({ id: "com.android.systemui:id/battery" }); 37 | expect(getOut()).toMatchInlineSnapshot(` 38 | "- assertNotVisible: 39 | id: com.android.systemui:id/battery 40 | " 41 | `); 42 | }); 43 | } 44 | 45 | export const assertTrue = (condition: string) => { 46 | addOut(`- assertTrue: ${condition}\n`); 47 | }; 48 | -------------------------------------------------------------------------------- /src/commands/commands.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PointProps, 3 | tapOn, 4 | tapOnPoint, 5 | tapOnText, 6 | waitForAndTapOn, 7 | } from "./tap"; 8 | import { runFlow, runScript } from "./run"; 9 | import { clearState, initFlow, launchApp } from "./init"; 10 | import { repeat, repeatWhileVisible, repeatWhileNotVisible } from "./repeat"; 11 | import { assertNotVisible, assertTrue, assertVisible } from "./assert"; 12 | import { addMedia } from "./addMedia"; 13 | import { addOut } from "../out"; 14 | import { 15 | copyTextFrom, 16 | eraseText, 17 | inputRandomEmail, 18 | inputRandomName, 19 | inputRandomNumber, 20 | inputRandomText, 21 | inputText, 22 | } from "./text"; 23 | import { swipeDown, swipeLeft, swipeRight, swipeUp } from "./swipe"; 24 | import { 25 | wait, 26 | waitForAnimationEnd, 27 | waitUntilNotVisible, 28 | waitUntilVisible, 29 | } from "./wait"; 30 | import { platform } from "os"; 31 | 32 | // Main translator functions 33 | const MaestroTranslators = { 34 | /** 35 | * Initializes the test flow with optional configuration. 36 | * @param config - Optional configuration with appId and other environment variables. 37 | */ 38 | initFlow, 39 | /** 40 | * Launches the application with optional configuration settings. 41 | * @param config - Configuration options for appId, state clearing, keychain clearing, and app stopping. 42 | */ 43 | launchApp, 44 | /** 45 | * Clears the state of the current app or the specified app by appId. 46 | * @param appId - Optional appId to clear state for a specific app. 47 | */ 48 | clearState, 49 | 50 | runScript, 51 | 52 | /** 53 | * Runs a sub-flow defined by a path with optional environment variables. 54 | * @param path - The path to the sub-flow. 55 | * @param env - Optional map of environment variables for the sub-flow. 56 | */ 57 | runFlow, 58 | /** 59 | * Taps on an element specified by a testId. 60 | * @param id - The testId of the target element. 61 | * @param props - Optional tap properties for customized tap behavior. 62 | */ 63 | tapOn, 64 | /** 65 | * Taps on a visible text element on the screen. 66 | * @param text - The text to tap on. 67 | * @param props - Optional tap properties such as retries, repeat count, and timeout. 68 | */ 69 | tapOnText, 70 | /** 71 | * Taps on a specific point on the screen. 72 | * @param point - Coordinates to tap, can use numbers (dips) or strings (percentages). 73 | * @param props - Optional tap properties. 74 | */ 75 | tapOnPoint, 76 | /** 77 | * Waits for an element by testId to appear, then taps on it. 78 | * @param id - Required: The testId of the element to wait for and tap. 79 | * @param props - Properties for wait and tap actions, combining both WaitProps and TapProps. 80 | */ 81 | waitForAndTapOn, 82 | /** 83 | * Clears the entire keychain. 84 | */ 85 | clearKeychain: () => { 86 | addOut("- clearKeychain\n"); 87 | }, 88 | /** 89 | * Performs a long press on an element identified by its testId. 90 | * @param id - The testId of the element to long press. 91 | */ 92 | longPressOn: (id: string) => { 93 | addOut(`- longPressOn:\n id: "${id}"\n`); 94 | }, 95 | /** 96 | * Performs a long press on a specified point. 97 | * @param point - The x and y coordinates to long press. 98 | */ 99 | longPressOnPoint: (pointProps: PointProps) => { 100 | addOut(`- longPressOn:\n point: ${pointProps.x},${pointProps.y}\n`); 101 | }, 102 | /** 103 | * Performs a long press on a text element visible on the screen. 104 | * @param text - The visible text to long press. 105 | */ 106 | longPressOnText: (text: string) => { 107 | addOut(`- longPressOn: ${text}\n`); 108 | }, 109 | 110 | /** 111 | * Swipes left from the screen center. 112 | */ 113 | swipeLeft, 114 | /** 115 | * Swipes right from the screen center. 116 | */ 117 | swipeRight, 118 | /** 119 | * Swipes down from the screen center. 120 | */ 121 | swipeDown, 122 | /** 123 | * Swipes up from the screen center. 124 | */ 125 | swipeUp, 126 | 127 | /** 128 | * Swipes from a specified start point to an end point. 129 | * @param start - Starting coordinates for the swipe. 130 | * @param end - Ending coordinates for the swipe. 131 | */ 132 | swipe: (start: PointProps, end: PointProps) => { 133 | addOut( 134 | `- swipe:\n start: ${start.x}, ${start.y}\n end: ${end.x}, ${end.y}\n` 135 | ); 136 | }, 137 | 138 | /** 139 | * Inputs text into the focused or specified input element. 140 | * @param text - The text to input. 141 | * @param id - Optional testId of the input element. 142 | */ 143 | inputText, 144 | 145 | /** 146 | * Inputs a random name into the focused or specified input element. 147 | * @param id - Optional testId of the input element. 148 | */ 149 | inputRandomName, 150 | 151 | /** 152 | * Inputs a random number into the focused or specified input element. 153 | * @param id - Optional testId of the input element. 154 | */ 155 | inputRandomNumber, 156 | 157 | /** 158 | * Copies text from an element identified by its testId. 159 | * @param id - The testId of the element to copy text from. 160 | */ 161 | copyTextFrom, 162 | 163 | /** 164 | * Inputs a random email address into the focused or specified input element. 165 | * @param id - Optional testId of the input element. 166 | */ 167 | inputRandomEmail, 168 | 169 | /** 170 | * Inputs random text into the focused or specified input element. 171 | * @param id - Optional testId of the input element. 172 | */ 173 | inputRandomText, 174 | 175 | /** 176 | * Erases a specified number of characters from the focused or specified input element. 177 | * @param chars - Number of characters to erase. 178 | * @param id - Optional testId of the input element. 179 | */ 180 | eraseText, 181 | 182 | /** 183 | * Opens a specified URL or deep link. 184 | * @param url - The URL or deep link to open. 185 | */ 186 | openLink: (url: string) => { 187 | addOut(`- openLink: ${url}\n`); 188 | }, 189 | 190 | /** 191 | * Navigates to a specified path using the deep link base. 192 | * @param path - The path to navigate to. 193 | */ 194 | navigate: (path: string) => { 195 | addOut(`- openLink: ${process.env["deepLinkBase"]}${path}\n`); 196 | }, 197 | 198 | /** 199 | * Asserts that an element with the given testId is visible. 200 | * @param id - The testId of the element to check. 201 | * @param enabled - If true, checks that the element is both visible and enabled. 202 | */ 203 | assertVisible, 204 | 205 | /** 206 | * Asserts that an element with the given testId is not visible. 207 | * @param id - The testId of the element to check. 208 | */ 209 | assertNotVisible, 210 | 211 | /** 212 | * Asserts that a specified condition is true. 213 | * @param condition - The condition to assert. 214 | */ 215 | assertTrue, 216 | 217 | /** 218 | * Scrolls down on the screen. 219 | */ 220 | scroll: () => { 221 | addOut("- scroll\n"); 222 | }, 223 | 224 | /** 225 | * Scrolls until an element with the given testId is visible. 226 | * @param id - The testId of the element to scroll until visible. 227 | */ 228 | scrollUntilVisible: (id: string) => { 229 | addOut(`- scrollUntilVisible:\n element:\n id: "${id}"\n`); 230 | }, 231 | 232 | /** 233 | * Waits until an ongoing animation or video ends. 234 | * @param maxWait - Optional timeout (in milliseconds) to wait before proceeding. 235 | */ 236 | waitForAnimationEnd, 237 | 238 | /** 239 | * Waits until an element with the given testId is visible. 240 | * @param id - The testId of the element to wait for. 241 | * @param maxWait - Maximum wait time (in milliseconds) for the element to appear. 242 | */ 243 | waitUntilVisible, 244 | 245 | /** 246 | * Waits until an element with the given testId is no longer visible. 247 | * @param id - The testId of the element to wait for. 248 | * @param maxWait - Maximum wait time (in milliseconds) for the element to disappear. 249 | */ 250 | waitUntilNotVisible, 251 | 252 | /** 253 | * Waits for a specified number of milliseconds. 254 | * @param ms - The number of milliseconds to wait. 255 | */ 256 | wait, 257 | 258 | /** 259 | * Dismisses the software keyboard. 260 | */ 261 | hideKeyboard: () => { 262 | addOut("- hideKeyboard\n"); 263 | }, 264 | 265 | /** 266 | * Takes a screenshot and stores it with the specified filename. 267 | * @param fileName - The name to save the screenshot as. 268 | */ 269 | screenshot: (fileName: string) => { 270 | addOut(`- takeScreenshot: ${fileName}\n`); 271 | }, 272 | 273 | /** 274 | * Presses the enter key on the software keyboard. 275 | */ 276 | pressEnter: () => { 277 | addOut("- pressKey: Enter\n"); 278 | }, 279 | 280 | /** 281 | * Presses the home button on the device. 282 | */ 283 | pressHomeButton: () => { 284 | addOut("- pressKey: Home\n"); 285 | }, 286 | 287 | /** 288 | * Presses the lock button on the device. 289 | */ 290 | pressLockButton: () => { 291 | addOut("- pressKey: Lock\n"); 292 | }, 293 | 294 | /** 295 | * Presses the back button on Android devices. 296 | */ 297 | back: () => { 298 | addOut("- pressKey: back\n"); 299 | }, 300 | 301 | /** 302 | * Decreases the device volume. 303 | */ 304 | volumeDown: () => { 305 | addOut("- pressKey: volume down\n"); 306 | }, 307 | 308 | /** 309 | * Increases the device volume. 310 | */ 311 | volumeUp: () => { 312 | addOut("- pressKey: volume up\n"); 313 | }, 314 | 315 | /** 316 | * Stops the current app or the specified app by appId. 317 | * @param appId - Optional appId to specify which app to stop. 318 | */ 319 | stopApp: ({ appId }: { appId?: string } = {}) => { 320 | addOut(appId ? `- stopApp: ${appId}\n` : "- stopApp\n"); 321 | }, 322 | 323 | /** 324 | * Repeats a set of actions a specified number of times. 325 | * @param props - The properties of the repeat command. 326 | * @param func - The function containing actions to repeat. 327 | */ 328 | repeat, 329 | 330 | /** 331 | * Repeats a set of actions while an element is visible. 332 | * @param matcher - The element matcher to repeat while visible. 333 | * @param func - The function containing actions to repeat. 334 | */ 335 | repeatWhileVisible, 336 | 337 | /** 338 | * Repeats a set of actions while an element is not visible. 339 | * @param matcher - The element matcher to repeat while not visible. 340 | * @param func - The function containing actions to repeat. 341 | */ 342 | repeatWhileNotVisible, 343 | 344 | addMedia, 345 | }; 346 | 347 | export { MaestroTranslators as M }; 348 | 349 | // utils for user 350 | export const getOutput = (key: string) => "${output." + key + "}"; 351 | 352 | // runScript Types 353 | declare global { 354 | namespace http { 355 | const get: (...args: any) => { body: string }; 356 | } 357 | 358 | const json: (str: string) => T; 359 | const output: Record; 360 | 361 | namespace maestro { 362 | const platform: "ios" | "android"; 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /src/commands/init.ts: -------------------------------------------------------------------------------- 1 | import { handleNest, addOut, getOut } from "../out"; 2 | 3 | const envAppId = process.env["appId"]; 4 | 5 | export const initFlow = ({ 6 | appId, 7 | onFlowStart, 8 | }: { appId?: string; onFlowStart?: () => void } = {}) => { 9 | const appIdCommand = `appId: ${appId ?? envAppId}\n`; 10 | let commands = appIdCommand; 11 | if (onFlowStart) { 12 | const nested = handleNest(onFlowStart); 13 | const flowCommand = `onFlowStart:\n${nested.replaceAll(/\n/g, ` \n`)}`; 14 | commands += flowCommand; 15 | } 16 | const separator = "---\n"; 17 | commands += separator; 18 | addOut(commands); 19 | }; 20 | 21 | if (import.meta.vitest) { 22 | it("initFlow with appId", () => { 23 | initFlow({ appId: "testAppId" }); 24 | expect(getOut()).toMatchInlineSnapshot(` 25 | "appId: testAppId 26 | --- 27 | " 28 | `); 29 | }); 30 | } 31 | 32 | export const launchApp = ({ appId }: { appId?: string } = {}) => { 33 | addOut(`- launchApp:\n appId: "${appId ?? envAppId}"\n`); 34 | }; 35 | 36 | if (import.meta.vitest) { 37 | it("launchApp with appId", () => { 38 | launchApp({ appId: "testAppId" }); 39 | expect(getOut()).toMatchInlineSnapshot(` 40 | "- launchApp: 41 | appId: "testAppId" 42 | " 43 | `); 44 | }); 45 | } 46 | 47 | export const clearState = ({ appId }: { appId?: string } = {}) => { 48 | addOut(appId ? `- clearState: ${appId}\n` : "- clearState\n"); 49 | }; 50 | 51 | if (import.meta.vitest) { 52 | it("clearState with appId", () => { 53 | clearState({ appId: "testAppId" }); 54 | expect(getOut()).toMatchInlineSnapshot(` 55 | "- clearState: testAppId 56 | " 57 | `); 58 | }); 59 | } 60 | -------------------------------------------------------------------------------- /src/commands/repeat.ts: -------------------------------------------------------------------------------- 1 | import { stringify } from "yaml"; 2 | import { handleNest, addOut, getOut } from "../out"; 3 | import { M } from "./commands"; 4 | import { ElementMatcher } from "./type"; 5 | 6 | type RepeatProps = { 7 | times?: number; 8 | while?: { visible?: ElementMatcher; notVisible?: ElementMatcher }; 9 | }; 10 | 11 | export const repeat = (props: RepeatProps, func: () => any) => { 12 | const out = handleNest(func, true); 13 | const cmd = [{ repeat: { ...props, commands: out } }]; 14 | addOut(stringify(cmd)); 15 | }; 16 | 17 | if (import.meta.vitest) { 18 | it("double nested repeat", () => { 19 | repeat( 20 | { 21 | times: 3, 22 | while: { 23 | visible: { 24 | text: "test", 25 | }, 26 | }, 27 | }, 28 | () => { 29 | M.tapOn("test"); 30 | M.tapOn("test"); 31 | repeat({ times: 3 }, () => { 32 | M.tapOn("test"); 33 | }); 34 | } 35 | ); 36 | 37 | expect(getOut()).toMatchInlineSnapshot(` 38 | "- repeat: 39 | times: 3 40 | while: 41 | visible: 42 | text: test 43 | commands: 44 | - tapOn: 45 | id: test 46 | retryTapIfNoChange: true 47 | - tapOn: 48 | id: test 49 | retryTapIfNoChange: true 50 | - repeat: 51 | times: 3 52 | commands: 53 | - tapOn: 54 | id: test 55 | retryTapIfNoChange: true 56 | " 57 | `); 58 | }); 59 | } 60 | 61 | export const repeatWhileVisible = ( 62 | matcher: ElementMatcher, 63 | func: () => any 64 | ) => { 65 | const out = handleNest(func, true); 66 | 67 | const cmd = [ 68 | { repeat: { times: 10, while: { visible: matcher }, commands: out } }, 69 | ]; 70 | addOut(stringify(cmd)); 71 | }; 72 | 73 | if (import.meta.vitest) { 74 | it("repeatWhileVisible", () => { 75 | repeatWhileVisible( 76 | { 77 | text: "test", 78 | }, 79 | () => { 80 | M.tapOn("test"); 81 | } 82 | ); 83 | 84 | expect(getOut()).toMatchInlineSnapshot(` 85 | "- repeat: 86 | times: 10 87 | while: 88 | visible: 89 | text: test 90 | commands: 91 | - tapOn: 92 | id: test 93 | retryTapIfNoChange: true 94 | " 95 | `); 96 | }); 97 | } 98 | 99 | export const repeatWhileNotVisible = ( 100 | matcher: ElementMatcher, 101 | func: () => any 102 | ) => { 103 | const out = handleNest(func, true); 104 | const cmd = [ 105 | { repeat: { times: 10, while: { notVisible: matcher }, commands: out } }, 106 | ]; 107 | addOut(stringify(cmd)); 108 | }; 109 | -------------------------------------------------------------------------------- /src/commands/run.ts: -------------------------------------------------------------------------------- 1 | import { buildSync } from "esbuild"; 2 | import { join } from "path"; 3 | import { stringify } from "yaml"; 4 | import { M } from "./commands"; 5 | import { addOut, getOut, handleNest } from "../out"; 6 | import { maestsDir, tsConfigDir, writeFileWithDirectorySync } from "../utils"; 7 | import { readFileSync, unlinkSync } from "fs"; 8 | import { WhenCondition } from "./type"; 9 | import { deleteExport } from "../rewrite-code"; 10 | 11 | const createScriptOutPath = (scriptFullPath: string) => { 12 | const scriptPath = scriptFullPath.replace(`${tsConfigDir}/`, ""); 13 | return join(maestsDir, scriptPath.replace(".ts", ".js")); 14 | }; 15 | 16 | export const runScript = (path: string | (() => void), funcName?: string) => { 17 | if (typeof path === "function") return; 18 | const scriptPath = createScriptOutPath(path); 19 | 20 | // add command that references scriptPath 21 | const command = `- runScript: ${scriptPath}\n`; 22 | addOut(command); 23 | 24 | // write script file to scriptPath 25 | const { outputFiles } = buildSync({ 26 | entryPoints: [path], 27 | bundle: true, 28 | format: "esm", 29 | sourcemap: false, 30 | legalComments: "none", 31 | write: false, 32 | }); 33 | let code = outputFiles[0].text; 34 | code = code.replace(/(?:\${)?process\.env\.([^\n\s}]*)}?/g, (_, p1) => { 35 | return process.env[p1] || ""; 36 | }); 37 | code = deleteExport(code); 38 | code += `\n${funcName ? `${funcName}();` : ""}`; 39 | writeFileWithDirectorySync(scriptPath, code); 40 | }; 41 | 42 | if (import.meta.vitest) { 43 | it("runScript", () => { 44 | const tsScriptPath = join( 45 | __dirname, 46 | "../../playground/e2e/utils/script.ts" 47 | ); 48 | runScript(tsScriptPath, "someScript"); 49 | const scriptPath = createScriptOutPath(tsScriptPath); 50 | expect(getOut()).toMatchInlineSnapshot(` 51 | "- runScript: ${scriptPath} 52 | " 53 | `); 54 | const code = readFileSync(scriptPath, "utf-8"); 55 | expect(code).toMatchInlineSnapshot(` 56 | "// playground/e2e/utils/hello.ts 57 | var hello = () => "Hello, World!"; 58 | // playground/e2e/utils/script.ts 59 | var someScript = () => { 60 | const body = http.get("https://jsonplaceholder.typicode.com/todos/1").body; 61 | const result = json(body); 62 | console.log("id " + result.userId); 63 | console.log(\`appId from env: \`); 64 | console.log("imported file " + hello()); 65 | if (maestro.platform === "android") { 66 | console.log("platform is android"); 67 | } 68 | output.id = "com.my.app:id/action_bar_root"; 69 | }; 70 | 71 | someScript();" 72 | `); 73 | unlinkSync(scriptPath); 74 | }); 75 | } 76 | 77 | export const runFlow = ({ 78 | flow, 79 | condition, 80 | }: { 81 | flow: () => void; 82 | condition?: WhenCondition; 83 | }) => { 84 | const out = handleNest(flow, true); 85 | const result = stringify([{ runFlow: { when: condition, commands: out } }]); 86 | addOut(result); 87 | }; 88 | 89 | if (import.meta.vitest) { 90 | it("runFlow", () => { 91 | runFlow({ 92 | flow: () => { 93 | M.tapOn("elementId"); 94 | M.repeat({ times: 3 }, () => { 95 | M.tapOn("elementId"); 96 | }); 97 | }, 98 | condition: { 99 | notVisible: "elementId", 100 | }, 101 | }); 102 | expect(getOut()).toMatchInlineSnapshot(` 103 | "- runFlow: 104 | when: 105 | notVisible: elementId 106 | commands: 107 | - tapOn: 108 | id: elementId 109 | retryTapIfNoChange: true 110 | - repeat: 111 | times: 3 112 | commands: 113 | - tapOn: 114 | id: elementId 115 | retryTapIfNoChange: true 116 | " 117 | `); 118 | }); 119 | } 120 | -------------------------------------------------------------------------------- /src/commands/swipe.ts: -------------------------------------------------------------------------------- 1 | import { addOut, getOut } from "../out"; 2 | 3 | /** 4 | * Swipes left from the screen center. 5 | */ 6 | export const swipeLeft = () => 7 | addOut("- swipe:\n direction: LEFT\n duration: 400\n"); 8 | 9 | if (import.meta.vitest) { 10 | it("swipeLeft", () => { 11 | swipeLeft(); 12 | expect(getOut()).toMatchInlineSnapshot(` 13 | "- swipe: 14 | direction: LEFT 15 | duration: 400 16 | " 17 | `); 18 | }); 19 | } 20 | 21 | /** 22 | * Swipes right from the screen center. 23 | */ 24 | export const swipeRight = () => 25 | addOut("- swipe:\n direction: RIGHT\n duration: 400\n"); 26 | 27 | if (import.meta.vitest) { 28 | it("swipeRight", () => { 29 | swipeRight(); 30 | expect(getOut()).toMatchInlineSnapshot(` 31 | "- swipe: 32 | direction: RIGHT 33 | duration: 400 34 | " 35 | `); 36 | }); 37 | } 38 | 39 | /** 40 | * Swipes down from the screen center. 41 | */ 42 | export const swipeDown = () => 43 | addOut("- swipe:\n direction: DOWN\n duration: 400\n"); 44 | 45 | if (import.meta.vitest) { 46 | it("swipeDown", () => { 47 | swipeDown(); 48 | expect(getOut()).toMatchInlineSnapshot(` 49 | "- swipe: 50 | direction: DOWN 51 | duration: 400 52 | " 53 | `); 54 | }); 55 | } 56 | 57 | /** 58 | * Swipes up from the screen center. 59 | */ 60 | export const swipeUp = () => 61 | addOut("- swipe:\n direction: UP\n duration: 400\n"); 62 | 63 | if (import.meta.vitest) { 64 | it("swipeUp", () => { 65 | swipeUp(); 66 | expect(getOut()).toMatchInlineSnapshot(` 67 | "- swipe: 68 | direction: UP 69 | duration: 400 70 | " 71 | `); 72 | }); 73 | } 74 | -------------------------------------------------------------------------------- /src/commands/tap.ts: -------------------------------------------------------------------------------- 1 | import { stringify } from "yaml"; 2 | import { addOut, getOut } from "../out"; 3 | 4 | // Helper function to format optional tap properties 5 | export interface TapOptions { 6 | index?: number; 7 | retryTapIfNoChange?: boolean; 8 | repeat?: number; 9 | waitToSettleTimeoutMs?: number; 10 | } 11 | 12 | export interface WaitProps { 13 | maxWait?: number; 14 | } 15 | 16 | export interface PointProps { 17 | x: number | string; 18 | y: number | string; 19 | } 20 | 21 | export const tapOn = (id: string, options: TapOptions = {}) => { 22 | const commands = [ 23 | { 24 | tapOn: { 25 | id, 26 | retryTapIfNoChange: true, 27 | ...options, 28 | }, 29 | }, 30 | ]; 31 | addOut(stringify(commands)); 32 | }; 33 | 34 | if (import.meta.vitest) { 35 | it("should match snapshot for basic tapOn", () => { 36 | tapOn("elementId"); 37 | expect(getOut()).toMatchInlineSnapshot(` 38 | "- tapOn: 39 | id: elementId 40 | retryTapIfNoChange: true 41 | " 42 | `); 43 | 44 | tapOn("elementId With Options", { 45 | index: 1, 46 | retryTapIfNoChange: false, 47 | repeat: 2, 48 | waitToSettleTimeoutMs: 1000, 49 | }); 50 | expect(getOut()).toMatchInlineSnapshot(` 51 | "- tapOn: 52 | id: elementId With Options 53 | retryTapIfNoChange: false 54 | index: 1 55 | repeat: 2 56 | waitToSettleTimeoutMs: 1000 57 | " 58 | `); 59 | }); 60 | } 61 | 62 | export const tapOnText = (text: string, options: TapOptions = {}) => { 63 | const command = [ 64 | { 65 | tapOn: { 66 | text, 67 | retryTapIfNoChange: true, 68 | ...options, 69 | }, 70 | }, 71 | ]; 72 | addOut(stringify(command)); 73 | }; 74 | 75 | if (import.meta.vitest) { 76 | it("should match snapshot for basic tapOnText", () => { 77 | tapOnText("SampleText"); 78 | expect(getOut()).toMatchInlineSnapshot(` 79 | "- tapOn: 80 | text: SampleText 81 | retryTapIfNoChange: true 82 | " 83 | `); 84 | }); 85 | } 86 | 87 | export const tapOnPoint = (point: PointProps, options: TapOptions = {}) => { 88 | const { x, y } = point; 89 | const command = [ 90 | { 91 | tapOn: { 92 | point: `${x},${y}`, 93 | retryTapIfNoChange: true, 94 | ...options, 95 | }, 96 | }, 97 | ]; 98 | addOut(stringify(command)); 99 | }; 100 | 101 | if (import.meta.vitest) { 102 | it("should match snapshot for basic tapOnPoint", () => { 103 | tapOnPoint({ x: 100, y: 200 }); 104 | expect(getOut()).toMatchInlineSnapshot(` 105 | "- tapOn: 106 | point: 100,200 107 | retryTapIfNoChange: true 108 | " 109 | `); 110 | }); 111 | } 112 | 113 | export const waitForAndTapOn = ( 114 | id: string, 115 | options: TapOptions & WaitProps = {} 116 | ) => { 117 | const { maxWait = 5000 } = options; 118 | let command = `- extendedWaitUntil:\n visible:\n id: "${id}"\n timeout: ${maxWait}\n`; 119 | const cmd = { 120 | extendedWaitUntil: { 121 | visible: { 122 | id: id, 123 | }, 124 | timeout: maxWait, 125 | }, 126 | }; 127 | 128 | const cmd2 = { 129 | tapOn: { 130 | id, 131 | retryTapIfNoChange: true, 132 | ...options, 133 | }, 134 | }; 135 | 136 | addOut(stringify([cmd, cmd2])); 137 | }; 138 | 139 | if (import.meta.vitest) { 140 | it("should match snapshot for basic waitForAndTapOn", () => { 141 | waitForAndTapOn("elementId With Options", { 142 | index: 1, 143 | retryTapIfNoChange: false, 144 | repeat: 2, 145 | waitToSettleTimeoutMs: 1000, 146 | maxWait: 10000, 147 | }); 148 | expect(getOut()).toMatchInlineSnapshot(` 149 | "- extendedWaitUntil: 150 | visible: 151 | id: elementId With Options 152 | timeout: 10000 153 | - tapOn: 154 | id: elementId With Options 155 | retryTapIfNoChange: false 156 | index: 1 157 | repeat: 2 158 | waitToSettleTimeoutMs: 1000 159 | maxWait: 10000 160 | " 161 | `); 162 | }); 163 | } 164 | -------------------------------------------------------------------------------- /src/commands/text.ts: -------------------------------------------------------------------------------- 1 | import { addOut, getOut } from "../out"; 2 | 3 | export const inputText = (text: string, id?: string) => { 4 | addOut( 5 | id 6 | ? `- tapOn:\n id: "${id}"\n- inputText: ${text}\n` 7 | : `- inputText: ${text}\n` 8 | ); 9 | }; 10 | 11 | if (import.meta.vitest) { 12 | it("inputText in focused element", () => { 13 | inputText("focused text"); 14 | expect(getOut()).toMatchInlineSnapshot(` 15 | "- inputText: focused text 16 | " 17 | `); 18 | }); 19 | } 20 | 21 | export const inputRandomName = (id?: string) => { 22 | addOut( 23 | id 24 | ? `- tapOn:\n id: "${id}"\n- inputRandomPersonName\n` 25 | : `- inputRandomPersonName\n` 26 | ); 27 | }; 28 | 29 | if (import.meta.vitest) { 30 | it("inputRandomName in focused element", () => { 31 | inputRandomName(); 32 | expect(getOut()).toMatchInlineSnapshot(` 33 | "- inputRandomPersonName 34 | " 35 | `); 36 | }); 37 | } 38 | 39 | export const inputRandomNumber = (id?: string) => { 40 | addOut( 41 | id 42 | ? `- tapOn:\n id: "${id}"\n- inputRandomNumber\n` 43 | : `- inputRandomNumber\n` 44 | ); 45 | }; 46 | 47 | if (import.meta.vitest) { 48 | it("inputRandomNumber in focused element", () => { 49 | inputRandomNumber(); 50 | expect(getOut()).toMatchInlineSnapshot(` 51 | "- inputRandomNumber 52 | " 53 | `); 54 | }); 55 | } 56 | 57 | export const copyTextFrom = (id: string) => { 58 | addOut(`- copyTextFrom:\n id: "${id}"\n`); 59 | }; 60 | 61 | if (import.meta.vitest) { 62 | it("copyTextFrom", () => { 63 | copyTextFrom("com.android.systemui:id/battery"); 64 | expect(getOut()).toMatchInlineSnapshot(` 65 | "- copyTextFrom: 66 | id: "com.android.systemui:id/battery" 67 | " 68 | `); 69 | }); 70 | } 71 | 72 | export const inputRandomEmail = (id?: string) => { 73 | addOut( 74 | id 75 | ? `- tapOn:\n id: "${id}"\n- inputRandomEmail\n` 76 | : `- inputRandomEmail\n` 77 | ); 78 | }; 79 | 80 | if (import.meta.vitest) { 81 | it("inputRandomEmail in focused element", () => { 82 | inputRandomEmail(); 83 | expect(getOut()).toMatchInlineSnapshot(` 84 | "- inputRandomEmail 85 | " 86 | `); 87 | }); 88 | } 89 | 90 | export const inputRandomText = (id?: string) => { 91 | addOut( 92 | id 93 | ? `- tapOn:\n id: "${id}"\n- inputRandomText\n` 94 | : `- inputRandomText\n` 95 | ); 96 | }; 97 | 98 | if (import.meta.vitest) { 99 | it("inputRandomText in focused element", () => { 100 | inputRandomText(); 101 | expect(getOut()).toMatchInlineSnapshot(` 102 | "- inputRandomText 103 | " 104 | `); 105 | }); 106 | } 107 | 108 | export const eraseText = (chars: number, id?: string) => { 109 | addOut( 110 | id 111 | ? `- tapOn:\n id: "${id}"\n- eraseText: ${chars ?? 50}\n` 112 | : `- eraseText: ${chars ?? 50}\n` 113 | ); 114 | }; 115 | 116 | if (import.meta.vitest) { 117 | it("eraseText in focused element", () => { 118 | eraseText(10); 119 | expect(getOut()).toMatchInlineSnapshot(` 120 | "- eraseText: 10 121 | " 122 | `); 123 | }); 124 | } 125 | -------------------------------------------------------------------------------- /src/commands/type.ts: -------------------------------------------------------------------------------- 1 | export type ElementMatcher = { 2 | id?: string; 3 | text?: string; 4 | }; 5 | 6 | export type WhenCondition = { 7 | visible?: string; 8 | notVisible?: string; 9 | true?: any; 10 | platform?: "Android" | "iOS" | "Web"; 11 | }; 12 | -------------------------------------------------------------------------------- /src/commands/wait.ts: -------------------------------------------------------------------------------- 1 | import { addOut } from "../out"; 2 | 3 | /** 4 | * Waits until an ongoing animation or video ends. 5 | * @param maxWait - Optional timeout (in milliseconds) to wait before proceeding. 6 | */ 7 | export const waitForAnimationEnd = (maxWait: number = 5000) => { 8 | addOut( 9 | maxWait 10 | ? `- waitForAnimationToEnd:\n timeout: ${maxWait}\n` 11 | : "- waitForAnimationToEnd\n" 12 | ); 13 | }; 14 | 15 | /** 16 | * Waits until an element with the given testId is visible. 17 | * @param id - The testId of the element to wait for. 18 | * @param maxWait - Maximum wait time (in milliseconds) for the element to appear. 19 | */ 20 | export const waitUntilVisible = (id: string, maxWait: number) => { 21 | addOut( 22 | `- extendedWaitUntil:\n visible:\n id: "${id}"\n timeout: ${ 23 | maxWait ?? 5000 24 | }\n` 25 | ); 26 | }; 27 | 28 | /** 29 | * Waits until an element with the given testId is no longer visible. 30 | * @param id - The testId of the element to wait for. 31 | * @param maxWait - Maximum wait time (in milliseconds) for the element to disappear. 32 | */ 33 | export const waitUntilNotVisible = (id: string, maxWait: number) => { 34 | addOut( 35 | `- extendedWaitUntil:\n notVisible:\n id: "${id}"\n timeout: ${ 36 | maxWait ?? 5000 37 | }\n` 38 | ); 39 | }; 40 | 41 | /** 42 | * Waits for a specified number of milliseconds. 43 | * @param ms - The number of milliseconds to wait. 44 | */ 45 | export const wait = (ms: number) => { 46 | addOut( 47 | `- swipe:\n start: -1, -1\n end: -1, -100\n duration: ${ms}\n` 48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import fs, { writeFileSync } from "fs"; 3 | import path, { join } from "path"; 4 | import dotenv from "dotenv"; 5 | import { consola } from "consola"; 6 | import { rewriteCode } from "./rewrite-code"; 7 | import { defineCommand, runMain } from "citty"; 8 | import { createYamlOutPath, jiti } from "./utils"; 9 | import { execSync } from "child_process"; 10 | 11 | const main = defineCommand({ 12 | args: { 13 | path: { 14 | type: "positional", 15 | required: true, 16 | }, 17 | device: { 18 | type: "string", 19 | alias: "d", 20 | description: "Device to run the test on", 21 | }, 22 | }, 23 | async run({ args }) { 24 | loadEnv(); 25 | const cwd = process.cwd(); 26 | 27 | // create temp file 28 | const fullFlowPath = args.path.startsWith("/") 29 | ? args.path 30 | : join(cwd, args.path); 31 | const code = await rewriteCode(fullFlowPath); 32 | const tempFilePath = fullFlowPath.replace(".ts", ".temp.ts"); 33 | writeFileSync(tempFilePath, code); 34 | 35 | // execute temp file 36 | const yamlOutPath = createYamlOutPath(fullFlowPath); 37 | try { 38 | await jiti.import(tempFilePath); 39 | consola.success(`Created Yaml to ${yamlOutPath} ✔`); 40 | } catch (e) { 41 | console.error(e); 42 | fs.unlinkSync(tempFilePath); 43 | process.exit(1); 44 | } 45 | fs.unlinkSync(tempFilePath); 46 | 47 | // run maestro test 48 | const command = `maestro ${ 49 | args.device ? `--device ${args.device}` : "" 50 | } test ${yamlOutPath}`; 51 | try { 52 | execSync(command, { 53 | stdio: "inherit", 54 | env: process.env, 55 | }); 56 | console.log("Test passed"); 57 | } catch (e) { 58 | if ("status" in e && e.status === 1) { 59 | consola.error({ 60 | message: `Test failed: ${fullFlowPath}`, 61 | }); 62 | } else { 63 | consola.error({ 64 | message: `Failed to start test: ${fullFlowPath}`, 65 | additional: `You can check actual yaml file at ${yamlOutPath}`, 66 | }); 67 | } 68 | process.exit(1); 69 | } 70 | }, 71 | }); 72 | if (!import.meta.vitest) runMain(main); 73 | 74 | function loadEnv() { 75 | let currentPath = process.cwd(); 76 | let dotEnvPath = ""; 77 | while (currentPath !== "/") { 78 | const files = fs.readdirSync(currentPath); 79 | if (files.includes(".env")) { 80 | dotEnvPath = path.join(currentPath, ".env"); 81 | break; 82 | } 83 | currentPath = path.join(currentPath, "../"); 84 | } 85 | if (dotEnvPath) { 86 | consola.info(`Found .env file at ${dotEnvPath}`); 87 | dotenv.config({ path: dotEnvPath }); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/out.ts: -------------------------------------------------------------------------------- 1 | import { parse } from "yaml"; 2 | 3 | // Nested command handling 4 | let nestLevel = 0; 5 | let nestedCommands: string[] = []; 6 | export const handleNest = (func: () => any, parseAsYaml?: boolean) => { 7 | nestLevel++; 8 | func(); 9 | const out = nestedCommands[nestLevel - 1]; 10 | nestedCommands[nestLevel - 1] = ""; 11 | nestLevel--; 12 | return parseAsYaml ? parse(out) : out; 13 | }; 14 | 15 | export let out = ""; 16 | export const resetOut = () => { 17 | out = ""; 18 | }; 19 | 20 | export const addOut = (command: string) => { 21 | if (nestLevel) { 22 | if (!nestedCommands[nestLevel - 1]) nestedCommands[nestLevel - 1] = ""; 23 | nestedCommands[nestLevel - 1] += command; 24 | } else out += command; 25 | }; 26 | 27 | export const getOut = () => { 28 | const currentOutput = out; 29 | resetOut(); 30 | return currentOutput; 31 | }; 32 | -------------------------------------------------------------------------------- /src/rewrite-code.ts: -------------------------------------------------------------------------------- 1 | import { parseModule } from "magicast"; 2 | 3 | import { createYamlOutPath, jiti, maestsDir, tsConfigDir } from "./utils"; 4 | import { fileURLToPath } from "url"; 5 | import { dirname, join } from "path"; 6 | import { build, Plugin } from "esbuild"; 7 | import { readFileSync } from "fs"; 8 | import * as ts from "typescript"; 9 | 10 | const rewriteRunScriptPlugin = (): Plugin => ({ 11 | name: "rewrite-run-script", 12 | setup(build) { 13 | build.onLoad({ filter: /.*/ }, async (args) => { 14 | let code = readFileSync(args.path, "utf-8"); 15 | 16 | const _imports = parseModule(code).imports; 17 | const imports = JSON.parse(JSON.stringify(_imports)) as typeof _imports; 18 | const rewriteMap: Record = Object.fromEntries( 19 | Object.entries(imports).map(([key, value]) => { 20 | if (value.from.startsWith(".")) { 21 | value.from = join(dirname(args.path), value.from); 22 | } 23 | let path = jiti.esmResolve(value.from, { try: true }); 24 | if (path) path = fileURLToPath(path); 25 | return [key, path]; 26 | }) 27 | ); 28 | code = rewriteRunScript(code, rewriteMap); 29 | return { 30 | contents: code, 31 | loader: "ts", 32 | }; 33 | }); 34 | }, 35 | }); 36 | 37 | export const rewriteCode = async (fullFlowPath: string) => { 38 | const yamlOutPath = createYamlOutPath(fullFlowPath); 39 | 40 | let code = await build({ 41 | entryPoints: [fullFlowPath], 42 | bundle: true, 43 | packages: "external", 44 | platform: "node", 45 | write: false, 46 | target: "esnext", 47 | plugins: [rewriteRunScriptPlugin()], 48 | format: "esm", 49 | }).then((bundled) => bundled.outputFiles[0].text); 50 | 51 | code = 52 | `import { writeYaml } from 'maests/write-yaml'\n` + 53 | code + 54 | `\nwriteYaml("${yamlOutPath}")`; 55 | 56 | return code; 57 | }; 58 | 59 | if (import.meta.vitest) { 60 | it("rewrites ts flow code", async () => { 61 | const fullFlowPath = join(__dirname, "../fixtures/sample-flow.ts"); 62 | const result = await rewriteCode(fullFlowPath); 63 | 64 | expect(result).toMatchInlineSnapshot(` 65 | "import { writeYaml } from 'maests/write-yaml' 66 | // fixtures/sample-flow.ts 67 | import { getOutput, M as M2 } from "maests"; 68 | 69 | // fixtures/utils/openApp.ts 70 | import { M } from "maests"; 71 | var openApp = () => { 72 | M.initFlow({ appId: "com.my.app" }); 73 | M.launchApp({ appId: "com.my.app" }); 74 | M.runScript("${join( 75 | tsConfigDir, 76 | "fixtures/utils/nest-script.ts" 77 | )}", "nestScript"); 78 | }; 79 | 80 | // fixtures/sample-flow.ts 81 | openApp(); 82 | M2.runScript("${join( 83 | tsConfigDir, 84 | "fixtures/utils/script.ts" 85 | )}", "someScript"); 86 | M2.assertVisible({ id: getOutput("id") }); 87 | M2.runFlow({ 88 | flow: () => { 89 | M2.repeatWhileNotVisible({ 90 | text: "4" 91 | }, () => { 92 | M2.tapOnText("Increment"); 93 | }); 94 | }, 95 | condition: { 96 | visible: "Increment" 97 | } 98 | }); 99 | 100 | writeYaml("${join(tsConfigDir, "maests/fixtures/sample-flow.yaml")}")" 101 | `); 102 | }); 103 | } 104 | 105 | const rewriteRunScript = ( 106 | code: string, 107 | rewriteMap: Record 108 | ): string => { 109 | const sourceFile = ts.createSourceFile( 110 | "tempFile.ts", 111 | code, 112 | ts.ScriptTarget.Latest, 113 | true 114 | ); 115 | 116 | const transformer = ( 117 | context: ts.TransformationContext 118 | ) => { 119 | const visit: ts.Visitor = (node: ts.Node): ts.Node | undefined => { 120 | if (ts.isCallExpression(node)) { 121 | const expression = node.expression; 122 | if ( 123 | ts.isPropertyAccessExpression(expression) && 124 | ts.isIdentifier(expression.expression) && 125 | expression.expression.text === "M" && 126 | expression.name.text === "runScript" && 127 | node.arguments.length > 0 && 128 | ts.isIdentifier(node.arguments[0]) 129 | ) { 130 | const argName = node.arguments[0].text; 131 | const newArguments = [ 132 | ts.factory.createStringLiteral(rewriteMap[argName]), 133 | ts.factory.createStringLiteral(argName), 134 | ]; 135 | return ts.factory.updateCallExpression( 136 | node, 137 | expression, 138 | node.typeArguments, 139 | newArguments 140 | ); 141 | } 142 | } 143 | return ts.visitEachChild(node, visit, context); 144 | }; 145 | return (node: T) => ts.visitNode(node, visit); 146 | }; 147 | 148 | const result = ts.transform(sourceFile, [transformer]); 149 | const printer = ts.createPrinter(); 150 | const transformedSourceFile = result.transformed[0] as ts.SourceFile; 151 | const transformedCode = printer.printFile(transformedSourceFile); 152 | 153 | return transformedCode; 154 | }; 155 | 156 | if (import.meta.vitest) { 157 | it("rewrite runScript", () => { 158 | const code = ` 159 | import { someScript } from "./utils/script"; 160 | M.runScript(someScript); 161 | `; 162 | const rewriteMap = { 163 | someScript: "/Users/user-name/my-oss/maests/fixtures/utils/script.ts", 164 | }; 165 | const result = rewriteRunScript(code, rewriteMap); 166 | expect(result).toMatchInlineSnapshot(` 167 | "import { someScript } from "./utils/script"; 168 | M.runScript("/Users/user-name/my-oss/maests/fixtures/utils/script.ts", "someScript"); 169 | " 170 | `); 171 | }); 172 | } 173 | 174 | export const deleteExport = (code: string): string => { 175 | const sourceFile = ts.createSourceFile( 176 | "tempFile.ts", 177 | code, 178 | ts.ScriptTarget.Latest, 179 | true 180 | ); 181 | 182 | const transformer = ( 183 | context: ts.TransformationContext 184 | ) => { 185 | const visit: ts.Visitor = (node: ts.Node): ts.Node | undefined => { 186 | // ExportNamedDeclarationを削除 187 | if (ts.isExportDeclaration(node) || ts.isExportAssignment(node)) { 188 | return undefined; 189 | } 190 | return ts.visitEachChild(node, visit, context); 191 | }; 192 | return (node: T) => ts.visitNode(node, visit); 193 | }; 194 | 195 | const result = ts.transform(sourceFile, [transformer]); 196 | const printer = ts.createPrinter(); 197 | const transformedSourceFile = result.transformed[0] as ts.SourceFile; 198 | const transformedCode = printer.printFile(transformedSourceFile); 199 | 200 | return transformedCode; 201 | }; 202 | 203 | if (import.meta.vitest) { 204 | it("delete export", () => { 205 | const code = ` 206 | const foo = 1; 207 | export { foo } 208 | `; 209 | const result = deleteExport(code); 210 | expect(result).toMatchInlineSnapshot(` 211 | "const foo = 1; 212 | " 213 | `); 214 | }); 215 | } 216 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, mkdirSync, writeFileSync } from "fs"; 2 | import { getTsconfig } from "get-tsconfig"; 3 | import { createJiti } from "jiti"; 4 | import { dirname, join } from "path"; 5 | 6 | const { path, config } = getTsconfig(); 7 | export const tsConfigDir = dirname(path); 8 | export const maestsDir = join(tsConfigDir, "maests"); 9 | if (!existsSync(maestsDir)) mkdirSync(maestsDir); 10 | 11 | export const createYamlOutPath = (fullTsFlowPath: string) => { 12 | const tsFlowPath = fullTsFlowPath.replace(`${tsConfigDir}/`, ""); 13 | return join(maestsDir, tsFlowPath.replace(".ts", ".yaml")); 14 | }; 15 | 16 | export const writeFileWithDirectorySync = (filePath: string, data: string) => { 17 | const dir = dirname(filePath); 18 | if (!existsSync(dir)) { 19 | mkdirSync(dir, { recursive: true }); 20 | } 21 | writeFileSync(filePath, data); 22 | }; 23 | 24 | // create jiti instance 25 | const normalizedAlias = Object.fromEntries( 26 | Object.entries(config?.compilerOptions?.paths || {}).map(([key, value]) => { 27 | const normalizedKey = key.replace("/*", ""); 28 | const normalizedValue = value[0].replace("/*", ""); 29 | return [normalizedKey, join(dirname(path), normalizedValue)]; 30 | }) 31 | ); 32 | export const jiti = createJiti(process.cwd(), { 33 | alias: normalizedAlias, 34 | }); 35 | -------------------------------------------------------------------------------- /src/write-yaml.ts: -------------------------------------------------------------------------------- 1 | import { out, resetOut } from "./out"; 2 | import { writeFileWithDirectorySync } from "./utils"; 3 | 4 | export const writeYaml = (outPath?: string) => { 5 | writeFileWithDirectorySync(outPath, out); 6 | resetOut(); 7 | }; 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "Preserve", 4 | "moduleResolution": "Bundler", 5 | "target": "ESNext", 6 | "sourceMap": true, 7 | "outDir": "dist", 8 | "declaration": true, 9 | "esModuleInterop": true, 10 | "allowSyntheticDefaultImports": true, 11 | "types": ["vitest/importMeta", "vitest/globals"], 12 | "paths": { 13 | "@/*": ["./*"] 14 | } 15 | }, 16 | "include": [ 17 | "src/**/*", 18 | "fixtures/**/*", 19 | "maestro.d.ts", 20 | "**/maestro-ext.d.ts", 21 | "playground", 22 | "__tests__" 23 | ], 24 | "exclude": ["node_modules", "dist", "playground"] 25 | } 26 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, defaultExclude } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | include: ["**/*.{test,spec}.?(c|m)[jt]s?(x)", "**/*.[jt]s?(x)"], 7 | exclude: [...defaultExclude, "playground/**/*", "maests/**/*", "docs/**/*"], 8 | passWithNoTests: true, 9 | root: "./", 10 | alias: { 11 | "@/": new URL("./", import.meta.url).pathname, 12 | }, 13 | }, 14 | }); 15 | --------------------------------------------------------------------------------