create-agent-skills 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/package.json +1 -1
- package/skills/maestro-testing/SKILL.md +691 -0
- package/skills/maestro-testing/examples/checkout-flow.md +246 -0
- package/skills/maestro-testing/examples/login-flow.md +164 -0
- package/skills/maestro-testing/examples/project-structure.md +245 -0
- package/skills/maestro-testing/resources/commands-reference.md +142 -0
package/README.md
CHANGED
|
@@ -41,6 +41,12 @@ npx create-agent-skills
|
|
|
41
41
|
| `code-review` | Reviews code for bugs, style, and security issues |
|
|
42
42
|
| `create-agent-skill` | Helps create new skills following guidelines |
|
|
43
43
|
| `documentation` | Creates clear READMEs, API docs, and comments |
|
|
44
|
+
| `git-commit` | Writes conventional commit messages |
|
|
45
|
+
| `git-pr` | Creates well-structured pull requests |
|
|
46
|
+
| `git-review` | Reviews PRs for code quality and best practices |
|
|
47
|
+
| `maestro-testing` | Write E2E tests for mobile/web apps using Maestro |
|
|
48
|
+
| `tailwindcss-v4` | Tailwind CSS v4 setup and migration guide |
|
|
49
|
+
| `tauri-v2` | Build desktop apps with Tauri v2 + React |
|
|
44
50
|
| `testing` | Helps write unit, integration, and E2E tests |
|
|
45
51
|
|
|
46
52
|
## Creating New Skills
|
package/package.json
CHANGED
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: maestro-testing
|
|
3
|
+
description: Write robust E2E tests for mobile apps and web using Maestro. Use when creating UI automation tests, flow-based testing, or setting up test suites with optimal selectors and best practices.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Maestro Testing Skill
|
|
7
|
+
|
|
8
|
+
Write robust End-to-End tests for mobile apps (iOS/Android) and web applications using Maestro - a simple, powerful, and reliable UI testing framework.
|
|
9
|
+
|
|
10
|
+
## When to Use This Skill
|
|
11
|
+
|
|
12
|
+
- Writing E2E tests for mobile apps (iOS, Android, React Native, Flutter)
|
|
13
|
+
- Testing web applications in desktop browsers
|
|
14
|
+
- Creating reusable test flows and page objects
|
|
15
|
+
- Setting up CI/CD test automation with Maestro
|
|
16
|
+
- Migrating from other testing frameworks to Maestro
|
|
17
|
+
|
|
18
|
+
## Examples & Resources
|
|
19
|
+
|
|
20
|
+
### Examples
|
|
21
|
+
- [Login Flow](examples/login-flow.md) - Complete login test with subflows and best practices
|
|
22
|
+
- [Checkout Flow](examples/checkout-flow.md) - E-commerce checkout with scrolling and forms
|
|
23
|
+
- [Project Structure](examples/project-structure.md) - Recommended test suite organization
|
|
24
|
+
|
|
25
|
+
### Resources
|
|
26
|
+
- [Commands Reference](resources/commands-reference.md) - Quick reference cheat sheet for all commands
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## Prerequisites
|
|
30
|
+
|
|
31
|
+
- Maestro CLI installed (`curl -fsSL "https://get.maestro.mobile.dev" | bash`)
|
|
32
|
+
- For mobile: Android emulator or iOS simulator running
|
|
33
|
+
- For web: Chrome browser installed
|
|
34
|
+
- App installed on device/emulator (for mobile testing)
|
|
35
|
+
|
|
36
|
+
## Flow File Structure
|
|
37
|
+
|
|
38
|
+
Every Maestro flow is a YAML file with optional configuration header and commands:
|
|
39
|
+
|
|
40
|
+
```yaml
|
|
41
|
+
# Flow configuration (optional header above ---)
|
|
42
|
+
appId: com.example.myapp # Required: package name (Android) or bundle ID (iOS)
|
|
43
|
+
name: Login Flow Test # Optional: custom name for reports
|
|
44
|
+
tags:
|
|
45
|
+
- smoke
|
|
46
|
+
- login
|
|
47
|
+
env:
|
|
48
|
+
USERNAME: testuser@example.com
|
|
49
|
+
PASSWORD: secret123
|
|
50
|
+
---
|
|
51
|
+
# Commands start after ---
|
|
52
|
+
- launchApp
|
|
53
|
+
- tapOn: "Login"
|
|
54
|
+
- inputText: "${USERNAME}"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Selector Priority (Most to Least Preferred)
|
|
58
|
+
|
|
59
|
+
> [!IMPORTANT]
|
|
60
|
+
> Always prefer stable selectors. The order of preference:
|
|
61
|
+
|
|
62
|
+
| Priority | Selector | When to Use | Example |
|
|
63
|
+
|----------|----------|-------------|---------|
|
|
64
|
+
| 1 | `id` | Best for dynamic content, i18n apps | `id: "login_button"` |
|
|
65
|
+
| 2 | `text` | Stable static text, readable tests | `text: "Submit"` |
|
|
66
|
+
| 3 | `id` + `text` | Unique identification | `id: "btn", text: "OK"` |
|
|
67
|
+
| 4 | Relative selectors | Complex layouts | `below: "Email", id: "input"` |
|
|
68
|
+
| 5 | `index` | Last resort for duplicates | `text: "Item", index: 2` |
|
|
69
|
+
| ❌ | `point` coordinates | Avoid - device dependent | `point: "50%, 50%"` |
|
|
70
|
+
|
|
71
|
+
### Selector Syntax Reference
|
|
72
|
+
|
|
73
|
+
```yaml
|
|
74
|
+
# Basic selectors
|
|
75
|
+
- tapOn: "Button Text" # Shorthand for text
|
|
76
|
+
- tapOn:
|
|
77
|
+
id: "submit_btn" # By accessibility ID
|
|
78
|
+
- tapOn:
|
|
79
|
+
text: "Login" # By visible text
|
|
80
|
+
enabled: true # Must be enabled
|
|
81
|
+
|
|
82
|
+
# Relative position selectors
|
|
83
|
+
- tapOn:
|
|
84
|
+
below: "Email Label" # Element below another
|
|
85
|
+
id: "input_field"
|
|
86
|
+
- tapOn:
|
|
87
|
+
above:
|
|
88
|
+
id: "footer" # Element above footer
|
|
89
|
+
- tapOn:
|
|
90
|
+
leftOf: "Price"
|
|
91
|
+
text: "Quantity"
|
|
92
|
+
- tapOn:
|
|
93
|
+
containsChild: "Icon" # Parent contains child with text
|
|
94
|
+
- tapOn:
|
|
95
|
+
childOf:
|
|
96
|
+
id: "toolbar" # Child of element with ID
|
|
97
|
+
|
|
98
|
+
# Multiple matches - use index (0-based)
|
|
99
|
+
- tapOn:
|
|
100
|
+
text: "Add to Cart"
|
|
101
|
+
index: 0 # First matching element
|
|
102
|
+
|
|
103
|
+
# Element state selectors
|
|
104
|
+
- assertVisible:
|
|
105
|
+
text: "Submit"
|
|
106
|
+
enabled: true # Must be enabled
|
|
107
|
+
checked: false # Checkbox unchecked
|
|
108
|
+
focused: true # Has keyboard focus
|
|
109
|
+
selected: true # Is selected
|
|
110
|
+
|
|
111
|
+
# Size-based selectors
|
|
112
|
+
- tapOn:
|
|
113
|
+
width: 100
|
|
114
|
+
height: 50
|
|
115
|
+
tolerance: 10 # ±10 pixels
|
|
116
|
+
|
|
117
|
+
# Element traits
|
|
118
|
+
- tapOn:
|
|
119
|
+
traits: text # Contains text
|
|
120
|
+
- tapOn:
|
|
121
|
+
traits: long-text # 200+ characters
|
|
122
|
+
- tapOn:
|
|
123
|
+
traits: square # Width ≈ Height
|
|
124
|
+
|
|
125
|
+
# Regular expressions (all text/id fields support regex)
|
|
126
|
+
- assertVisible: "Total: \\$[0-9]+\\.[0-9]{2}"
|
|
127
|
+
- tapOn:
|
|
128
|
+
id: ".*_submit_button"
|
|
129
|
+
- assertVisible: ".*brown fox.*" # Partial match with .*
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Essential Commands
|
|
133
|
+
|
|
134
|
+
### App Lifecycle
|
|
135
|
+
|
|
136
|
+
```yaml
|
|
137
|
+
# Launch app (uses appId from config)
|
|
138
|
+
- launchApp
|
|
139
|
+
|
|
140
|
+
# Launch with clean state
|
|
141
|
+
- launchApp:
|
|
142
|
+
clearState: true
|
|
143
|
+
clearKeychain: true # iOS only
|
|
144
|
+
|
|
145
|
+
# Launch specific app
|
|
146
|
+
- launchApp:
|
|
147
|
+
appId: "com.other.app"
|
|
148
|
+
|
|
149
|
+
# Launch with specific permissions
|
|
150
|
+
- launchApp:
|
|
151
|
+
permissions:
|
|
152
|
+
notifications: deny
|
|
153
|
+
camera: allow
|
|
154
|
+
location: deny
|
|
155
|
+
|
|
156
|
+
# Stop and kill app
|
|
157
|
+
- stopApp
|
|
158
|
+
- killApp
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Tap Interactions
|
|
162
|
+
|
|
163
|
+
```yaml
|
|
164
|
+
# Simple tap
|
|
165
|
+
- tapOn: "Button"
|
|
166
|
+
- tapOn:
|
|
167
|
+
id: "submit_btn"
|
|
168
|
+
|
|
169
|
+
# Double tap
|
|
170
|
+
- doubleTapOn: "Zoom In"
|
|
171
|
+
|
|
172
|
+
# Long press
|
|
173
|
+
- longPressOn: "Item to Delete"
|
|
174
|
+
|
|
175
|
+
# Tap with retry (for async loading)
|
|
176
|
+
- tapOn:
|
|
177
|
+
text: "Load More"
|
|
178
|
+
retryTapIfNoChange: true # Retry if screen doesn't change
|
|
179
|
+
|
|
180
|
+
# Repeated taps
|
|
181
|
+
- tapOn:
|
|
182
|
+
text: "+"
|
|
183
|
+
repeat: 5
|
|
184
|
+
delay: 200 # 200ms between taps
|
|
185
|
+
|
|
186
|
+
# Tap relative point within element
|
|
187
|
+
- tapOn:
|
|
188
|
+
text: "A text with a hyperlink"
|
|
189
|
+
point: "90%, 50%" # Tap right side of element
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Text Input
|
|
193
|
+
|
|
194
|
+
```yaml
|
|
195
|
+
# Type text (into focused field)
|
|
196
|
+
- inputText: "Hello World"
|
|
197
|
+
|
|
198
|
+
# Tap field first, then type
|
|
199
|
+
- tapOn:
|
|
200
|
+
id: "email_input"
|
|
201
|
+
- inputText: "user@example.com"
|
|
202
|
+
|
|
203
|
+
# Random data generation
|
|
204
|
+
- inputRandomEmail # Random email
|
|
205
|
+
- inputRandomPersonName # Random name
|
|
206
|
+
- inputRandomNumber:
|
|
207
|
+
length: 6 # 6-digit number
|
|
208
|
+
- inputRandomText:
|
|
209
|
+
length: 10 # 10 random characters
|
|
210
|
+
|
|
211
|
+
# Erase text
|
|
212
|
+
- eraseText: 10 # Delete 10 characters
|
|
213
|
+
- eraseText # Delete all (focused field)
|
|
214
|
+
|
|
215
|
+
# Copy and paste
|
|
216
|
+
- copyTextFrom:
|
|
217
|
+
id: "generated_code"
|
|
218
|
+
- pasteText
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Assertions
|
|
222
|
+
|
|
223
|
+
```yaml
|
|
224
|
+
# Assert element is visible
|
|
225
|
+
- assertVisible: "Welcome"
|
|
226
|
+
- assertVisible:
|
|
227
|
+
id: "success_message"
|
|
228
|
+
enabled: true
|
|
229
|
+
|
|
230
|
+
# Assert element is NOT visible
|
|
231
|
+
- assertNotVisible: "Error"
|
|
232
|
+
- assertNotVisible:
|
|
233
|
+
id: "loading_spinner"
|
|
234
|
+
|
|
235
|
+
# Assert with custom message (for debugging)
|
|
236
|
+
- assertTrue:
|
|
237
|
+
condition: ${value > 0}
|
|
238
|
+
label: "Value should be positive"
|
|
239
|
+
|
|
240
|
+
# AI-powered assertions (requires API key)
|
|
241
|
+
- assertWithAI: "The login form is displayed correctly"
|
|
242
|
+
- assertNoDefectsWithAI # Check for visual defects
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Scrolling
|
|
246
|
+
|
|
247
|
+
```yaml
|
|
248
|
+
# Simple scroll down
|
|
249
|
+
- scroll
|
|
250
|
+
|
|
251
|
+
# Scroll until element visible
|
|
252
|
+
- scrollUntilVisible:
|
|
253
|
+
element: "Terms and Conditions"
|
|
254
|
+
direction: DOWN # DOWN|UP|LEFT|RIGHT
|
|
255
|
+
timeout: 30000 # Max 30 seconds
|
|
256
|
+
speed: 40 # 0-100 (higher = faster)
|
|
257
|
+
|
|
258
|
+
# Scroll with centering
|
|
259
|
+
- scrollUntilVisible:
|
|
260
|
+
element:
|
|
261
|
+
id: "target_item"
|
|
262
|
+
centerElement: true # Center element on screen
|
|
263
|
+
|
|
264
|
+
# Horizontal scroll
|
|
265
|
+
- scrollUntilVisible:
|
|
266
|
+
element: "Category 5"
|
|
267
|
+
direction: RIGHT
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Swipe Gestures
|
|
271
|
+
|
|
272
|
+
```yaml
|
|
273
|
+
# Swipe in direction
|
|
274
|
+
- swipe:
|
|
275
|
+
direction: LEFT # Swipe left (e.g., dismiss)
|
|
276
|
+
|
|
277
|
+
# Swipe on specific element
|
|
278
|
+
- swipe:
|
|
279
|
+
from:
|
|
280
|
+
id: "swipeable_item"
|
|
281
|
+
direction: LEFT
|
|
282
|
+
|
|
283
|
+
# Swipe between points (relative %)
|
|
284
|
+
- swipe:
|
|
285
|
+
start: "90%, 50%"
|
|
286
|
+
end: "10%, 50%"
|
|
287
|
+
|
|
288
|
+
# Swipe between absolute coordinates
|
|
289
|
+
- swipe:
|
|
290
|
+
start: "300, 500"
|
|
291
|
+
end: "100, 500"
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Navigation
|
|
295
|
+
|
|
296
|
+
```yaml
|
|
297
|
+
# Press back button (Android/iOS)
|
|
298
|
+
- back
|
|
299
|
+
|
|
300
|
+
# Open deep link
|
|
301
|
+
- openLink: "myapp://profile/123"
|
|
302
|
+
|
|
303
|
+
# Press hardware keys (Android)
|
|
304
|
+
- pressKey: Home
|
|
305
|
+
- pressKey: Enter
|
|
306
|
+
- pressKey: Volume Up
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### Wait Commands
|
|
310
|
+
|
|
311
|
+
```yaml
|
|
312
|
+
# Wait for element (default behavior)
|
|
313
|
+
# Most commands automatically wait for elements
|
|
314
|
+
|
|
315
|
+
# Extended wait with timeout
|
|
316
|
+
- extendedWaitUntil:
|
|
317
|
+
visible: "Success"
|
|
318
|
+
timeout: 30000 # Wait up to 30 seconds
|
|
319
|
+
|
|
320
|
+
# Wait for animation to complete
|
|
321
|
+
- waitForAnimationToEnd
|
|
322
|
+
|
|
323
|
+
# Control settle time for dynamic content
|
|
324
|
+
- tapOn:
|
|
325
|
+
text: "Submit"
|
|
326
|
+
waitToSettleTimeoutMs: 1000 # Max wait for screen to settle
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Screenshots and Recording
|
|
330
|
+
|
|
331
|
+
```yaml
|
|
332
|
+
# Take screenshot
|
|
333
|
+
- takeScreenshot: "login_screen" # Saved to output directory
|
|
334
|
+
|
|
335
|
+
# Video recording
|
|
336
|
+
- startRecording: "test_flow"
|
|
337
|
+
- launchApp
|
|
338
|
+
- tapOn: "Login"
|
|
339
|
+
- stopRecording
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
## Reusable Flows (Page Object Pattern)
|
|
343
|
+
|
|
344
|
+
### Directory Structure
|
|
345
|
+
|
|
346
|
+
```
|
|
347
|
+
.maestro/
|
|
348
|
+
├── config.yaml # Workspace configuration
|
|
349
|
+
├── flows/
|
|
350
|
+
│ ├── login.yaml # Test: Login flow
|
|
351
|
+
│ ├── checkout.yaml # Test: Checkout flow
|
|
352
|
+
│ └── ...
|
|
353
|
+
├── subflows/ # Reusable components
|
|
354
|
+
│ ├── login-steps.yaml # Login actions
|
|
355
|
+
│ ├── logout-steps.yaml # Logout actions
|
|
356
|
+
│ └── navigate-to-*.yaml # Navigation helpers
|
|
357
|
+
└── scripts/
|
|
358
|
+
└── helpers.js # JavaScript helpers
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### Subflow Example (login-steps.yaml)
|
|
362
|
+
|
|
363
|
+
```yaml
|
|
364
|
+
# subflows/login-steps.yaml
|
|
365
|
+
# No appId needed - inherits from parent flow
|
|
366
|
+
|
|
367
|
+
- tapOn:
|
|
368
|
+
id: "email_input"
|
|
369
|
+
- inputText: "${EMAIL}"
|
|
370
|
+
- tapOn:
|
|
371
|
+
id: "password_input"
|
|
372
|
+
- inputText: "${PASSWORD}"
|
|
373
|
+
- tapOn:
|
|
374
|
+
id: "login_button"
|
|
375
|
+
- assertVisible:
|
|
376
|
+
id: "home_screen"
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### Main Flow Using Subflow
|
|
380
|
+
|
|
381
|
+
```yaml
|
|
382
|
+
# flows/login.yaml
|
|
383
|
+
appId: com.example.app
|
|
384
|
+
env:
|
|
385
|
+
EMAIL: test@example.com
|
|
386
|
+
PASSWORD: password123
|
|
387
|
+
---
|
|
388
|
+
- launchApp:
|
|
389
|
+
clearState: true
|
|
390
|
+
|
|
391
|
+
- runFlow: ../subflows/login-steps.yaml
|
|
392
|
+
|
|
393
|
+
- assertVisible: "Welcome back"
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### Conditional Flow Execution
|
|
397
|
+
|
|
398
|
+
```yaml
|
|
399
|
+
# Run subflow only if element visible
|
|
400
|
+
- runFlow:
|
|
401
|
+
when:
|
|
402
|
+
visible: "Cookie Banner"
|
|
403
|
+
file: ../subflows/dismiss-cookies.yaml
|
|
404
|
+
|
|
405
|
+
# Run inline commands conditionally
|
|
406
|
+
- runFlow:
|
|
407
|
+
when:
|
|
408
|
+
visible: "Update Available"
|
|
409
|
+
commands:
|
|
410
|
+
- tapOn: "Later"
|
|
411
|
+
|
|
412
|
+
# Platform-specific flows
|
|
413
|
+
- runFlow:
|
|
414
|
+
when:
|
|
415
|
+
platform: iOS
|
|
416
|
+
file: ../subflows/ios-specific.yaml
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### Passing Parameters to Subflows
|
|
420
|
+
|
|
421
|
+
```yaml
|
|
422
|
+
# Main flow
|
|
423
|
+
- runFlow:
|
|
424
|
+
file: ../subflows/add-to-cart.yaml
|
|
425
|
+
env:
|
|
426
|
+
PRODUCT_NAME: "Blue T-Shirt"
|
|
427
|
+
QUANTITY: "2"
|
|
428
|
+
|
|
429
|
+
# subflows/add-to-cart.yaml
|
|
430
|
+
- scrollUntilVisible:
|
|
431
|
+
element: "${PRODUCT_NAME}"
|
|
432
|
+
- tapOn: "${PRODUCT_NAME}"
|
|
433
|
+
- tapOn:
|
|
434
|
+
text: "+"
|
|
435
|
+
repeat: ${QUANTITY - 1}
|
|
436
|
+
- tapOn: "Add to Cart"
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
## Workspace Configuration
|
|
440
|
+
|
|
441
|
+
Create `.maestro/config.yaml`:
|
|
442
|
+
|
|
443
|
+
```yaml
|
|
444
|
+
# Flows to include (glob patterns)
|
|
445
|
+
flows:
|
|
446
|
+
- 'flows/*'
|
|
447
|
+
- '!flows/wip-*' # Exclude work-in-progress
|
|
448
|
+
|
|
449
|
+
# Tags configuration
|
|
450
|
+
includeTags:
|
|
451
|
+
- smoke
|
|
452
|
+
excludeTags:
|
|
453
|
+
- flaky
|
|
454
|
+
|
|
455
|
+
# Execution order
|
|
456
|
+
executionOrder:
|
|
457
|
+
continueOnFailure: false
|
|
458
|
+
flowsOrder:
|
|
459
|
+
- flows/login.yaml # Run first
|
|
460
|
+
- flows/onboarding.yaml # Run second
|
|
461
|
+
|
|
462
|
+
# Test output directory
|
|
463
|
+
testOutputDir: test-results
|
|
464
|
+
|
|
465
|
+
# Platform-specific settings
|
|
466
|
+
platform:
|
|
467
|
+
ios:
|
|
468
|
+
disableAnimations: true # Reduce flakiness
|
|
469
|
+
android:
|
|
470
|
+
disableAnimations: true
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
## Loops and Repeat
|
|
474
|
+
|
|
475
|
+
```yaml
|
|
476
|
+
# Repeat commands N times
|
|
477
|
+
- repeat:
|
|
478
|
+
times: 3
|
|
479
|
+
commands:
|
|
480
|
+
- tapOn: "Next"
|
|
481
|
+
- scroll
|
|
482
|
+
|
|
483
|
+
# Repeat while condition true
|
|
484
|
+
- repeat:
|
|
485
|
+
while:
|
|
486
|
+
visible: "Load More"
|
|
487
|
+
commands:
|
|
488
|
+
- tapOn: "Load More"
|
|
489
|
+
- scroll
|
|
490
|
+
|
|
491
|
+
# Repeat with index variable
|
|
492
|
+
- repeat:
|
|
493
|
+
times: ${ITEM_COUNT}
|
|
494
|
+
commands:
|
|
495
|
+
- tapOn: "Item ${maestro.repeatIndex}"
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
## JavaScript Integration
|
|
499
|
+
|
|
500
|
+
```yaml
|
|
501
|
+
# Inline JavaScript
|
|
502
|
+
- evalScript: ${output.total = quantity * price}
|
|
503
|
+
|
|
504
|
+
# Run external script
|
|
505
|
+
- runScript: scripts/calculate-total.js
|
|
506
|
+
|
|
507
|
+
# Use script output
|
|
508
|
+
- assertVisible: "Total: $${output.total}"
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
### JavaScript File Example (scripts/helpers.js)
|
|
512
|
+
|
|
513
|
+
```javascript
|
|
514
|
+
// scripts/helpers.js
|
|
515
|
+
function generateTestEmail() {
|
|
516
|
+
const timestamp = Date.now();
|
|
517
|
+
return `test_${timestamp}@example.com`;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Set output variables
|
|
521
|
+
output.testEmail = generateTestEmail();
|
|
522
|
+
output.currentDate = new Date().toISOString().split('T')[0];
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
## Retry Mechanism
|
|
526
|
+
|
|
527
|
+
```yaml
|
|
528
|
+
# Retry a block of commands on failure
|
|
529
|
+
- retry:
|
|
530
|
+
maxRetries: 3
|
|
531
|
+
commands:
|
|
532
|
+
- tapOn: "Retry Connection"
|
|
533
|
+
- assertVisible: "Connected"
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
## Device Settings
|
|
537
|
+
|
|
538
|
+
```yaml
|
|
539
|
+
# Airplane mode
|
|
540
|
+
- setAirplaneMode:
|
|
541
|
+
enabled: true
|
|
542
|
+
- toggleAirplaneMode
|
|
543
|
+
|
|
544
|
+
# Location (requires permissions)
|
|
545
|
+
- setLocation:
|
|
546
|
+
latitude: 37.7749
|
|
547
|
+
longitude: -122.4194
|
|
548
|
+
|
|
549
|
+
# Orientation
|
|
550
|
+
- setOrientation: LANDSCAPE
|
|
551
|
+
- setOrientation: PORTRAIT
|
|
552
|
+
|
|
553
|
+
# Clipboard
|
|
554
|
+
- setClipboard: "Pasted content"
|
|
555
|
+
- pasteText
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
## Best Practices
|
|
559
|
+
|
|
560
|
+
### 1. Use Stable Selectors
|
|
561
|
+
|
|
562
|
+
```yaml
|
|
563
|
+
# ✅ Good: Use accessibility IDs
|
|
564
|
+
- tapOn:
|
|
565
|
+
id: "submit_button"
|
|
566
|
+
|
|
567
|
+
# ✅ Good: Use stable text
|
|
568
|
+
- tapOn: "Sign In"
|
|
569
|
+
|
|
570
|
+
# ⚠️ Avoid: Index unless necessary
|
|
571
|
+
- tapOn:
|
|
572
|
+
text: "Button"
|
|
573
|
+
index: 3
|
|
574
|
+
|
|
575
|
+
# ❌ Bad: Avoid coordinates
|
|
576
|
+
- tapOn:
|
|
577
|
+
point: "150, 300"
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
### 2. Wait for Async Operations
|
|
581
|
+
|
|
582
|
+
```yaml
|
|
583
|
+
# ✅ Good: Use assertVisible before interaction
|
|
584
|
+
- assertVisible:
|
|
585
|
+
id: "loaded_content"
|
|
586
|
+
- tapOn: "Continue"
|
|
587
|
+
|
|
588
|
+
# ✅ Good: Use extendedWaitUntil for long operations
|
|
589
|
+
- tapOn: "Submit Order"
|
|
590
|
+
- extendedWaitUntil:
|
|
591
|
+
visible: "Order Confirmed"
|
|
592
|
+
timeout: 60000
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
### 3. Clear State for Isolation
|
|
596
|
+
|
|
597
|
+
```yaml
|
|
598
|
+
# ✅ Good: Start fresh
|
|
599
|
+
- launchApp:
|
|
600
|
+
clearState: true
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
### 4. Use Descriptive Flow Names
|
|
604
|
+
|
|
605
|
+
```yaml
|
|
606
|
+
# ✅ Good naming
|
|
607
|
+
appId: com.example.app
|
|
608
|
+
name: "User can complete checkout with credit card"
|
|
609
|
+
tags:
|
|
610
|
+
- checkout
|
|
611
|
+
- payment
|
|
612
|
+
- smoke
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
### 5. Create Reusable Subflows
|
|
616
|
+
|
|
617
|
+
```yaml
|
|
618
|
+
# ✅ Good: Extract common sequences
|
|
619
|
+
- runFlow: ../subflows/login-as-user.yaml
|
|
620
|
+
- runFlow: ../subflows/navigate-to-settings.yaml
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
### 6. Handle Dynamic Content
|
|
624
|
+
|
|
625
|
+
```yaml
|
|
626
|
+
# ✅ Good: Use regex for dynamic text
|
|
627
|
+
- assertVisible: "Order #[0-9]+"
|
|
628
|
+
- assertVisible: "Welcome, .*"
|
|
629
|
+
|
|
630
|
+
# ✅ Good: Use relative selectors
|
|
631
|
+
- tapOn:
|
|
632
|
+
below: "Shipping Address"
|
|
633
|
+
id: "edit_button"
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
## Running Tests
|
|
637
|
+
|
|
638
|
+
```bash
|
|
639
|
+
# Run single flow
|
|
640
|
+
maestro test flows/login.yaml
|
|
641
|
+
|
|
642
|
+
# Run all flows in directory
|
|
643
|
+
maestro test .maestro/flows/
|
|
644
|
+
|
|
645
|
+
# Run with specific config
|
|
646
|
+
maestro test --config .maestro/config.yaml .maestro/flows/
|
|
647
|
+
|
|
648
|
+
# Run with tags
|
|
649
|
+
maestro test --include-tags=smoke --exclude-tags=flaky .maestro/
|
|
650
|
+
|
|
651
|
+
# Continuous mode (re-run on file changes)
|
|
652
|
+
maestro test --continuous flows/login.yaml
|
|
653
|
+
|
|
654
|
+
# Debug mode with hierarchy viewer
|
|
655
|
+
maestro hierarchy
|
|
656
|
+
|
|
657
|
+
# Interactive studio
|
|
658
|
+
maestro studio
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
## Common Mistakes to Avoid
|
|
662
|
+
|
|
663
|
+
| Mistake | Problem | Solution |
|
|
664
|
+
|---------|---------|----------|
|
|
665
|
+
| Using coordinates | Breaks on different devices | Use `id` or `text` selectors |
|
|
666
|
+
| Not clearing state | Tests depend on previous state | Use `clearState: true` |
|
|
667
|
+
| Hardcoding wait times | Flaky or slow tests | Use `assertVisible` or `extendedWaitUntil` |
|
|
668
|
+
| Duplicate selectors | Wrong element tapped | Use relative selectors or `index` |
|
|
669
|
+
| Long monolithic flows | Hard to maintain | Break into subflows |
|
|
670
|
+
| Not using tags | Can't run subsets | Tag flows by category |
|
|
671
|
+
|
|
672
|
+
## Web Testing (Chrome)
|
|
673
|
+
|
|
674
|
+
```yaml
|
|
675
|
+
appId: com.google.chrome
|
|
676
|
+
---
|
|
677
|
+
- launchApp
|
|
678
|
+
- openLink: "https://example.com"
|
|
679
|
+
- assertVisible: "Example Domain"
|
|
680
|
+
- tapOn: "More information..."
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
> [!NOTE]
|
|
684
|
+
> For web testing, Maestro uses Chrome and interacts with the DOM through accessibility APIs. Some complex SPAs may require `waitForAnimationToEnd` after navigation.
|
|
685
|
+
|
|
686
|
+
## References
|
|
687
|
+
|
|
688
|
+
- [Maestro Documentation](https://docs.maestro.dev/)
|
|
689
|
+
- [Commands Reference](https://docs.maestro.dev/api-reference/commands)
|
|
690
|
+
- [Selectors Reference](https://docs.maestro.dev/api-reference/selectors)
|
|
691
|
+
- [Workspace Configuration](https://docs.maestro.dev/api-reference/configuration/workspace-configuration)
|