@xenon-device-management/xenon 2.12.0 → 2.13.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/lib/package.json +2 -2
- package/lib/public/assets/{AnnotationOverlay-Crkn72_3.js → AnnotationOverlay-CzbFYtPI.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-BThVlj_j.js → ApiKeyGate-CA5KFHHJ.js} +1 -1
- package/lib/public/assets/{BugReportButton-Dwa0GnBh.js → BugReportButton-D-DSGg8C.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-Ccc-pOls.js → DeviceMosaicView-paDEFU4B.js} +1 -1
- package/lib/public/assets/{EmptyState-BeuTrlN_.js → EmptyState-BE_5f8Vw.js} +1 -1
- package/lib/public/assets/{FieldGroup-DBIaNxl1.js → FieldGroup-DqMf7Rf3.js} +1 -1
- package/lib/public/assets/{FilterMenu-BgSAS6iT.js → FilterMenu-BQY75Su3.js} +1 -1
- package/lib/public/assets/{Menu-GCbP6gYN.js → Menu-BiW3os9u.js} +1 -1
- package/lib/public/assets/{Modal-ByESeBRC.js → Modal-DfawTxih.js} +1 -1
- package/lib/public/assets/{RecordingPage-Bwyf3WMl.js → RecordingPage-BvemyRiy.js} +1 -1
- package/lib/public/assets/{RecordingsPage-7kM1rB3i.js → RecordingsPage-CXfw7wpd.js} +1 -1
- package/lib/public/assets/{SegmentedControl-B8tTU8kD.js → SegmentedControl-B7m9kpom.js} +1 -1
- package/lib/public/assets/{SettingCard-Clg_Ijbc.js → SettingCard-Cw6lDD0l.js} +1 -1
- package/lib/public/assets/{Table-CIaR0QWV.js → Table-DlEuFgv-.js} +1 -1
- package/lib/public/assets/{activity-Bengh9SD.js → activity-CnLvNwae.js} +1 -1
- package/lib/public/assets/{ai-settings-G9SymiPH.js → ai-settings-D7Te8o-a.js} +1 -1
- package/lib/public/assets/{api-keys-CrGlbfn-.js → api-keys-DphSVzBr.js} +1 -1
- package/lib/public/assets/{apps-DIbcGVlU.js → apps-vUfu15lX.js} +1 -1
- package/lib/public/assets/{arrow-left-BtJIXXMY.js → arrow-left-CLQWAiCY.js} +1 -1
- package/lib/public/assets/{arrow-right-zm-NagQh.js → arrow-right-K6A5e_Ee.js} +1 -1
- package/lib/public/assets/{arrow-up-right-DdU8YEYp.js → arrow-up-right-DUA_lq6f.js} +1 -1
- package/lib/public/assets/{auth-shell-CHmBFmOz.js → auth-shell-BYRHcZd6.js} +2 -2
- package/lib/public/assets/{builds-page-JkzWKRpU.js → builds-page-C0fTvIG8.js} +1 -1
- package/lib/public/assets/{button-CqhPBGRj.js → button-BxnHCCsX.js} +1 -1
- package/lib/public/assets/{calendar-C7eYYJTo.js → calendar-CqVsn28A.js} +1 -1
- package/lib/public/assets/{check-DpbIM4E0.js → check-R6V3bot0.js} +1 -1
- package/lib/public/assets/{chevron-right-Dl4X1PRz.js → chevron-right-BA2Fntu-.js} +1 -1
- package/lib/public/assets/{circle-check-Du3sjbfV.js → circle-check-D4EzyGzs.js} +1 -1
- package/lib/public/assets/{circle-x-BSSqhpXi.js → circle-x-Cf6AF2B2.js} +1 -1
- package/lib/public/assets/{clock-DMT60v1C.js → clock-EZfet0sS.js} +1 -1
- package/lib/public/assets/{copy-BKNyOehd.js → copy-Bc6P8v9V.js} +1 -1
- package/lib/public/assets/{device-explorer-CoPR8DW3.js → device-explorer-BFXpRq6d.js} +1 -1
- package/lib/public/assets/{download-BM6Xn22t.js → download-Bd8OGLzi.js} +1 -1
- package/lib/public/assets/{forgot-password-B8WqMqBT.js → forgot-password-CoE7Ugtx.js} +1 -1
- package/lib/public/assets/{index-DauQh6ie.js → index-BiL0Enl_.js} +1 -1
- package/lib/public/assets/{index-ClrpAMAT.js → index-CHOr4JCs.js} +2 -2
- package/lib/public/assets/{input-CyKdLnEx.js → input-CJ8z3IO5.js} +1 -1
- package/lib/public/assets/{line-chart-EIBXwYGo.js → line-chart-CPK0ObjV.js} +1 -1
- package/lib/public/assets/{list-checks-fWsgD9bI.js → list-checks-mXGHUVbB.js} +1 -1
- package/lib/public/assets/{lock-CVCe56TH.js → lock-BSXM1xvq.js} +1 -1
- package/lib/public/assets/{login-BJ8a7yVD.js → login-Ct2iW066.js} +1 -1
- package/lib/public/assets/{maintenance-settings-DcRmTLS6.js → maintenance-settings-DaOs00P6.js} +1 -1
- package/lib/public/assets/{monitor-Bw1YQZnL.js → monitor-D8WQK8md.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-C01jbqkO.js → mouse-pointer-2-Cw2tWzDX.js} +1 -1
- package/lib/public/assets/{network-CBGUjJDJ.js → network-Ba_uFxWo.js} +1 -1
- package/lib/public/assets/{overview-Cxe8aQ7C.js → overview-CxrZ7_6k.js} +1 -1
- package/lib/public/assets/{page-header-B92DKLiq.js → page-header-Cm1XL49f.js} +1 -1
- package/lib/public/assets/{play-Ck0L-0_m.js → play-C0CuKLXj.js} +1 -1
- package/lib/public/assets/{plus-Dq3tCy2N.js → plus-CIIvR_6Z.js} +1 -1
- package/lib/public/assets/{profile-page--dkDiKbr.js → profile-page-BZ_e-Bgw.js} +1 -1
- package/lib/public/assets/{recording-group-store-5BYIFFN9.js → recording-group-store-BWeeNs2_.js} +1 -1
- package/lib/public/assets/{reset-password-KYWlfjia.js → reset-password-lfNz8Rm1.js} +1 -1
- package/lib/public/assets/{runbook-page-mnsxgS1d.js → runbook-page-Cnec3VSx.js} +1 -1
- package/lib/public/assets/{select-BBOIZTYm.js → select-CZuCnold.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-CcI2j_Su.js → selector-detail-redirect-BG3pxFLw.js} +1 -1
- package/lib/public/assets/{selector-health-page-BdJb_x5L.js → selector-health-page-DsyL58tb.js} +1 -1
- package/lib/public/assets/{session-detail-page-BjLaJy8z.js → session-detail-page-ZZyGM413.js} +1 -1
- package/lib/public/assets/{settings-B6cpMhsx.js → settings-DGZ7B_AR.js} +1 -1
- package/lib/public/assets/{stat-tile-BU9e4s36.js → stat-tile-DFhv8JoI.js} +1 -1
- package/lib/public/assets/{tablet-hgbEwrWq.js → tablet-68J5f6zh.js} +1 -1
- package/lib/public/assets/{teams-uiiG4hZM.js → teams-eHSyEpbD.js} +1 -1
- package/lib/public/assets/{trash-2-NK_Iazmg.js → trash-2-CcSsv4fS.js} +1 -1
- package/lib/public/assets/{upload-BUX8TNFi.js → upload-BYf26K71.js} +1 -1
- package/lib/public/assets/{use-builds-data-DZszmFyl.js → use-builds-data-CHW-MhaP.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-BK4B9kRC.js → use-password-reset-mode-DozShXkk.js} +1 -1
- package/lib/public/assets/{users-BU4XBbMT.js → users-BpYnpOl2.js} +1 -1
- package/lib/public/assets/{users-BDh1xjad.js → users-CcSQRoK9.js} +1 -1
- package/lib/public/assets/{video-off-BOjNwT4R.js → video-off-dFKgps73.js} +1 -1
- package/lib/public/assets/{webhook-settings-BMzyUcx5.js → webhook-settings-DD1XjiOD.js} +1 -1
- package/lib/public/assets/{zap-DTWwVMUg.js → zap-COC2tZaH.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/src/app/apiErrors.js +118 -0
- package/lib/src/app/index.js +6 -1
- package/lib/src/app/openapi/control.yaml +3129 -0
- package/lib/src/app/openapi/grid.yaml +2295 -0
- package/lib/src/app/openapi/identity.yaml +2168 -0
- package/lib/src/app/openapi/platform.yaml +2885 -0
- package/lib/src/app/openapi/sessions.yaml +3784 -0
- package/lib/src/app/routers/bug-report.js +4 -1
- package/lib/src/app/routers/config.js +6 -107
- package/lib/src/app/routers/control.js +117 -59
- package/lib/src/app/routers/dashboard.js +13 -7
- package/lib/src/app/routers/grid.js +54 -14
- package/lib/src/app/routers/profile.js +27 -13
- package/lib/src/app/routers/recordings.js +11 -5
- package/lib/src/app/routers/reservation.js +63 -15
- package/lib/src/app/routers/users.js +4 -0
- package/lib/src/app/routers/webhook.js +17 -8
- package/lib/src/app/swagger.js +259 -177
- package/lib/src/data-service/device-service.js +4 -1
- package/lib/src/data-service/deviceFieldOwners.js +1 -0
- package/lib/src/device-managers/AndroidDeviceManager.js +5 -2
- package/lib/src/device-managers/ios/WDAClient.js +32 -32
- package/lib/src/generated/client/edge.js +4 -3
- package/lib/src/generated/client/index-browser.js +1 -0
- package/lib/src/generated/client/index.d.ts +38 -0
- package/lib/src/generated/client/index.js +4 -3
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +2 -0
- package/lib/src/generated/client/wasm.js +1 -0
- package/lib/src/middleware/csrfMiddleware.js +13 -5
- package/lib/src/middleware/rateLimitMiddleware.js +19 -5
- package/lib/src/middleware/roleGuard.js +20 -0
- package/lib/src/services/AIService.js +13 -3
- package/lib/src/services/NotificationService.js +28 -28
- package/lib/src/services/bug-report/BugReportService.js +8 -2
- package/lib/src/services/lease/LeaseService.js +71 -20
- package/lib/src/services/omni-vision/OmniVisionService.js +14 -5
- package/lib/src/services/recording/RecordingOrchestrator.js +16 -2
- package/lib/test/helpers/expressRoutes.js +41 -0
- package/lib/test/integration/team-visibility-control.spec.js +2 -2
- package/lib/test/unit/access-scopes.spec.js +227 -0
- package/lib/test/unit/api-error-handling.spec.js +179 -0
- package/lib/test/unit/bug-report/route.spec.js +29 -0
- package/lib/test/unit/bug-report/service.spec.js +30 -0
- package/lib/test/unit/control-honest-answers.spec.js +134 -0
- package/lib/test/unit/device-allocation-routes.spec.js +232 -0
- package/lib/test/unit/healing-state-endpoints.spec.js +8 -5
- package/lib/test/unit/install-repository-app-team.spec.js +4 -1
- package/lib/test/unit/lease/LeaseService.spec.js +5 -4
- package/lib/test/unit/lease/lease-device-match.spec.js +161 -0
- package/lib/test/unit/lease/lease-session-ownership.spec.js +10 -9
- package/lib/test/unit/omni-vision-failures.spec.js +81 -0
- package/lib/test/unit/openapi-coverage.spec.js +114 -0
- package/lib/test/unit/profile-router.test.js +42 -0
- package/lib/test/unit/rateLimitMiddleware.test.js +49 -0
- package/lib/test/unit/recording-orchestrator.spec.js +80 -0
- package/lib/test/unit/recordings-library-routes.spec.js +37 -0
- package/lib/test/unit/reservation-team-visibility.spec.js +4 -3
- package/lib/test/unit/reset-link.test.js +2 -1
- package/lib/test/unit/stream-ticket-identity.spec.js +1 -1
- package/lib/test/unit/users-router.test.js +14 -1
- package/lib/test/unit/wda-client-failures.spec.js +90 -0
- package/lib/test/unit/webhook-delivery.spec.js +142 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/prisma/migrations/20261004120000_reservation_holder/migration.sql +2 -0
- package/prisma/schema.prisma +2 -0
- package/lib/src/app/swagger-docs.js +0 -1701
|
@@ -0,0 +1,3129 @@
|
|
|
1
|
+
# Control: drive one device.
|
|
2
|
+
# Every route here sits behind the /control guards, in this order: role MEMBER,
|
|
3
|
+
# the devices scope on mutations, the team guard (a hidden device answers as
|
|
4
|
+
# unknown), the ownership guard (409), and on a hub the node-phone gate.
|
|
5
|
+
paths:
|
|
6
|
+
/api/control/{udid}/tap:
|
|
7
|
+
post:
|
|
8
|
+
tags:
|
|
9
|
+
- Control
|
|
10
|
+
parameters:
|
|
11
|
+
- $ref: '#/components/parameters/ControlUdid'
|
|
12
|
+
operationId: tapDevice
|
|
13
|
+
summary: Tap the screen
|
|
14
|
+
description: >-
|
|
15
|
+
Taps the device screen once at (`x`, `y`), in the device's screen points (the `screenWidth`
|
|
16
|
+
× `screenHeight` the device list reports). Android taps through `adb shell input tap`; iOS
|
|
17
|
+
through WebDriverAgent.
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
Needs the `MEMBER` role and the `devices` scope. Refused with `409` while another user holds
|
|
21
|
+
the device (their live preview or recording) or runs an Appium session on it; admins are
|
|
22
|
+
exempt, and your own Appium session on it stays controllable.
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
On a hub, for a node's phone, the request is sent on to that node once (never retried),
|
|
26
|
+
signed for you, and the node's answer is relayed unchanged. A cloud provider's phone answers
|
|
27
|
+
`501`.
|
|
28
|
+
requestBody:
|
|
29
|
+
required: true
|
|
30
|
+
content:
|
|
31
|
+
application/json:
|
|
32
|
+
schema:
|
|
33
|
+
$ref: '#/components/schemas/ControlPoint'
|
|
34
|
+
example:
|
|
35
|
+
x: 196
|
|
36
|
+
'y': 420
|
|
37
|
+
responses:
|
|
38
|
+
'200':
|
|
39
|
+
description: Tapped.
|
|
40
|
+
content:
|
|
41
|
+
application/json:
|
|
42
|
+
schema:
|
|
43
|
+
$ref: '#/components/schemas/Success'
|
|
44
|
+
example:
|
|
45
|
+
success: true
|
|
46
|
+
'400':
|
|
47
|
+
description: This server can't do this on the device's platform, or the udid is malformed.
|
|
48
|
+
content:
|
|
49
|
+
application/json:
|
|
50
|
+
schema:
|
|
51
|
+
$ref: '#/components/schemas/Error'
|
|
52
|
+
examples:
|
|
53
|
+
invalidUdid:
|
|
54
|
+
summary: The udid segment is not valid percent-encoding
|
|
55
|
+
value:
|
|
56
|
+
success: false
|
|
57
|
+
error: invalid_udid
|
|
58
|
+
notSupported:
|
|
59
|
+
summary: No device manager for the platform, or it can't do this
|
|
60
|
+
value:
|
|
61
|
+
error: not_supported
|
|
62
|
+
message: Manager not found or tap not supported
|
|
63
|
+
'401':
|
|
64
|
+
$ref: '#/components/responses/Unauthorized'
|
|
65
|
+
'403':
|
|
66
|
+
$ref: '#/components/responses/ControlForbiddenMutation'
|
|
67
|
+
'404':
|
|
68
|
+
$ref: '#/components/responses/ControlDeviceNotFound'
|
|
69
|
+
'409':
|
|
70
|
+
$ref: '#/components/responses/ControlHeld'
|
|
71
|
+
'429':
|
|
72
|
+
$ref: '#/components/responses/RateLimited'
|
|
73
|
+
'500':
|
|
74
|
+
description: The tap failed on the device.
|
|
75
|
+
content:
|
|
76
|
+
application/json:
|
|
77
|
+
schema:
|
|
78
|
+
$ref: '#/components/schemas/Error'
|
|
79
|
+
example:
|
|
80
|
+
error: 'Command failed: adb -s R5CT32ABCDE shell input tap 196 420'
|
|
81
|
+
'501':
|
|
82
|
+
$ref: '#/components/responses/ControlCloudPhone'
|
|
83
|
+
'502':
|
|
84
|
+
$ref: '#/components/responses/ControlNodeUnreachable'
|
|
85
|
+
'503':
|
|
86
|
+
$ref: '#/components/responses/ControlOwnershipUnavailable'
|
|
87
|
+
'504':
|
|
88
|
+
$ref: '#/components/responses/ControlNodeTimeout'
|
|
89
|
+
/api/control/{udid}/swipe:
|
|
90
|
+
post:
|
|
91
|
+
tags:
|
|
92
|
+
- Control
|
|
93
|
+
parameters:
|
|
94
|
+
- $ref: '#/components/parameters/ControlUdid'
|
|
95
|
+
operationId: swipeDevice
|
|
96
|
+
summary: Swipe across the screen
|
|
97
|
+
description: >-
|
|
98
|
+
Swipes from (`x`, `y`) to (`endX`, `endY`) over `duration` milliseconds, in screen points.
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
Needs the `MEMBER` role and the `devices` scope. Refused with `409` while another user holds
|
|
102
|
+
the device (their live preview or recording) or runs an Appium session on it; admins are
|
|
103
|
+
exempt, and your own Appium session on it stays controllable.
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
On a hub, for a node's phone, the request is sent on to that node once (never retried),
|
|
107
|
+
signed for you, and the node's answer is relayed unchanged. A cloud provider's phone answers
|
|
108
|
+
`501`.
|
|
109
|
+
requestBody:
|
|
110
|
+
required: true
|
|
111
|
+
content:
|
|
112
|
+
application/json:
|
|
113
|
+
schema:
|
|
114
|
+
$ref: '#/components/schemas/ControlSwipeRequest'
|
|
115
|
+
example:
|
|
116
|
+
x: 196
|
|
117
|
+
'y': 700
|
|
118
|
+
endX: 196
|
|
119
|
+
endY: 200
|
|
120
|
+
duration: 300
|
|
121
|
+
responses:
|
|
122
|
+
'200':
|
|
123
|
+
description: Swiped.
|
|
124
|
+
content:
|
|
125
|
+
application/json:
|
|
126
|
+
schema:
|
|
127
|
+
$ref: '#/components/schemas/Success'
|
|
128
|
+
example:
|
|
129
|
+
success: true
|
|
130
|
+
'400':
|
|
131
|
+
description: This server can't do this on the device's platform, or the udid is malformed.
|
|
132
|
+
content:
|
|
133
|
+
application/json:
|
|
134
|
+
schema:
|
|
135
|
+
$ref: '#/components/schemas/Error'
|
|
136
|
+
examples:
|
|
137
|
+
invalidUdid:
|
|
138
|
+
summary: The udid segment is not valid percent-encoding
|
|
139
|
+
value:
|
|
140
|
+
success: false
|
|
141
|
+
error: invalid_udid
|
|
142
|
+
notSupported:
|
|
143
|
+
summary: No device manager for the platform, or it can't do this
|
|
144
|
+
value:
|
|
145
|
+
error: not_supported
|
|
146
|
+
message: Manager not found or swipe not supported
|
|
147
|
+
'401':
|
|
148
|
+
$ref: '#/components/responses/Unauthorized'
|
|
149
|
+
'403':
|
|
150
|
+
$ref: '#/components/responses/ControlForbiddenMutation'
|
|
151
|
+
'404':
|
|
152
|
+
$ref: '#/components/responses/ControlDeviceNotFound'
|
|
153
|
+
'409':
|
|
154
|
+
$ref: '#/components/responses/ControlHeld'
|
|
155
|
+
'429':
|
|
156
|
+
$ref: '#/components/responses/RateLimited'
|
|
157
|
+
'500':
|
|
158
|
+
description: The swipe failed on the device.
|
|
159
|
+
content:
|
|
160
|
+
application/json:
|
|
161
|
+
schema:
|
|
162
|
+
$ref: '#/components/schemas/Error'
|
|
163
|
+
example:
|
|
164
|
+
error: 'WDA request failed: socket hang up'
|
|
165
|
+
'501':
|
|
166
|
+
$ref: '#/components/responses/ControlCloudPhone'
|
|
167
|
+
'502':
|
|
168
|
+
$ref: '#/components/responses/ControlNodeUnreachable'
|
|
169
|
+
'503':
|
|
170
|
+
$ref: '#/components/responses/ControlOwnershipUnavailable'
|
|
171
|
+
'504':
|
|
172
|
+
$ref: '#/components/responses/ControlNodeTimeout'
|
|
173
|
+
/api/control/{udid}/touchAndHold:
|
|
174
|
+
post:
|
|
175
|
+
tags:
|
|
176
|
+
- Control
|
|
177
|
+
parameters:
|
|
178
|
+
- $ref: '#/components/parameters/ControlUdid'
|
|
179
|
+
operationId: touchAndHoldDevice
|
|
180
|
+
summary: Touch and hold a point
|
|
181
|
+
description: >-
|
|
182
|
+
Presses (`x`, `y`) for `duration` milliseconds (a long press). On Android it is a
|
|
183
|
+
zero-length swipe through `adb`.
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
Needs the `MEMBER` role and the `devices` scope. Refused with `409` while another user holds
|
|
187
|
+
the device (their live preview or recording) or runs an Appium session on it; admins are
|
|
188
|
+
exempt, and your own Appium session on it stays controllable.
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
On a hub, for a node's phone, the request is sent on to that node once (never retried),
|
|
192
|
+
signed for you, and the node's answer is relayed unchanged. A cloud provider's phone answers
|
|
193
|
+
`501`.
|
|
194
|
+
requestBody:
|
|
195
|
+
required: true
|
|
196
|
+
content:
|
|
197
|
+
application/json:
|
|
198
|
+
schema:
|
|
199
|
+
$ref: '#/components/schemas/ControlTouchAndHoldRequest'
|
|
200
|
+
example:
|
|
201
|
+
x: 196
|
|
202
|
+
'y': 420
|
|
203
|
+
duration: 1500
|
|
204
|
+
responses:
|
|
205
|
+
'200':
|
|
206
|
+
description: Held and released.
|
|
207
|
+
content:
|
|
208
|
+
application/json:
|
|
209
|
+
schema:
|
|
210
|
+
$ref: '#/components/schemas/Success'
|
|
211
|
+
example:
|
|
212
|
+
success: true
|
|
213
|
+
'400':
|
|
214
|
+
description: This server can't do this on the device's platform, or the udid is malformed.
|
|
215
|
+
content:
|
|
216
|
+
application/json:
|
|
217
|
+
schema:
|
|
218
|
+
$ref: '#/components/schemas/Error'
|
|
219
|
+
examples:
|
|
220
|
+
invalidUdid:
|
|
221
|
+
summary: The udid segment is not valid percent-encoding
|
|
222
|
+
value:
|
|
223
|
+
success: false
|
|
224
|
+
error: invalid_udid
|
|
225
|
+
notSupported:
|
|
226
|
+
summary: No device manager for the platform, or it can't do this
|
|
227
|
+
value:
|
|
228
|
+
error: not_supported
|
|
229
|
+
message: Manager not found or touchAndHold not supported
|
|
230
|
+
'401':
|
|
231
|
+
$ref: '#/components/responses/Unauthorized'
|
|
232
|
+
'403':
|
|
233
|
+
$ref: '#/components/responses/ControlForbiddenMutation'
|
|
234
|
+
'404':
|
|
235
|
+
$ref: '#/components/responses/ControlDeviceNotFound'
|
|
236
|
+
'409':
|
|
237
|
+
$ref: '#/components/responses/ControlHeld'
|
|
238
|
+
'429':
|
|
239
|
+
$ref: '#/components/responses/RateLimited'
|
|
240
|
+
'500':
|
|
241
|
+
description: The long press failed on the device.
|
|
242
|
+
content:
|
|
243
|
+
application/json:
|
|
244
|
+
schema:
|
|
245
|
+
$ref: '#/components/schemas/Error'
|
|
246
|
+
example:
|
|
247
|
+
error: 'WDA request failed: socket hang up'
|
|
248
|
+
'501':
|
|
249
|
+
$ref: '#/components/responses/ControlCloudPhone'
|
|
250
|
+
'502':
|
|
251
|
+
$ref: '#/components/responses/ControlNodeUnreachable'
|
|
252
|
+
'503':
|
|
253
|
+
$ref: '#/components/responses/ControlOwnershipUnavailable'
|
|
254
|
+
'504':
|
|
255
|
+
$ref: '#/components/responses/ControlNodeTimeout'
|
|
256
|
+
/api/control/{udid}/text:
|
|
257
|
+
post:
|
|
258
|
+
tags:
|
|
259
|
+
- Control
|
|
260
|
+
parameters:
|
|
261
|
+
- $ref: '#/components/parameters/ControlUdid'
|
|
262
|
+
operationId: typeTextOnDevice
|
|
263
|
+
summary: Type text into the focused field
|
|
264
|
+
description: >-
|
|
265
|
+
Types `text` into whatever has focus on the device. Android sends it through `adb shell
|
|
266
|
+
input text` (spaces become `%s`), so characters the shell or `input` treat specially may not
|
|
267
|
+
arrive as typed; iOS types through WebDriverAgent.
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
Needs the `MEMBER` role and the `devices` scope. Refused with `409` while another user holds
|
|
271
|
+
the device (their live preview or recording) or runs an Appium session on it; admins are
|
|
272
|
+
exempt, and your own Appium session on it stays controllable.
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
On a hub, for a node's phone, the request is sent on to that node once (never retried),
|
|
276
|
+
signed for you, and the node's answer is relayed unchanged. A cloud provider's phone answers
|
|
277
|
+
`501`.
|
|
278
|
+
requestBody:
|
|
279
|
+
required: true
|
|
280
|
+
content:
|
|
281
|
+
application/json:
|
|
282
|
+
schema:
|
|
283
|
+
$ref: '#/components/schemas/ControlTextRequest'
|
|
284
|
+
example:
|
|
285
|
+
text: qa.user@example.com
|
|
286
|
+
responses:
|
|
287
|
+
'200':
|
|
288
|
+
description: Typed.
|
|
289
|
+
content:
|
|
290
|
+
application/json:
|
|
291
|
+
schema:
|
|
292
|
+
$ref: '#/components/schemas/Success'
|
|
293
|
+
example:
|
|
294
|
+
success: true
|
|
295
|
+
'400':
|
|
296
|
+
description: This server can't do this on the device's platform, or the udid is malformed.
|
|
297
|
+
content:
|
|
298
|
+
application/json:
|
|
299
|
+
schema:
|
|
300
|
+
$ref: '#/components/schemas/Error'
|
|
301
|
+
examples:
|
|
302
|
+
invalidUdid:
|
|
303
|
+
summary: The udid segment is not valid percent-encoding
|
|
304
|
+
value:
|
|
305
|
+
success: false
|
|
306
|
+
error: invalid_udid
|
|
307
|
+
notSupported:
|
|
308
|
+
summary: No device manager for the platform, or it can't do this
|
|
309
|
+
value:
|
|
310
|
+
error: not_supported
|
|
311
|
+
message: Manager not found or typeText not supported
|
|
312
|
+
'401':
|
|
313
|
+
$ref: '#/components/responses/Unauthorized'
|
|
314
|
+
'403':
|
|
315
|
+
$ref: '#/components/responses/ControlForbiddenMutation'
|
|
316
|
+
'404':
|
|
317
|
+
$ref: '#/components/responses/ControlDeviceNotFound'
|
|
318
|
+
'409':
|
|
319
|
+
$ref: '#/components/responses/ControlHeld'
|
|
320
|
+
'429':
|
|
321
|
+
$ref: '#/components/responses/RateLimited'
|
|
322
|
+
'500':
|
|
323
|
+
description: Typing failed on the device.
|
|
324
|
+
content:
|
|
325
|
+
application/json:
|
|
326
|
+
schema:
|
|
327
|
+
$ref: '#/components/schemas/Error'
|
|
328
|
+
example:
|
|
329
|
+
error: 'Command failed: adb -s R5CT32ABCDE shell input text qa.user@example.com'
|
|
330
|
+
'501':
|
|
331
|
+
$ref: '#/components/responses/ControlCloudPhone'
|
|
332
|
+
'502':
|
|
333
|
+
$ref: '#/components/responses/ControlNodeUnreachable'
|
|
334
|
+
'503':
|
|
335
|
+
$ref: '#/components/responses/ControlOwnershipUnavailable'
|
|
336
|
+
'504':
|
|
337
|
+
$ref: '#/components/responses/ControlNodeTimeout'
|
|
338
|
+
/api/control/{udid}/keyevent:
|
|
339
|
+
post:
|
|
340
|
+
tags:
|
|
341
|
+
- Control
|
|
342
|
+
parameters:
|
|
343
|
+
- $ref: '#/components/parameters/ControlUdid'
|
|
344
|
+
operationId: pressDeviceKey
|
|
345
|
+
summary: Press a key or hardware button
|
|
346
|
+
description: >-
|
|
347
|
+
Presses one key.
|
|
348
|
+
|
|
349
|
+
- **Android:** `keyCode` is an Android key code, passed to `adb shell input keyevent` as
|
|
350
|
+
given: a number (`4` Back, `3` Home, `66` Enter, `67` Backspace) or a name such as
|
|
351
|
+
`KEYCODE_BACK`.
|
|
352
|
+
|
|
353
|
+
- **iOS:** `keyCode` is a name, case-insensitive. `home` (or `3`), `volumeUp`/`volume_up`
|
|
354
|
+
and `volumeDown`/`volume_down` press hardware buttons; `enter`, `backspace`, `delete`, `tab`
|
|
355
|
+
and `escape` type that key. Anything else is tried as a WebDriverAgent button name; one
|
|
356
|
+
the iPhone doesn't have answers `400 unsupported_key`. A hardware button or keyboard key
|
|
357
|
+
whose WebDriverAgent call fails (after the fallback: `/wda/homescreen` for `home`,
|
|
358
|
+
`/wda/type` for a keyboard key) answers `500`, and so does any key while WebDriverAgent
|
|
359
|
+
can't be reached.
|
|
360
|
+
|
|
361
|
+
Needs the `MEMBER` role and the `devices` scope. Refused with `409` while another user holds
|
|
362
|
+
the device (their live preview or recording) or runs an Appium session on it; admins are
|
|
363
|
+
exempt, and your own Appium session on it stays controllable.
|
|
364
|
+
|
|
365
|
+
On a hub, for a node's phone, the request is sent on to that node once (never retried),
|
|
366
|
+
signed for you, and the node's answer is relayed unchanged. A cloud provider's phone answers
|
|
367
|
+
`501`.
|
|
368
|
+
requestBody:
|
|
369
|
+
required: true
|
|
370
|
+
content:
|
|
371
|
+
application/json:
|
|
372
|
+
schema:
|
|
373
|
+
$ref: '#/components/schemas/ControlKeyEventRequest'
|
|
374
|
+
example:
|
|
375
|
+
keyCode: 4
|
|
376
|
+
responses:
|
|
377
|
+
'200':
|
|
378
|
+
description: Pressed.
|
|
379
|
+
content:
|
|
380
|
+
application/json:
|
|
381
|
+
schema:
|
|
382
|
+
$ref: '#/components/schemas/Success'
|
|
383
|
+
example:
|
|
384
|
+
success: true
|
|
385
|
+
'400':
|
|
386
|
+
description: >-
|
|
387
|
+
On iOS, a key the iPhone has no equivalent for; this server can't do this on the
|
|
388
|
+
device's platform; or the udid is malformed.
|
|
389
|
+
content:
|
|
390
|
+
application/json:
|
|
391
|
+
schema:
|
|
392
|
+
$ref: '#/components/schemas/Error'
|
|
393
|
+
examples:
|
|
394
|
+
invalidUdid:
|
|
395
|
+
summary: The udid segment is not valid percent-encoding
|
|
396
|
+
value:
|
|
397
|
+
success: false
|
|
398
|
+
error: invalid_udid
|
|
399
|
+
unsupportedKey:
|
|
400
|
+
summary: A key the iPhone has no equivalent for (iOS)
|
|
401
|
+
value:
|
|
402
|
+
error: unsupported_key
|
|
403
|
+
message: The key "f13" isn't available on an iPhone.
|
|
404
|
+
notSupported:
|
|
405
|
+
summary: No device manager for the platform, or it can't do this
|
|
406
|
+
value:
|
|
407
|
+
error: not_supported
|
|
408
|
+
message: Manager not found or pressKey not supported
|
|
409
|
+
'401':
|
|
410
|
+
$ref: '#/components/responses/Unauthorized'
|
|
411
|
+
'403':
|
|
412
|
+
$ref: '#/components/responses/ControlForbiddenMutation'
|
|
413
|
+
'404':
|
|
414
|
+
$ref: '#/components/responses/ControlDeviceNotFound'
|
|
415
|
+
'409':
|
|
416
|
+
$ref: '#/components/responses/ControlHeld'
|
|
417
|
+
'429':
|
|
418
|
+
$ref: '#/components/responses/RateLimited'
|
|
419
|
+
'500':
|
|
420
|
+
description: >-
|
|
421
|
+
The key press failed on the device (on iOS, a hardware button or keyboard key whose
|
|
422
|
+
WebDriverAgent call and fallback both failed, or WebDriverAgent couldn't be reached).
|
|
423
|
+
content:
|
|
424
|
+
application/json:
|
|
425
|
+
schema:
|
|
426
|
+
$ref: '#/components/schemas/Error'
|
|
427
|
+
examples:
|
|
428
|
+
android:
|
|
429
|
+
summary: Android
|
|
430
|
+
value:
|
|
431
|
+
error: 'Command failed: adb -s R5CT32ABCDE shell input keyevent 4'
|
|
432
|
+
ios:
|
|
433
|
+
summary: iOS
|
|
434
|
+
value:
|
|
435
|
+
error: 'WDA request failed: socket hang up'
|
|
436
|
+
'501':
|
|
437
|
+
$ref: '#/components/responses/ControlCloudPhone'
|
|
438
|
+
'502':
|
|
439
|
+
$ref: '#/components/responses/ControlNodeUnreachable'
|
|
440
|
+
'503':
|
|
441
|
+
$ref: '#/components/responses/ControlOwnershipUnavailable'
|
|
442
|
+
'504':
|
|
443
|
+
$ref: '#/components/responses/ControlNodeTimeout'
|
|
444
|
+
/api/control/{udid}/lock:
|
|
445
|
+
post:
|
|
446
|
+
tags:
|
|
447
|
+
- Control
|
|
448
|
+
parameters:
|
|
449
|
+
- $ref: '#/components/parameters/ControlUdid'
|
|
450
|
+
operationId: lockDevice
|
|
451
|
+
summary: Lock the screen
|
|
452
|
+
description: >-
|
|
453
|
+
Locks the device. Android presses the power key (key code 26), which locks a lit screen and
|
|
454
|
+
wakes a dark one. iOS locks through WebDriverAgent, and a WebDriverAgent failure answers
|
|
455
|
+
`500`.
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
Needs the `MEMBER` role and the `devices` scope. Refused with `409` while another user holds
|
|
459
|
+
the device (their live preview or recording) or runs an Appium session on it; admins are
|
|
460
|
+
exempt, and your own Appium session on it stays controllable.
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
On a hub, for a node's phone, the request is sent on to that node once (never retried),
|
|
464
|
+
signed for you, and the node's answer is relayed unchanged. A cloud provider's phone answers
|
|
465
|
+
`501`.
|
|
466
|
+
responses:
|
|
467
|
+
'200':
|
|
468
|
+
description: Locked.
|
|
469
|
+
content:
|
|
470
|
+
application/json:
|
|
471
|
+
schema:
|
|
472
|
+
$ref: '#/components/schemas/Success'
|
|
473
|
+
example:
|
|
474
|
+
success: true
|
|
475
|
+
'400':
|
|
476
|
+
description: This server can't do this on the device's platform, or the udid is malformed.
|
|
477
|
+
content:
|
|
478
|
+
application/json:
|
|
479
|
+
schema:
|
|
480
|
+
$ref: '#/components/schemas/Error'
|
|
481
|
+
examples:
|
|
482
|
+
invalidUdid:
|
|
483
|
+
summary: The udid segment is not valid percent-encoding
|
|
484
|
+
value:
|
|
485
|
+
success: false
|
|
486
|
+
error: invalid_udid
|
|
487
|
+
notSupported:
|
|
488
|
+
summary: No device manager for the platform, or it can't do this
|
|
489
|
+
value:
|
|
490
|
+
error: not_supported
|
|
491
|
+
message: Manager not found or lock not supported
|
|
492
|
+
'401':
|
|
493
|
+
$ref: '#/components/responses/Unauthorized'
|
|
494
|
+
'403':
|
|
495
|
+
$ref: '#/components/responses/ControlForbiddenMutation'
|
|
496
|
+
'404':
|
|
497
|
+
$ref: '#/components/responses/ControlDeviceNotFound'
|
|
498
|
+
'409':
|
|
499
|
+
$ref: '#/components/responses/ControlHeld'
|
|
500
|
+
'429':
|
|
501
|
+
$ref: '#/components/responses/RateLimited'
|
|
502
|
+
'500':
|
|
503
|
+
description: Locking failed on the device (on iOS, WebDriverAgent refused or didn't answer).
|
|
504
|
+
content:
|
|
505
|
+
application/json:
|
|
506
|
+
schema:
|
|
507
|
+
$ref: '#/components/schemas/Error'
|
|
508
|
+
examples:
|
|
509
|
+
android:
|
|
510
|
+
summary: Android
|
|
511
|
+
value:
|
|
512
|
+
error: ADB is not available
|
|
513
|
+
ios:
|
|
514
|
+
summary: iOS
|
|
515
|
+
value:
|
|
516
|
+
error: 'WDA request failed: socket hang up'
|
|
517
|
+
'501':
|
|
518
|
+
$ref: '#/components/responses/ControlCloudPhone'
|
|
519
|
+
'502':
|
|
520
|
+
$ref: '#/components/responses/ControlNodeUnreachable'
|
|
521
|
+
'503':
|
|
522
|
+
$ref: '#/components/responses/ControlOwnershipUnavailable'
|
|
523
|
+
'504':
|
|
524
|
+
$ref: '#/components/responses/ControlNodeTimeout'
|
|
525
|
+
/api/control/{udid}/unlock:
|
|
526
|
+
post:
|
|
527
|
+
tags:
|
|
528
|
+
- Control
|
|
529
|
+
parameters:
|
|
530
|
+
- $ref: '#/components/parameters/ControlUdid'
|
|
531
|
+
operationId: unlockDevice
|
|
532
|
+
summary: Wake and unlock the screen
|
|
533
|
+
description: >-
|
|
534
|
+
Wakes the device and dismisses a lock screen without a passcode. Android sends WAKEUP (224)
|
|
535
|
+
then MENU (82); iOS unlocks through WebDriverAgent, and a WebDriverAgent failure answers
|
|
536
|
+
`500`. A passcode is never entered.
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
Needs the `MEMBER` role and the `devices` scope. Refused with `409` while another user holds
|
|
540
|
+
the device (their live preview or recording) or runs an Appium session on it; admins are
|
|
541
|
+
exempt, and your own Appium session on it stays controllable.
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
On a hub, for a node's phone, the request is sent on to that node once (never retried),
|
|
545
|
+
signed for you, and the node's answer is relayed unchanged. A cloud provider's phone answers
|
|
546
|
+
`501`.
|
|
547
|
+
responses:
|
|
548
|
+
'200':
|
|
549
|
+
description: Woken and unlocked.
|
|
550
|
+
content:
|
|
551
|
+
application/json:
|
|
552
|
+
schema:
|
|
553
|
+
$ref: '#/components/schemas/Success'
|
|
554
|
+
example:
|
|
555
|
+
success: true
|
|
556
|
+
'400':
|
|
557
|
+
description: This server can't do this on the device's platform, or the udid is malformed.
|
|
558
|
+
content:
|
|
559
|
+
application/json:
|
|
560
|
+
schema:
|
|
561
|
+
$ref: '#/components/schemas/Error'
|
|
562
|
+
examples:
|
|
563
|
+
invalidUdid:
|
|
564
|
+
summary: The udid segment is not valid percent-encoding
|
|
565
|
+
value:
|
|
566
|
+
success: false
|
|
567
|
+
error: invalid_udid
|
|
568
|
+
notSupported:
|
|
569
|
+
summary: No device manager for the platform, or it can't do this
|
|
570
|
+
value:
|
|
571
|
+
error: not_supported
|
|
572
|
+
message: Manager not found or unlock not supported
|
|
573
|
+
'401':
|
|
574
|
+
$ref: '#/components/responses/Unauthorized'
|
|
575
|
+
'403':
|
|
576
|
+
$ref: '#/components/responses/ControlForbiddenMutation'
|
|
577
|
+
'404':
|
|
578
|
+
$ref: '#/components/responses/ControlDeviceNotFound'
|
|
579
|
+
'409':
|
|
580
|
+
$ref: '#/components/responses/ControlHeld'
|
|
581
|
+
'429':
|
|
582
|
+
$ref: '#/components/responses/RateLimited'
|
|
583
|
+
'500':
|
|
584
|
+
description: Unlocking failed on the device (on iOS, WebDriverAgent refused or didn't answer).
|
|
585
|
+
content:
|
|
586
|
+
application/json:
|
|
587
|
+
schema:
|
|
588
|
+
$ref: '#/components/schemas/Error'
|
|
589
|
+
examples:
|
|
590
|
+
android:
|
|
591
|
+
summary: Android
|
|
592
|
+
value:
|
|
593
|
+
error: ADB is not available
|
|
594
|
+
ios:
|
|
595
|
+
summary: iOS
|
|
596
|
+
value:
|
|
597
|
+
error: 'WDA request failed: socket hang up'
|
|
598
|
+
'501':
|
|
599
|
+
$ref: '#/components/responses/ControlCloudPhone'
|
|
600
|
+
'502':
|
|
601
|
+
$ref: '#/components/responses/ControlNodeUnreachable'
|
|
602
|
+
'503':
|
|
603
|
+
$ref: '#/components/responses/ControlOwnershipUnavailable'
|
|
604
|
+
'504':
|
|
605
|
+
$ref: '#/components/responses/ControlNodeTimeout'
|
|
606
|
+
/api/control/{udid}/screenshot:
|
|
607
|
+
get:
|
|
608
|
+
tags:
|
|
609
|
+
- Control
|
|
610
|
+
parameters:
|
|
611
|
+
- $ref: '#/components/parameters/ControlUdid'
|
|
612
|
+
operationId: getDeviceScreenshot
|
|
613
|
+
summary: Take a screenshot
|
|
614
|
+
description: >-
|
|
615
|
+
Captures the screen as base64. Android reuses the latest frame of a running MJPEG preview
|
|
616
|
+
when there is one (a JPEG), else runs `screencap` (a PNG). iOS uses go-ios (opening the
|
|
617
|
+
phone's tunnel on iOS 17+ if needed), then WebDriverAgent (PNG).
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
Open to watchers: a busy device can be read by anyone who can see it. Needs the `MEMBER`
|
|
621
|
+
role; any scope will do.
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
On a hub, for a node's phone, the request is sent on to that node once (never retried),
|
|
625
|
+
signed for you, and the node's answer is relayed unchanged. A cloud provider's phone answers
|
|
626
|
+
`501`.
|
|
627
|
+
responses:
|
|
628
|
+
'200':
|
|
629
|
+
description: The screenshot.
|
|
630
|
+
content:
|
|
631
|
+
application/json:
|
|
632
|
+
schema:
|
|
633
|
+
$ref: '#/components/schemas/ControlScreenshot'
|
|
634
|
+
example:
|
|
635
|
+
screenshot: iVBORw0KGgoAAAANSUhEUgAABkAAAAzQCAYAAAB...
|
|
636
|
+
'400':
|
|
637
|
+
description: This server can't do this on the device's platform, or the udid is malformed.
|
|
638
|
+
content:
|
|
639
|
+
application/json:
|
|
640
|
+
schema:
|
|
641
|
+
$ref: '#/components/schemas/Error'
|
|
642
|
+
examples:
|
|
643
|
+
invalidUdid:
|
|
644
|
+
summary: The udid segment is not valid percent-encoding
|
|
645
|
+
value:
|
|
646
|
+
success: false
|
|
647
|
+
error: invalid_udid
|
|
648
|
+
notSupported:
|
|
649
|
+
summary: No device manager for the platform, or it can't do this
|
|
650
|
+
value:
|
|
651
|
+
error: not_supported
|
|
652
|
+
message: Manager not found or screenshot not supported
|
|
653
|
+
'401':
|
|
654
|
+
$ref: '#/components/responses/Unauthorized'
|
|
655
|
+
'403':
|
|
656
|
+
$ref: '#/components/responses/ControlForbiddenRead'
|
|
657
|
+
'404':
|
|
658
|
+
$ref: '#/components/responses/ControlDeviceNotFound'
|
|
659
|
+
'429':
|
|
660
|
+
$ref: '#/components/responses/RateLimited'
|
|
661
|
+
'501':
|
|
662
|
+
$ref: '#/components/responses/ControlCloudPhone'
|
|
663
|
+
'502':
|
|
664
|
+
description: >-
|
|
665
|
+
The device didn't give a usable image, or, on a hub, the phone's node couldn't be
|
|
666
|
+
reached (`node_unreachable`).
|
|
667
|
+
content:
|
|
668
|
+
application/json:
|
|
669
|
+
schema:
|
|
670
|
+
$ref: '#/components/schemas/Error'
|
|
671
|
+
examples:
|
|
672
|
+
captureFailed:
|
|
673
|
+
summary: No usable image
|
|
674
|
+
value:
|
|
675
|
+
error: >-
|
|
676
|
+
Screenshot capture failed. Device may be busy or WDA is unresponsive. Try
|
|
677
|
+
again.
|
|
678
|
+
nodeUnreachable:
|
|
679
|
+
summary: The node can't be reached
|
|
680
|
+
value:
|
|
681
|
+
success: false
|
|
682
|
+
error: node_unreachable
|
|
683
|
+
message: 'Xenon could not reach node http://10.0.4.21:4723 for this phone: ECONNREFUSED'
|
|
684
|
+
'503':
|
|
685
|
+
$ref: '#/components/responses/ControlOwnershipUnavailable'
|
|
686
|
+
'504':
|
|
687
|
+
$ref: '#/components/responses/ControlNodeTimeout'
|
|
688
|
+
/api/control/{udid}/display:
|
|
689
|
+
get:
|
|
690
|
+
tags:
|
|
691
|
+
- Control
|
|
692
|
+
parameters:
|
|
693
|
+
- $ref: '#/components/parameters/ControlUdid'
|
|
694
|
+
operationId: getDeviceDisplayState
|
|
695
|
+
summary: Tell whether the screen is lit
|
|
696
|
+
description: >-
|
|
697
|
+
Says whether the panel is on, so a black preview can be told apart from a sleeping device.
|
|
698
|
+
Android reads `dumpsys power` (cached for 2 seconds per device); platforms with no reader,
|
|
699
|
+
iOS included, answer `unknown`. Never fails on the device side: an unreadable device is
|
|
700
|
+
`unknown`.
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
Open to watchers. Needs the `MEMBER` role; any scope will do.
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
On a hub, for a node's phone, the request is sent on to that node once (never retried),
|
|
707
|
+
signed for you, and the node's answer is relayed unchanged. A cloud provider's phone answers
|
|
708
|
+
`501`.
|
|
709
|
+
responses:
|
|
710
|
+
'200':
|
|
711
|
+
description: The display state.
|
|
712
|
+
content:
|
|
713
|
+
application/json:
|
|
714
|
+
schema:
|
|
715
|
+
$ref: '#/components/schemas/ControlDisplayState'
|
|
716
|
+
example:
|
|
717
|
+
state: 'off'
|
|
718
|
+
'400':
|
|
719
|
+
description: The udid is malformed.
|
|
720
|
+
content:
|
|
721
|
+
application/json:
|
|
722
|
+
schema:
|
|
723
|
+
$ref: '#/components/schemas/Error'
|
|
724
|
+
examples:
|
|
725
|
+
invalidUdid:
|
|
726
|
+
summary: The udid segment is not valid percent-encoding
|
|
727
|
+
value:
|
|
728
|
+
success: false
|
|
729
|
+
error: invalid_udid
|
|
730
|
+
'401':
|
|
731
|
+
$ref: '#/components/responses/Unauthorized'
|
|
732
|
+
'403':
|
|
733
|
+
$ref: '#/components/responses/ControlForbiddenRead'
|
|
734
|
+
'404':
|
|
735
|
+
$ref: '#/components/responses/ControlDeviceNotFound'
|
|
736
|
+
'429':
|
|
737
|
+
$ref: '#/components/responses/RateLimited'
|
|
738
|
+
'501':
|
|
739
|
+
$ref: '#/components/responses/ControlCloudPhone'
|
|
740
|
+
'502':
|
|
741
|
+
$ref: '#/components/responses/ControlNodeUnreachable'
|
|
742
|
+
'503':
|
|
743
|
+
$ref: '#/components/responses/ControlOwnershipUnavailable'
|
|
744
|
+
'504':
|
|
745
|
+
$ref: '#/components/responses/ControlNodeTimeout'
|
|
746
|
+
/api/control/{udid}/clipboard:
|
|
747
|
+
get:
|
|
748
|
+
tags:
|
|
749
|
+
- Control
|
|
750
|
+
parameters:
|
|
751
|
+
- $ref: '#/components/parameters/ControlUdid'
|
|
752
|
+
operationId: getDeviceClipboard
|
|
753
|
+
summary: Read the device's clipboard
|
|
754
|
+
description: >-
|
|
755
|
+
Returns the clipboard's plain text. Android reads it through the Appium Settings app, which
|
|
756
|
+
it makes the input method for a moment and then restores; iOS reads it through
|
|
757
|
+
WebDriverAgent (on a real iPhone WebDriverAgent is brought to the foreground first, as iOS
|
|
758
|
+
requires).
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
Unlike other reads this one is ownership-checked: while another user holds the device or
|
|
762
|
+
runs a session on it you get `409`, since the clipboard is their work (often a password or
|
|
763
|
+
code). Needs the `MEMBER` role; any scope will do.
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
On a hub, for a node's phone, the request is sent on to that node once (never retried),
|
|
767
|
+
signed for you, and the node's answer is relayed unchanged. A cloud provider's phone answers
|
|
768
|
+
`501`.
|
|
769
|
+
responses:
|
|
770
|
+
'200':
|
|
771
|
+
description: The clipboard text (empty when the clipboard is empty).
|
|
772
|
+
content:
|
|
773
|
+
application/json:
|
|
774
|
+
schema:
|
|
775
|
+
$ref: '#/components/schemas/ControlClipboard'
|
|
776
|
+
example:
|
|
777
|
+
content: https://example.com/reset?code=4821
|
|
778
|
+
'400':
|
|
779
|
+
description: This server can't do this on the device's platform, or the udid is malformed.
|
|
780
|
+
content:
|
|
781
|
+
application/json:
|
|
782
|
+
schema:
|
|
783
|
+
$ref: '#/components/schemas/Error'
|
|
784
|
+
examples:
|
|
785
|
+
invalidUdid:
|
|
786
|
+
summary: The udid segment is not valid percent-encoding
|
|
787
|
+
value:
|
|
788
|
+
success: false
|
|
789
|
+
error: invalid_udid
|
|
790
|
+
notSupported:
|
|
791
|
+
summary: No device manager for the platform, or it can't do this
|
|
792
|
+
value:
|
|
793
|
+
error: not_supported
|
|
794
|
+
message: Manager not found or getClipboard not supported
|
|
795
|
+
'401':
|
|
796
|
+
$ref: '#/components/responses/Unauthorized'
|
|
797
|
+
'403':
|
|
798
|
+
$ref: '#/components/responses/ControlForbiddenRead'
|
|
799
|
+
'404':
|
|
800
|
+
$ref: '#/components/responses/ControlDeviceNotFound'
|
|
801
|
+
'409':
|
|
802
|
+
$ref: '#/components/responses/ControlHeld'
|
|
803
|
+
'429':
|
|
804
|
+
$ref: '#/components/responses/RateLimited'
|
|
805
|
+
'500':
|
|
806
|
+
description: Reading the clipboard failed on the device.
|
|
807
|
+
content:
|
|
808
|
+
application/json:
|
|
809
|
+
schema:
|
|
810
|
+
$ref: '#/components/schemas/Error'
|
|
811
|
+
example:
|
|
812
|
+
error: Appium Settings is not installed on R5CT32ABCDE
|
|
813
|
+
'501':
|
|
814
|
+
$ref: '#/components/responses/ControlCloudPhone'
|
|
815
|
+
'502':
|
|
816
|
+
$ref: '#/components/responses/ControlNodeUnreachable'
|
|
817
|
+
'503':
|
|
818
|
+
$ref: '#/components/responses/ControlOwnershipUnavailable'
|
|
819
|
+
'504':
|
|
820
|
+
$ref: '#/components/responses/ControlNodeTimeout'
|
|
821
|
+
post:
|
|
822
|
+
tags:
|
|
823
|
+
- Control
|
|
824
|
+
parameters:
|
|
825
|
+
- $ref: '#/components/parameters/ControlUdid'
|
|
826
|
+
operationId: setDeviceClipboard
|
|
827
|
+
summary: Set the device's clipboard
|
|
828
|
+
description: >-
|
|
829
|
+
Puts `content` on the clipboard as plain text. **iOS only:** Android can't set the clipboard
|
|
830
|
+
outside a test session and answers `501`. A WebDriverAgent failure on iOS answers `500`.
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
Needs the `MEMBER` role and the `devices` scope. Refused with `409` while another user holds
|
|
834
|
+
the device (their live preview or recording) or runs an Appium session on it; admins are
|
|
835
|
+
exempt, and your own Appium session on it stays controllable.
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
On a hub, for a node's phone, the request is sent on to that node once (never retried),
|
|
839
|
+
signed for you, and the node's answer is relayed unchanged. A cloud provider's phone answers
|
|
840
|
+
`501`.
|
|
841
|
+
requestBody:
|
|
842
|
+
required: true
|
|
843
|
+
content:
|
|
844
|
+
application/json:
|
|
845
|
+
schema:
|
|
846
|
+
$ref: '#/components/schemas/ControlClipboard'
|
|
847
|
+
example:
|
|
848
|
+
content: Hunter2-staging
|
|
849
|
+
responses:
|
|
850
|
+
'200':
|
|
851
|
+
description: Set.
|
|
852
|
+
content:
|
|
853
|
+
application/json:
|
|
854
|
+
schema:
|
|
855
|
+
$ref: '#/components/schemas/Success'
|
|
856
|
+
example:
|
|
857
|
+
success: true
|
|
858
|
+
'400':
|
|
859
|
+
description: This server can't do this on the device's platform, or the udid is malformed.
|
|
860
|
+
content:
|
|
861
|
+
application/json:
|
|
862
|
+
schema:
|
|
863
|
+
$ref: '#/components/schemas/Error'
|
|
864
|
+
examples:
|
|
865
|
+
invalidUdid:
|
|
866
|
+
summary: The udid segment is not valid percent-encoding
|
|
867
|
+
value:
|
|
868
|
+
success: false
|
|
869
|
+
error: invalid_udid
|
|
870
|
+
notSupported:
|
|
871
|
+
summary: No device manager for the platform, or it can't do this
|
|
872
|
+
value:
|
|
873
|
+
error: not_supported
|
|
874
|
+
message: Manager not found or setClipboard not supported
|
|
875
|
+
'401':
|
|
876
|
+
$ref: '#/components/responses/Unauthorized'
|
|
877
|
+
'403':
|
|
878
|
+
$ref: '#/components/responses/ControlForbiddenMutation'
|
|
879
|
+
'404':
|
|
880
|
+
$ref: '#/components/responses/ControlDeviceNotFound'
|
|
881
|
+
'409':
|
|
882
|
+
$ref: '#/components/responses/ControlHeld'
|
|
883
|
+
'429':
|
|
884
|
+
$ref: '#/components/responses/RateLimited'
|
|
885
|
+
'500':
|
|
886
|
+
description: Setting the clipboard failed on the device.
|
|
887
|
+
content:
|
|
888
|
+
application/json:
|
|
889
|
+
schema:
|
|
890
|
+
$ref: '#/components/schemas/Error'
|
|
891
|
+
example:
|
|
892
|
+
error: 'WDA request failed: socket hang up'
|
|
893
|
+
'501':
|
|
894
|
+
description: >-
|
|
895
|
+
Android can't set its clipboard from here; or, on a hub, the phone is a cloud provider's
|
|
896
|
+
(`not_available_for_cloud_phone`).
|
|
897
|
+
content:
|
|
898
|
+
application/json:
|
|
899
|
+
schema:
|
|
900
|
+
$ref: '#/components/schemas/Error'
|
|
901
|
+
examples:
|
|
902
|
+
android:
|
|
903
|
+
summary: An Android device
|
|
904
|
+
value:
|
|
905
|
+
error: >-
|
|
906
|
+
Android doesn’t allow setting the clipboard from here: Appium Settings can
|
|
907
|
+
only read it.
|
|
908
|
+
cloud:
|
|
909
|
+
summary: A cloud provider's phone, on a hub
|
|
910
|
+
value:
|
|
911
|
+
success: false
|
|
912
|
+
error: not_available_for_cloud_phone
|
|
913
|
+
message: This phone is a cloud provider's. Device control isn't available for it here.
|
|
914
|
+
'502':
|
|
915
|
+
$ref: '#/components/responses/ControlNodeUnreachable'
|
|
916
|
+
'503':
|
|
917
|
+
$ref: '#/components/responses/ControlOwnershipUnavailable'
|
|
918
|
+
'504':
|
|
919
|
+
$ref: '#/components/responses/ControlNodeTimeout'
|
|
920
|
+
/api/control/{udid}/apps:
|
|
921
|
+
get:
|
|
922
|
+
tags:
|
|
923
|
+
- Control
|
|
924
|
+
parameters:
|
|
925
|
+
- $ref: '#/components/parameters/ControlUdid'
|
|
926
|
+
operationId: listDeviceApps
|
|
927
|
+
summary: List the apps installed on the device
|
|
928
|
+
description: >-
|
|
929
|
+
Returns the installed apps' package names (Android: third-party packages from `pm list
|
|
930
|
+
packages -3`) or bundle ids (iOS: `ideviceinstaller`, falling back to go-ios).
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
Open to watchers. Needs the `MEMBER` role; any scope will do.
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
On a hub, for a node's phone, the request is sent on to that node once (never retried),
|
|
937
|
+
signed for you, and the node's answer is relayed unchanged. A cloud provider's phone answers
|
|
938
|
+
`501`.
|
|
939
|
+
responses:
|
|
940
|
+
'200':
|
|
941
|
+
description: Package names or bundle ids.
|
|
942
|
+
content:
|
|
943
|
+
application/json:
|
|
944
|
+
schema:
|
|
945
|
+
type: array
|
|
946
|
+
items:
|
|
947
|
+
type: string
|
|
948
|
+
example:
|
|
949
|
+
- com.example.shop
|
|
950
|
+
- com.example.shop.debug
|
|
951
|
+
- io.appium.settings
|
|
952
|
+
'400':
|
|
953
|
+
description: This server can't do this on the device's platform, or the udid is malformed.
|
|
954
|
+
content:
|
|
955
|
+
application/json:
|
|
956
|
+
schema:
|
|
957
|
+
$ref: '#/components/schemas/Error'
|
|
958
|
+
examples:
|
|
959
|
+
invalidUdid:
|
|
960
|
+
summary: The udid segment is not valid percent-encoding
|
|
961
|
+
value:
|
|
962
|
+
success: false
|
|
963
|
+
error: invalid_udid
|
|
964
|
+
notSupported:
|
|
965
|
+
summary: No device manager for the platform, or it can't do this
|
|
966
|
+
value:
|
|
967
|
+
error: not_supported
|
|
968
|
+
message: Manager not found or listApps not supported
|
|
969
|
+
'401':
|
|
970
|
+
$ref: '#/components/responses/Unauthorized'
|
|
971
|
+
'403':
|
|
972
|
+
$ref: '#/components/responses/ControlForbiddenRead'
|
|
973
|
+
'404':
|
|
974
|
+
$ref: '#/components/responses/ControlDeviceNotFound'
|
|
975
|
+
'429':
|
|
976
|
+
$ref: '#/components/responses/RateLimited'
|
|
977
|
+
'500':
|
|
978
|
+
description: Listing apps failed on the device.
|
|
979
|
+
content:
|
|
980
|
+
application/json:
|
|
981
|
+
schema:
|
|
982
|
+
$ref: '#/components/schemas/Error'
|
|
983
|
+
example:
|
|
984
|
+
error: 'Command failed: ideviceinstaller -u 00008110-00084CE80E51401E list'
|
|
985
|
+
'501':
|
|
986
|
+
$ref: '#/components/responses/ControlCloudPhone'
|
|
987
|
+
'502':
|
|
988
|
+
$ref: '#/components/responses/ControlNodeUnreachable'
|
|
989
|
+
'503':
|
|
990
|
+
$ref: '#/components/responses/ControlOwnershipUnavailable'
|
|
991
|
+
'504':
|
|
992
|
+
$ref: '#/components/responses/ControlNodeTimeout'
|
|
993
|
+
/api/control/{udid}/install:
|
|
994
|
+
post:
|
|
995
|
+
tags:
|
|
996
|
+
- Control
|
|
997
|
+
parameters:
|
|
998
|
+
- $ref: '#/components/parameters/ControlUdid'
|
|
999
|
+
operationId: installAppFromPath
|
|
1000
|
+
summary: Install an app from a path on the server
|
|
1001
|
+
description: >-
|
|
1002
|
+
Installs the `.apk`, `.ipa` or `.app` at `appPath`, a path on **this server's** file system
|
|
1003
|
+
(Android installs with `adb install -r`, replacing an existing version). To send a file from
|
|
1004
|
+
your machine use `upload-install`; to install from the app library use
|
|
1005
|
+
`install-repository-app`.
|
|
1006
|
+
|
|
1007
|
+
|
|
1008
|
+
Needs the `MEMBER` role and the `devices` scope. Refused with `409` while another user holds
|
|
1009
|
+
the device (their live preview or recording) or runs an Appium session on it; admins are
|
|
1010
|
+
exempt, and your own Appium session on it stays controllable.
|
|
1011
|
+
|
|
1012
|
+
|
|
1013
|
+
On a hub this isn't available for a node's phone (a path names a file on one machine): it
|
|
1014
|
+
answers `501 not_available_through_hub`. A cloud provider's phone answers `501` too.
|
|
1015
|
+
requestBody:
|
|
1016
|
+
required: true
|
|
1017
|
+
content:
|
|
1018
|
+
application/json:
|
|
1019
|
+
schema:
|
|
1020
|
+
$ref: '#/components/schemas/ControlInstallPathRequest'
|
|
1021
|
+
example:
|
|
1022
|
+
appPath: /opt/builds/shop-2.14.0-debug.apk
|
|
1023
|
+
responses:
|
|
1024
|
+
'200':
|
|
1025
|
+
description: Installed.
|
|
1026
|
+
content:
|
|
1027
|
+
application/json:
|
|
1028
|
+
schema:
|
|
1029
|
+
$ref: '#/components/schemas/Success'
|
|
1030
|
+
example:
|
|
1031
|
+
success: true
|
|
1032
|
+
'400':
|
|
1033
|
+
description: This server can't do this on the device's platform, or the udid is malformed.
|
|
1034
|
+
content:
|
|
1035
|
+
application/json:
|
|
1036
|
+
schema:
|
|
1037
|
+
$ref: '#/components/schemas/Error'
|
|
1038
|
+
examples:
|
|
1039
|
+
invalidUdid:
|
|
1040
|
+
summary: The udid segment is not valid percent-encoding
|
|
1041
|
+
value:
|
|
1042
|
+
success: false
|
|
1043
|
+
error: invalid_udid
|
|
1044
|
+
notSupported:
|
|
1045
|
+
summary: No device manager for the platform, or it can't do this
|
|
1046
|
+
value:
|
|
1047
|
+
error: not_supported
|
|
1048
|
+
message: Manager not found or installApp not supported
|
|
1049
|
+
'401':
|
|
1050
|
+
$ref: '#/components/responses/Unauthorized'
|
|
1051
|
+
'403':
|
|
1052
|
+
$ref: '#/components/responses/ControlForbiddenMutation'
|
|
1053
|
+
'404':
|
|
1054
|
+
$ref: '#/components/responses/ControlDeviceNotFound'
|
|
1055
|
+
'409':
|
|
1056
|
+
$ref: '#/components/responses/ControlHeld'
|
|
1057
|
+
'429':
|
|
1058
|
+
$ref: '#/components/responses/RateLimited'
|
|
1059
|
+
'500':
|
|
1060
|
+
description: The install failed on the device.
|
|
1061
|
+
content:
|
|
1062
|
+
application/json:
|
|
1063
|
+
schema:
|
|
1064
|
+
$ref: '#/components/schemas/Error'
|
|
1065
|
+
example:
|
|
1066
|
+
error: 'Command failed: adb -s R5CT32ABCDE install -r /opt/builds/shop-2.14.0-debug.apk'
|
|
1067
|
+
'501':
|
|
1068
|
+
description: >-
|
|
1069
|
+
On a hub: the phone is a node's, and the hub doesn't pass this action on, or it is a
|
|
1070
|
+
cloud provider's.
|
|
1071
|
+
content:
|
|
1072
|
+
application/json:
|
|
1073
|
+
schema:
|
|
1074
|
+
$ref: '#/components/schemas/Error'
|
|
1075
|
+
examples:
|
|
1076
|
+
node:
|
|
1077
|
+
summary: A node's phone
|
|
1078
|
+
value:
|
|
1079
|
+
success: false
|
|
1080
|
+
error: not_available_through_hub
|
|
1081
|
+
message: >-
|
|
1082
|
+
This phone is on node http://10.0.4.21:4723, and the hub doesn't pass install
|
|
1083
|
+
on to nodes yet.
|
|
1084
|
+
cloud:
|
|
1085
|
+
summary: A cloud provider's phone
|
|
1086
|
+
value:
|
|
1087
|
+
success: false
|
|
1088
|
+
error: not_available_for_cloud_phone
|
|
1089
|
+
message: This phone is a cloud provider's. Device control isn't available for it here.
|
|
1090
|
+
'503':
|
|
1091
|
+
$ref: '#/components/responses/ControlOwnershipUnavailable'
|
|
1092
|
+
/api/control/{udid}/install-repository-app:
|
|
1093
|
+
post:
|
|
1094
|
+
tags:
|
|
1095
|
+
- Control
|
|
1096
|
+
parameters:
|
|
1097
|
+
- $ref: '#/components/parameters/ControlUdid'
|
|
1098
|
+
operationId: installLibraryApp
|
|
1099
|
+
summary: Install an app from the app library
|
|
1100
|
+
description: >-
|
|
1101
|
+
Installs an uploaded app (see Applications) by its id. The app must be visible to you:
|
|
1102
|
+
another team's app answers `404` exactly like an unknown one. The answer comes when the
|
|
1103
|
+
install has finished.
|
|
1104
|
+
|
|
1105
|
+
|
|
1106
|
+
Needs the `MEMBER` role and the `devices` scope. Refused with `409` while another user holds
|
|
1107
|
+
the device (their live preview or recording) or runs an Appium session on it; admins are
|
|
1108
|
+
exempt, and your own Appium session on it stays controllable.
|
|
1109
|
+
|
|
1110
|
+
|
|
1111
|
+
On a hub, for a node's phone, the app file (which only the hub has) is streamed to the
|
|
1112
|
+
node's `upload-install` and the node's answer is relayed; the node has up to 15 minutes to
|
|
1113
|
+
start answering. A cloud provider's phone answers `501`.
|
|
1114
|
+
requestBody:
|
|
1115
|
+
required: true
|
|
1116
|
+
content:
|
|
1117
|
+
application/json:
|
|
1118
|
+
schema:
|
|
1119
|
+
$ref: '#/components/schemas/ControlInstallLibraryAppRequest'
|
|
1120
|
+
example:
|
|
1121
|
+
appId: b3c1f0e2-6a8d-4b6e-9a51-2f7d0c9e4a13
|
|
1122
|
+
responses:
|
|
1123
|
+
'200':
|
|
1124
|
+
description: Installed.
|
|
1125
|
+
content:
|
|
1126
|
+
application/json:
|
|
1127
|
+
schema:
|
|
1128
|
+
$ref: '#/components/schemas/ControlInstallResult'
|
|
1129
|
+
example:
|
|
1130
|
+
success: true
|
|
1131
|
+
message: Installed Shop
|
|
1132
|
+
'400':
|
|
1133
|
+
description: >-
|
|
1134
|
+
`appId` is missing, empty or not a string, this server can't do this on the device's
|
|
1135
|
+
platform, or the udid is malformed.
|
|
1136
|
+
content:
|
|
1137
|
+
application/json:
|
|
1138
|
+
schema:
|
|
1139
|
+
$ref: '#/components/schemas/Error'
|
|
1140
|
+
examples:
|
|
1141
|
+
invalidUdid:
|
|
1142
|
+
summary: The udid segment is not valid percent-encoding
|
|
1143
|
+
value:
|
|
1144
|
+
success: false
|
|
1145
|
+
error: invalid_udid
|
|
1146
|
+
noAppId:
|
|
1147
|
+
summary: No `appId`
|
|
1148
|
+
value:
|
|
1149
|
+
error: bad_request
|
|
1150
|
+
message: appId is required
|
|
1151
|
+
notSupported:
|
|
1152
|
+
summary: No device manager for the platform, or it can't do this
|
|
1153
|
+
value:
|
|
1154
|
+
error: not_supported
|
|
1155
|
+
message: Manager not found or installApp not supported
|
|
1156
|
+
'401':
|
|
1157
|
+
$ref: '#/components/responses/Unauthorized'
|
|
1158
|
+
'403':
|
|
1159
|
+
$ref: '#/components/responses/ControlForbiddenMutation'
|
|
1160
|
+
'404':
|
|
1161
|
+
description: No such device or app, or one of them is outside your teams (each answers as unknown).
|
|
1162
|
+
content:
|
|
1163
|
+
application/json:
|
|
1164
|
+
schema:
|
|
1165
|
+
$ref: '#/components/schemas/Error'
|
|
1166
|
+
examples:
|
|
1167
|
+
device:
|
|
1168
|
+
summary: Unknown or hidden device
|
|
1169
|
+
value:
|
|
1170
|
+
error: not_found
|
|
1171
|
+
message: Device not found
|
|
1172
|
+
app:
|
|
1173
|
+
summary: Unknown or hidden app
|
|
1174
|
+
value:
|
|
1175
|
+
error: not_found
|
|
1176
|
+
message: App not found in repository
|
|
1177
|
+
'409':
|
|
1178
|
+
$ref: '#/components/responses/ControlHeld'
|
|
1179
|
+
'429':
|
|
1180
|
+
$ref: '#/components/responses/RateLimited'
|
|
1181
|
+
'500':
|
|
1182
|
+
description: The install failed on the device.
|
|
1183
|
+
content:
|
|
1184
|
+
application/json:
|
|
1185
|
+
schema:
|
|
1186
|
+
$ref: '#/components/schemas/Error'
|
|
1187
|
+
example:
|
|
1188
|
+
error: >-
|
|
1189
|
+
Command failed: adb -s R5CT32ABCDE install -r
|
|
1190
|
+
/home/xenon/.cache/xenon/apps/shop-2.14.0.apk
|
|
1191
|
+
'501':
|
|
1192
|
+
$ref: '#/components/responses/ControlCloudPhone'
|
|
1193
|
+
'502':
|
|
1194
|
+
$ref: '#/components/responses/ControlNodeUnreachable'
|
|
1195
|
+
'503':
|
|
1196
|
+
$ref: '#/components/responses/ControlOwnershipUnavailable'
|
|
1197
|
+
'504':
|
|
1198
|
+
$ref: '#/components/responses/ControlNodeTimeout'
|
|
1199
|
+
/api/control/{udid}/upload-install:
|
|
1200
|
+
post:
|
|
1201
|
+
tags:
|
|
1202
|
+
- Control
|
|
1203
|
+
parameters:
|
|
1204
|
+
- $ref: '#/components/parameters/ControlUdid'
|
|
1205
|
+
operationId: uploadAndInstallApp
|
|
1206
|
+
summary: Upload an app file and install it
|
|
1207
|
+
description: >-
|
|
1208
|
+
Uploads an `.apk`, `.ipa` or zipped `.app` as the multipart field `app` (at most 4 GiB) and
|
|
1209
|
+
installs it. The file is written to a temporary file, installed, and deleted before the
|
|
1210
|
+
answer is sent, whether the install worked or not. The answer comes when the install has
|
|
1211
|
+
finished.
|
|
1212
|
+
|
|
1213
|
+
|
|
1214
|
+
Needs the `MEMBER` role and the `devices` scope. Refused with `409` while another user holds
|
|
1215
|
+
the device (their live preview or recording) or runs an Appium session on it; admins are
|
|
1216
|
+
exempt, and your own Appium session on it stays controllable.
|
|
1217
|
+
|
|
1218
|
+
|
|
1219
|
+
On a hub, for a node's phone, the upload is streamed on to the node as it arrives, and the
|
|
1220
|
+
node has up to 15 minutes to start answering. A cloud provider's phone answers `501`.
|
|
1221
|
+
requestBody:
|
|
1222
|
+
required: true
|
|
1223
|
+
content:
|
|
1224
|
+
multipart/form-data:
|
|
1225
|
+
schema:
|
|
1226
|
+
$ref: '#/components/schemas/ControlUploadInstallRequest'
|
|
1227
|
+
responses:
|
|
1228
|
+
'200':
|
|
1229
|
+
description: Installed.
|
|
1230
|
+
content:
|
|
1231
|
+
application/json:
|
|
1232
|
+
schema:
|
|
1233
|
+
$ref: '#/components/schemas/ControlInstallResult'
|
|
1234
|
+
example:
|
|
1235
|
+
success: true
|
|
1236
|
+
message: App shop-2.14.0-debug.apk installed successfully
|
|
1237
|
+
'400':
|
|
1238
|
+
description: No file, no `app` field, no installer for this platform, or a malformed udid.
|
|
1239
|
+
content:
|
|
1240
|
+
application/json:
|
|
1241
|
+
schema:
|
|
1242
|
+
$ref: '#/components/schemas/Error'
|
|
1243
|
+
examples:
|
|
1244
|
+
invalidUdid:
|
|
1245
|
+
summary: The udid segment is not valid percent-encoding
|
|
1246
|
+
value:
|
|
1247
|
+
success: false
|
|
1248
|
+
error: invalid_udid
|
|
1249
|
+
noFiles:
|
|
1250
|
+
summary: No file in the request
|
|
1251
|
+
value:
|
|
1252
|
+
error: bad_request
|
|
1253
|
+
message: No files were uploaded.
|
|
1254
|
+
noAppField:
|
|
1255
|
+
summary: A file, but not in the `app` field
|
|
1256
|
+
value:
|
|
1257
|
+
error: bad_request
|
|
1258
|
+
message: File "app" is required
|
|
1259
|
+
notSupported:
|
|
1260
|
+
summary: No installer for this platform
|
|
1261
|
+
value:
|
|
1262
|
+
error: not_supported
|
|
1263
|
+
message: Manager not found or installApp not supported
|
|
1264
|
+
'401':
|
|
1265
|
+
$ref: '#/components/responses/Unauthorized'
|
|
1266
|
+
'403':
|
|
1267
|
+
$ref: '#/components/responses/ControlForbiddenMutation'
|
|
1268
|
+
'404':
|
|
1269
|
+
$ref: '#/components/responses/ControlDeviceNotFound'
|
|
1270
|
+
'409':
|
|
1271
|
+
$ref: '#/components/responses/ControlHeld'
|
|
1272
|
+
'413':
|
|
1273
|
+
description: The file is larger than 4 GiB.
|
|
1274
|
+
content:
|
|
1275
|
+
text/html:
|
|
1276
|
+
schema:
|
|
1277
|
+
type: string
|
|
1278
|
+
example: File size limit has been reached
|
|
1279
|
+
'429':
|
|
1280
|
+
$ref: '#/components/responses/RateLimited'
|
|
1281
|
+
'500':
|
|
1282
|
+
description: The install failed on the device.
|
|
1283
|
+
content:
|
|
1284
|
+
application/json:
|
|
1285
|
+
schema:
|
|
1286
|
+
$ref: '#/components/schemas/Error'
|
|
1287
|
+
example:
|
|
1288
|
+
error: >-
|
|
1289
|
+
Command failed: adb -s R5CT32ABCDE install -r
|
|
1290
|
+
/tmp/xenon-uploads/1759571234567-shop-2.14.0-debug.apk
|
|
1291
|
+
'501':
|
|
1292
|
+
$ref: '#/components/responses/ControlCloudPhone'
|
|
1293
|
+
'502':
|
|
1294
|
+
$ref: '#/components/responses/ControlNodeUnreachable'
|
|
1295
|
+
'503':
|
|
1296
|
+
$ref: '#/components/responses/ControlOwnershipUnavailable'
|
|
1297
|
+
'504':
|
|
1298
|
+
$ref: '#/components/responses/ControlNodeTimeout'
|
|
1299
|
+
/api/control/{udid}/uninstall:
|
|
1300
|
+
post:
|
|
1301
|
+
tags:
|
|
1302
|
+
- Control
|
|
1303
|
+
parameters:
|
|
1304
|
+
- $ref: '#/components/parameters/ControlUdid'
|
|
1305
|
+
operationId: uninstallDeviceApp
|
|
1306
|
+
summary: Uninstall an app
|
|
1307
|
+
description: >-
|
|
1308
|
+
Removes the app with package name or bundle id `bundleId` from the device.
|
|
1309
|
+
|
|
1310
|
+
|
|
1311
|
+
Needs the `MEMBER` role and the `devices` scope. Refused with `409` while another user holds
|
|
1312
|
+
the device (their live preview or recording) or runs an Appium session on it; admins are
|
|
1313
|
+
exempt, and your own Appium session on it stays controllable.
|
|
1314
|
+
|
|
1315
|
+
|
|
1316
|
+
On a hub, for a node's phone, the request is sent on to that node once (never retried),
|
|
1317
|
+
signed for you, and the node's answer is relayed unchanged. A cloud provider's phone answers
|
|
1318
|
+
`501`.
|
|
1319
|
+
requestBody:
|
|
1320
|
+
required: true
|
|
1321
|
+
content:
|
|
1322
|
+
application/json:
|
|
1323
|
+
schema:
|
|
1324
|
+
$ref: '#/components/schemas/ControlUninstallRequest'
|
|
1325
|
+
example:
|
|
1326
|
+
bundleId: com.example.shop
|
|
1327
|
+
responses:
|
|
1328
|
+
'200':
|
|
1329
|
+
description: Uninstalled.
|
|
1330
|
+
content:
|
|
1331
|
+
application/json:
|
|
1332
|
+
schema:
|
|
1333
|
+
$ref: '#/components/schemas/Success'
|
|
1334
|
+
example:
|
|
1335
|
+
success: true
|
|
1336
|
+
'400':
|
|
1337
|
+
description: This server can't do this on the device's platform, or the udid is malformed.
|
|
1338
|
+
content:
|
|
1339
|
+
application/json:
|
|
1340
|
+
schema:
|
|
1341
|
+
$ref: '#/components/schemas/Error'
|
|
1342
|
+
examples:
|
|
1343
|
+
invalidUdid:
|
|
1344
|
+
summary: The udid segment is not valid percent-encoding
|
|
1345
|
+
value:
|
|
1346
|
+
success: false
|
|
1347
|
+
error: invalid_udid
|
|
1348
|
+
notSupported:
|
|
1349
|
+
summary: No device manager for the platform, or it can't do this
|
|
1350
|
+
value:
|
|
1351
|
+
error: not_supported
|
|
1352
|
+
message: Manager not found or uninstallApp not supported
|
|
1353
|
+
'401':
|
|
1354
|
+
$ref: '#/components/responses/Unauthorized'
|
|
1355
|
+
'403':
|
|
1356
|
+
$ref: '#/components/responses/ControlForbiddenMutation'
|
|
1357
|
+
'404':
|
|
1358
|
+
$ref: '#/components/responses/ControlDeviceNotFound'
|
|
1359
|
+
'409':
|
|
1360
|
+
$ref: '#/components/responses/ControlHeld'
|
|
1361
|
+
'429':
|
|
1362
|
+
$ref: '#/components/responses/RateLimited'
|
|
1363
|
+
'500':
|
|
1364
|
+
description: The uninstall failed on the device.
|
|
1365
|
+
content:
|
|
1366
|
+
application/json:
|
|
1367
|
+
schema:
|
|
1368
|
+
$ref: '#/components/schemas/Error'
|
|
1369
|
+
example:
|
|
1370
|
+
error: 'Command failed: adb -s R5CT32ABCDE uninstall com.example.shop'
|
|
1371
|
+
'501':
|
|
1372
|
+
$ref: '#/components/responses/ControlCloudPhone'
|
|
1373
|
+
'502':
|
|
1374
|
+
$ref: '#/components/responses/ControlNodeUnreachable'
|
|
1375
|
+
'503':
|
|
1376
|
+
$ref: '#/components/responses/ControlOwnershipUnavailable'
|
|
1377
|
+
'504':
|
|
1378
|
+
$ref: '#/components/responses/ControlNodeTimeout'
|
|
1379
|
+
/api/control/{udid}/logs:
|
|
1380
|
+
get:
|
|
1381
|
+
tags:
|
|
1382
|
+
- Control
|
|
1383
|
+
parameters:
|
|
1384
|
+
- $ref: '#/components/parameters/ControlUdid'
|
|
1385
|
+
operationId: getDeviceLogs
|
|
1386
|
+
summary: Read recent device logs
|
|
1387
|
+
description: >-
|
|
1388
|
+
Returns recent device log lines as one string.
|
|
1389
|
+
|
|
1390
|
+
- **Android:** the last 500 lines of `logcat` (`-v threadtime`). If `adb` fails or isn't
|
|
1391
|
+
available, the answer is `500` with the reason in `error`.
|
|
1392
|
+
|
|
1393
|
+
- **iOS (real device):** the lines the syslog stream has gathered since the previous call.
|
|
1394
|
+
The first call starts that stream and may return nothing; each call empties the buffer, so
|
|
1395
|
+
two clients polling the same phone split the lines between them. The stream stops after a
|
|
1396
|
+
while without calls.
|
|
1397
|
+
|
|
1398
|
+
- **iOS simulator:** the last 10 seconds of `log show`.
|
|
1399
|
+
|
|
1400
|
+
For a continuous Android log use the logcat WebSocket (see `POST
|
|
1401
|
+
/api/control/{udid}/stream/ticket`).
|
|
1402
|
+
|
|
1403
|
+
Ownership-checked like the clipboard: logs carry tokens and personal data from the app under
|
|
1404
|
+
test, so while another user holds the device or runs a session on it you get `409`. Needs
|
|
1405
|
+
the `MEMBER` role; any scope will do.
|
|
1406
|
+
|
|
1407
|
+
On a hub, for a node's phone, the request is sent on to that node once (never retried),
|
|
1408
|
+
signed for you, and the node's answer is relayed unchanged. A cloud provider's phone answers
|
|
1409
|
+
`501`.
|
|
1410
|
+
responses:
|
|
1411
|
+
'200':
|
|
1412
|
+
description: The log text.
|
|
1413
|
+
content:
|
|
1414
|
+
application/json:
|
|
1415
|
+
schema:
|
|
1416
|
+
$ref: '#/components/schemas/ControlLogs'
|
|
1417
|
+
example:
|
|
1418
|
+
logs: >-
|
|
1419
|
+
10-04 14:02:11.532 1203 1250 I ActivityManager: Start proc
|
|
1420
|
+
8812:com.example.shop/u0a201 for activity
|
|
1421
|
+
|
|
1422
|
+
10-04 14:02:11.871 8812 8812 D ShopApp: onCreate
|
|
1423
|
+
'400':
|
|
1424
|
+
description: This server can't do this on the device's platform, or the udid is malformed.
|
|
1425
|
+
content:
|
|
1426
|
+
application/json:
|
|
1427
|
+
schema:
|
|
1428
|
+
$ref: '#/components/schemas/Error'
|
|
1429
|
+
examples:
|
|
1430
|
+
invalidUdid:
|
|
1431
|
+
summary: The udid segment is not valid percent-encoding
|
|
1432
|
+
value:
|
|
1433
|
+
success: false
|
|
1434
|
+
error: invalid_udid
|
|
1435
|
+
notSupported:
|
|
1436
|
+
summary: No device manager for the platform, or it can't do this
|
|
1437
|
+
value:
|
|
1438
|
+
error: not_supported
|
|
1439
|
+
message: Manager not found or getLogs not supported
|
|
1440
|
+
'401':
|
|
1441
|
+
$ref: '#/components/responses/Unauthorized'
|
|
1442
|
+
'403':
|
|
1443
|
+
$ref: '#/components/responses/ControlForbiddenRead'
|
|
1444
|
+
'404':
|
|
1445
|
+
$ref: '#/components/responses/ControlDeviceNotFound'
|
|
1446
|
+
'409':
|
|
1447
|
+
$ref: '#/components/responses/ControlHeld'
|
|
1448
|
+
'429':
|
|
1449
|
+
$ref: '#/components/responses/RateLimited'
|
|
1450
|
+
'500':
|
|
1451
|
+
description: Reading logs failed on the device (on Android, `adb` failed or isn't available).
|
|
1452
|
+
content:
|
|
1453
|
+
application/json:
|
|
1454
|
+
schema:
|
|
1455
|
+
$ref: '#/components/schemas/Error'
|
|
1456
|
+
examples:
|
|
1457
|
+
android:
|
|
1458
|
+
summary: Android, no adb
|
|
1459
|
+
value:
|
|
1460
|
+
error: ADB is not available
|
|
1461
|
+
ios:
|
|
1462
|
+
summary: iOS
|
|
1463
|
+
value:
|
|
1464
|
+
error: spawn idevicesyslog ENOENT
|
|
1465
|
+
'501':
|
|
1466
|
+
$ref: '#/components/responses/ControlCloudPhone'
|
|
1467
|
+
'502':
|
|
1468
|
+
$ref: '#/components/responses/ControlNodeUnreachable'
|
|
1469
|
+
'503':
|
|
1470
|
+
$ref: '#/components/responses/ControlOwnershipUnavailable'
|
|
1471
|
+
'504':
|
|
1472
|
+
$ref: '#/components/responses/ControlNodeTimeout'
|
|
1473
|
+
/api/control/{udid}/shell:
|
|
1474
|
+
post:
|
|
1475
|
+
tags:
|
|
1476
|
+
- Control
|
|
1477
|
+
parameters:
|
|
1478
|
+
- $ref: '#/components/parameters/ControlUdid'
|
|
1479
|
+
operationId: runDeviceShellCommand
|
|
1480
|
+
summary: Run an allowed shell command
|
|
1481
|
+
description: >-
|
|
1482
|
+
Runs one diagnostic command from an allowlist and returns its output. Anything else is
|
|
1483
|
+
refused, and so is a command that fails, **with `200`** and an `error` field instead of
|
|
1484
|
+
`output` (the dashboard's terminal prints either).
|
|
1485
|
+
|
|
1486
|
+
- **Android** (`adb shell`): commands starting with `ls`, `ps`, `top`, `dumpsys battery`,
|
|
1487
|
+
`dumpsys wifi`, `dumpsys power`, `whoami`, `getprop`, `pm list packages`, `ip addr`, `cat
|
|
1488
|
+
/proc/meminfo`, `cat /proc/cpuinfo`, `date`, `uptime` or `netstat`.
|
|
1489
|
+
|
|
1490
|
+
- **iOS:** simulator commands `listapps`, `get_app_container`, `list`, `getenv` (through
|
|
1491
|
+
`simctl`); go-ios commands `apps`, `info`, `syslog`, `list`, `deviceinfo`, `diagnostics`;
|
|
1492
|
+
and `ls`, `ps`, `top`, `whoami`, `date`, `uptime`, `netstat`, `id`.
|
|
1493
|
+
|
|
1494
|
+
Needs the `MEMBER` role and the `devices` scope. Refused with `409` while another user holds
|
|
1495
|
+
the device (their live preview or recording) or runs an Appium session on it; admins are
|
|
1496
|
+
exempt, and your own Appium session on it stays controllable.
|
|
1497
|
+
|
|
1498
|
+
On a hub, for a node's phone, the request is sent on to that node once (never retried),
|
|
1499
|
+
signed for you, and the node's answer is relayed unchanged. A cloud provider's phone answers
|
|
1500
|
+
`501`.
|
|
1501
|
+
requestBody:
|
|
1502
|
+
required: true
|
|
1503
|
+
content:
|
|
1504
|
+
application/json:
|
|
1505
|
+
schema:
|
|
1506
|
+
$ref: '#/components/schemas/ControlShellRequest'
|
|
1507
|
+
example:
|
|
1508
|
+
command: dumpsys battery
|
|
1509
|
+
responses:
|
|
1510
|
+
'200':
|
|
1511
|
+
description: The output, or an `error` for a refused or failed command.
|
|
1512
|
+
content:
|
|
1513
|
+
application/json:
|
|
1514
|
+
schema:
|
|
1515
|
+
$ref: '#/components/schemas/ControlShellResult'
|
|
1516
|
+
examples:
|
|
1517
|
+
output:
|
|
1518
|
+
summary: The command ran
|
|
1519
|
+
value:
|
|
1520
|
+
output: |
|
|
1521
|
+
Current Battery Service state:
|
|
1522
|
+
AC powered: false
|
|
1523
|
+
USB powered: true
|
|
1524
|
+
level: 87
|
|
1525
|
+
refused:
|
|
1526
|
+
summary: Not on the allowlist
|
|
1527
|
+
value:
|
|
1528
|
+
error: Command 'rm -rf /sdcard' is not allowed for security reasons.
|
|
1529
|
+
'400':
|
|
1530
|
+
description: '`command` is missing or empty, the platform has no shell here, or the udid is malformed.'
|
|
1531
|
+
content:
|
|
1532
|
+
application/json:
|
|
1533
|
+
schema:
|
|
1534
|
+
$ref: '#/components/schemas/Error'
|
|
1535
|
+
examples:
|
|
1536
|
+
invalidUdid:
|
|
1537
|
+
summary: The udid segment is not valid percent-encoding
|
|
1538
|
+
value:
|
|
1539
|
+
success: false
|
|
1540
|
+
error: invalid_udid
|
|
1541
|
+
noCommand:
|
|
1542
|
+
summary: No `command`
|
|
1543
|
+
value:
|
|
1544
|
+
error: bad_request
|
|
1545
|
+
message: Command is required
|
|
1546
|
+
notSupported:
|
|
1547
|
+
summary: No shell for the platform here
|
|
1548
|
+
value:
|
|
1549
|
+
error: not_supported
|
|
1550
|
+
message: Manager not found or executeShell not supported
|
|
1551
|
+
'401':
|
|
1552
|
+
$ref: '#/components/responses/Unauthorized'
|
|
1553
|
+
'403':
|
|
1554
|
+
$ref: '#/components/responses/ControlForbiddenMutation'
|
|
1555
|
+
'404':
|
|
1556
|
+
$ref: '#/components/responses/ControlDeviceNotFound'
|
|
1557
|
+
'409':
|
|
1558
|
+
$ref: '#/components/responses/ControlHeld'
|
|
1559
|
+
'429':
|
|
1560
|
+
$ref: '#/components/responses/RateLimited'
|
|
1561
|
+
'501':
|
|
1562
|
+
$ref: '#/components/responses/ControlCloudPhone'
|
|
1563
|
+
'502':
|
|
1564
|
+
$ref: '#/components/responses/ControlNodeUnreachable'
|
|
1565
|
+
'503':
|
|
1566
|
+
$ref: '#/components/responses/ControlOwnershipUnavailable'
|
|
1567
|
+
'504':
|
|
1568
|
+
$ref: '#/components/responses/ControlNodeTimeout'
|
|
1569
|
+
/api/control/{udid}/inspector/snapshot:
|
|
1570
|
+
get:
|
|
1571
|
+
tags:
|
|
1572
|
+
- Control
|
|
1573
|
+
parameters:
|
|
1574
|
+
- $ref: '#/components/parameters/ControlUdid'
|
|
1575
|
+
operationId: getInspectorSnapshot
|
|
1576
|
+
summary: Capture the screen and its element tree
|
|
1577
|
+
description: >-
|
|
1578
|
+
Takes a screenshot and the UI hierarchy together and returns the tree with suggested
|
|
1579
|
+
locators and code for each element. When an Appium session is running on the device the
|
|
1580
|
+
hierarchy is the session's own page source (`hierarchySource: appium-session`); otherwise it
|
|
1581
|
+
is read from the device directly. Missing screen dimensions are filled in and saved.
|
|
1582
|
+
|
|
1583
|
+
|
|
1584
|
+
Open to watchers. Needs the `MEMBER` role; any scope will do.
|
|
1585
|
+
|
|
1586
|
+
|
|
1587
|
+
On a hub, for a node's phone, the request is sent on to that node once (never retried),
|
|
1588
|
+
signed for you, and the node's answer is relayed unchanged. A cloud provider's phone answers
|
|
1589
|
+
`501`.
|
|
1590
|
+
responses:
|
|
1591
|
+
'200':
|
|
1592
|
+
description: The snapshot.
|
|
1593
|
+
content:
|
|
1594
|
+
application/json:
|
|
1595
|
+
schema:
|
|
1596
|
+
$ref: '#/components/schemas/ControlInspectorSnapshot'
|
|
1597
|
+
'400':
|
|
1598
|
+
description: The udid is malformed.
|
|
1599
|
+
content:
|
|
1600
|
+
application/json:
|
|
1601
|
+
schema:
|
|
1602
|
+
$ref: '#/components/schemas/Error'
|
|
1603
|
+
examples:
|
|
1604
|
+
invalidUdid:
|
|
1605
|
+
summary: The udid segment is not valid percent-encoding
|
|
1606
|
+
value:
|
|
1607
|
+
success: false
|
|
1608
|
+
error: invalid_udid
|
|
1609
|
+
'401':
|
|
1610
|
+
$ref: '#/components/responses/Unauthorized'
|
|
1611
|
+
'403':
|
|
1612
|
+
$ref: '#/components/responses/ControlForbiddenRead'
|
|
1613
|
+
'404':
|
|
1614
|
+
$ref: '#/components/responses/ControlDeviceNotFound'
|
|
1615
|
+
'429':
|
|
1616
|
+
$ref: '#/components/responses/RateLimited'
|
|
1617
|
+
'500':
|
|
1618
|
+
description: The snapshot failed on the device.
|
|
1619
|
+
content:
|
|
1620
|
+
application/json:
|
|
1621
|
+
schema:
|
|
1622
|
+
$ref: '#/components/schemas/Error'
|
|
1623
|
+
example:
|
|
1624
|
+
error: Screenshot not supported
|
|
1625
|
+
'501':
|
|
1626
|
+
$ref: '#/components/responses/ControlCloudPhone'
|
|
1627
|
+
'502':
|
|
1628
|
+
$ref: '#/components/responses/ControlNodeUnreachable'
|
|
1629
|
+
'503':
|
|
1630
|
+
$ref: '#/components/responses/ControlOwnershipUnavailable'
|
|
1631
|
+
'504':
|
|
1632
|
+
$ref: '#/components/responses/ControlNodeTimeout'
|
|
1633
|
+
/api/control/{udid}/omni-scan:
|
|
1634
|
+
get:
|
|
1635
|
+
tags:
|
|
1636
|
+
- Control
|
|
1637
|
+
parameters:
|
|
1638
|
+
- $ref: '#/components/parameters/ControlUdid'
|
|
1639
|
+
operationId: scanDeviceScreen
|
|
1640
|
+
summary: Analyse the screen with OCR and AI
|
|
1641
|
+
description: >-
|
|
1642
|
+
Screenshots the device, runs OCR over it (every word with its confidence and box) and asks
|
|
1643
|
+
the configured AI provider for a qualitative read of the screen. No Appium session is
|
|
1644
|
+
needed. Slow: allow tens of seconds.
|
|
1645
|
+
|
|
1646
|
+
|
|
1647
|
+
If the analysis itself fails (an empty screenshot, OCR failing), the answer is `500` with
|
|
1648
|
+
`{ status: 'error', message }`. If only the AI step fails, the answer is `200` and
|
|
1649
|
+
`ai_insights` is `null`.
|
|
1650
|
+
|
|
1651
|
+
|
|
1652
|
+
Open to watchers. Needs the `MEMBER` role; any scope will do.
|
|
1653
|
+
|
|
1654
|
+
|
|
1655
|
+
On a hub, for a node's phone, this runs here and asks the node only for what it needs. A
|
|
1656
|
+
cloud provider's phone answers `501`. The OCR and AI run on the hub, with the hub's AI
|
|
1657
|
+
settings, on a screenshot taken by the node.
|
|
1658
|
+
responses:
|
|
1659
|
+
'200':
|
|
1660
|
+
description: The analysis.
|
|
1661
|
+
content:
|
|
1662
|
+
application/json:
|
|
1663
|
+
schema:
|
|
1664
|
+
$ref: '#/components/schemas/ControlOmniScanResult'
|
|
1665
|
+
examples:
|
|
1666
|
+
analysed:
|
|
1667
|
+
summary: Analysed
|
|
1668
|
+
value:
|
|
1669
|
+
status: success
|
|
1670
|
+
value:
|
|
1671
|
+
timestamp: '2026-10-04T14:05:22.118Z'
|
|
1672
|
+
ocr:
|
|
1673
|
+
text: |-
|
|
1674
|
+
Sign in
|
|
1675
|
+
Email
|
|
1676
|
+
Password
|
|
1677
|
+
Forgot password?
|
|
1678
|
+
words:
|
|
1679
|
+
- text: Sign
|
|
1680
|
+
confidence: 96.2
|
|
1681
|
+
bbox:
|
|
1682
|
+
x0: 142
|
|
1683
|
+
y0: 210
|
|
1684
|
+
x1: 188
|
|
1685
|
+
y1: 236
|
|
1686
|
+
ai_insights:
|
|
1687
|
+
summary: >-
|
|
1688
|
+
A sign-in form with email and password fields and a disabled Sign in
|
|
1689
|
+
button.
|
|
1690
|
+
'400':
|
|
1691
|
+
description: This server has no device manager for the platform, or the udid is malformed.
|
|
1692
|
+
content:
|
|
1693
|
+
application/json:
|
|
1694
|
+
schema:
|
|
1695
|
+
$ref: '#/components/schemas/Error'
|
|
1696
|
+
examples:
|
|
1697
|
+
invalidUdid:
|
|
1698
|
+
summary: The udid segment is not valid percent-encoding
|
|
1699
|
+
value:
|
|
1700
|
+
success: false
|
|
1701
|
+
error: invalid_udid
|
|
1702
|
+
notSupported:
|
|
1703
|
+
summary: No device manager for the platform, or it can't do this
|
|
1704
|
+
value:
|
|
1705
|
+
error: not_supported
|
|
1706
|
+
message: Manager not found
|
|
1707
|
+
'401':
|
|
1708
|
+
$ref: '#/components/responses/Unauthorized'
|
|
1709
|
+
'403':
|
|
1710
|
+
$ref: '#/components/responses/ControlForbiddenRead'
|
|
1711
|
+
'404':
|
|
1712
|
+
$ref: '#/components/responses/ControlDeviceNotFound'
|
|
1713
|
+
'429':
|
|
1714
|
+
$ref: '#/components/responses/RateLimited'
|
|
1715
|
+
'500':
|
|
1716
|
+
description: The analysis failed (an empty screenshot, OCR failing).
|
|
1717
|
+
content:
|
|
1718
|
+
application/json:
|
|
1719
|
+
schema:
|
|
1720
|
+
$ref: '#/components/schemas/ControlStatusError'
|
|
1721
|
+
examples:
|
|
1722
|
+
emptyScreenshot:
|
|
1723
|
+
summary: The screenshot was empty
|
|
1724
|
+
value:
|
|
1725
|
+
status: error
|
|
1726
|
+
message: Screenshot capture returned empty data.
|
|
1727
|
+
ocr:
|
|
1728
|
+
summary: OCR failed
|
|
1729
|
+
value:
|
|
1730
|
+
status: error
|
|
1731
|
+
message: OCR worker failed to start
|
|
1732
|
+
'501':
|
|
1733
|
+
$ref: '#/components/responses/ControlCloudPhone'
|
|
1734
|
+
'503':
|
|
1735
|
+
$ref: '#/components/responses/ControlOwnershipUnavailable'
|
|
1736
|
+
/api/control/{udid}/test-locator:
|
|
1737
|
+
post:
|
|
1738
|
+
tags:
|
|
1739
|
+
- Control
|
|
1740
|
+
parameters:
|
|
1741
|
+
- $ref: '#/components/parameters/ControlUdid'
|
|
1742
|
+
operationId: testDeviceAiLocator
|
|
1743
|
+
summary: Try an AI locator on the current screen
|
|
1744
|
+
description: >-
|
|
1745
|
+
Runs one of Xenon's AI locator strategies against a fresh screenshot, as a test would with
|
|
1746
|
+
`findElement`, without an Appium session.
|
|
1747
|
+
|
|
1748
|
+
- `-custom:ai-text`: OCR; every word containing `selector` (case-insensitive, confidence
|
|
1749
|
+
above 60%).
|
|
1750
|
+
|
|
1751
|
+
- `-custom:ai-icon`: the configured AI provider looks for what `selector` describes; at most
|
|
1752
|
+
one match, a 40×40 box around the point it names.
|
|
1753
|
+
|
|
1754
|
+
Each match gets a virtual element id (`omni_…`). No match answers `200` with an empty
|
|
1755
|
+
`value`. A failed screenshot, a failed OCR for `ai-text`, or for `ai-icon` no AI provider
|
|
1756
|
+
configured or a failed AI call answers `500`, so a failure never reads as "nothing found".
|
|
1757
|
+
Counts against the `heavy` rate-limit budget.
|
|
1758
|
+
|
|
1759
|
+
Needs the `MEMBER` role and the `devices` scope. Refused with `409` while another user holds
|
|
1760
|
+
the device (their live preview or recording) or runs an Appium session on it; admins are
|
|
1761
|
+
exempt, and your own Appium session on it stays controllable.
|
|
1762
|
+
|
|
1763
|
+
On a hub, for a node's phone, this runs here and asks the node only for what it needs. A
|
|
1764
|
+
cloud provider's phone answers `501`. The OCR and AI run on the hub, with the hub's AI
|
|
1765
|
+
settings, on a screenshot taken by the node.
|
|
1766
|
+
requestBody:
|
|
1767
|
+
required: true
|
|
1768
|
+
content:
|
|
1769
|
+
application/json:
|
|
1770
|
+
schema:
|
|
1771
|
+
$ref: '#/components/schemas/ControlTestLocatorRequest'
|
|
1772
|
+
example:
|
|
1773
|
+
strategy: '-custom:ai-text'
|
|
1774
|
+
selector: Sign in
|
|
1775
|
+
responses:
|
|
1776
|
+
'200':
|
|
1777
|
+
description: The matches, possibly none.
|
|
1778
|
+
content:
|
|
1779
|
+
application/json:
|
|
1780
|
+
schema:
|
|
1781
|
+
$ref: '#/components/schemas/ControlTestLocatorResult'
|
|
1782
|
+
example:
|
|
1783
|
+
status: success
|
|
1784
|
+
value:
|
|
1785
|
+
- id: omni_ocr_1759586722118_k3j9x
|
|
1786
|
+
text: Sign
|
|
1787
|
+
rect:
|
|
1788
|
+
x: 142
|
|
1789
|
+
'y': 210
|
|
1790
|
+
width: 46
|
|
1791
|
+
height: 26
|
|
1792
|
+
confidence: 0.962
|
|
1793
|
+
'400':
|
|
1794
|
+
description: An unsupported `strategy`, no device manager for the platform, or a malformed udid.
|
|
1795
|
+
content:
|
|
1796
|
+
application/json:
|
|
1797
|
+
schema:
|
|
1798
|
+
$ref: '#/components/schemas/Error'
|
|
1799
|
+
examples:
|
|
1800
|
+
invalidUdid:
|
|
1801
|
+
summary: The udid segment is not valid percent-encoding
|
|
1802
|
+
value:
|
|
1803
|
+
success: false
|
|
1804
|
+
error: invalid_udid
|
|
1805
|
+
unsupportedStrategy:
|
|
1806
|
+
summary: Unsupported strategy
|
|
1807
|
+
value:
|
|
1808
|
+
status: error
|
|
1809
|
+
message: 'Unsupported strategy: xpath'
|
|
1810
|
+
notSupported:
|
|
1811
|
+
summary: No device manager for the platform, or it can't do this
|
|
1812
|
+
value:
|
|
1813
|
+
error: not_supported
|
|
1814
|
+
message: Manager not found
|
|
1815
|
+
'401':
|
|
1816
|
+
$ref: '#/components/responses/Unauthorized'
|
|
1817
|
+
'403':
|
|
1818
|
+
$ref: '#/components/responses/ControlForbiddenMutation'
|
|
1819
|
+
'404':
|
|
1820
|
+
$ref: '#/components/responses/ControlDeviceNotFound'
|
|
1821
|
+
'409':
|
|
1822
|
+
$ref: '#/components/responses/ControlHeld'
|
|
1823
|
+
'429':
|
|
1824
|
+
$ref: '#/components/responses/RateLimited'
|
|
1825
|
+
'500':
|
|
1826
|
+
description: >-
|
|
1827
|
+
The screenshot failed, the OCR failed (`ai-text`), or no AI provider is configured or
|
|
1828
|
+
the AI call failed (`ai-icon`).
|
|
1829
|
+
content:
|
|
1830
|
+
application/json:
|
|
1831
|
+
schema:
|
|
1832
|
+
$ref: '#/components/schemas/ControlStatusError'
|
|
1833
|
+
examples:
|
|
1834
|
+
ocr:
|
|
1835
|
+
summary: OCR failed (ai-text)
|
|
1836
|
+
value:
|
|
1837
|
+
status: error
|
|
1838
|
+
message: OCR worker failed to start
|
|
1839
|
+
noProvider:
|
|
1840
|
+
summary: No AI provider configured (ai-icon)
|
|
1841
|
+
value:
|
|
1842
|
+
status: error
|
|
1843
|
+
message: No AI provider is configured
|
|
1844
|
+
'501':
|
|
1845
|
+
$ref: '#/components/responses/ControlCloudPhone'
|
|
1846
|
+
'503':
|
|
1847
|
+
$ref: '#/components/responses/ControlOwnershipUnavailable'
|
|
1848
|
+
/api/control/{udid}/appium-session:
|
|
1849
|
+
get:
|
|
1850
|
+
tags:
|
|
1851
|
+
- Control
|
|
1852
|
+
parameters:
|
|
1853
|
+
- $ref: '#/components/parameters/ControlUdid'
|
|
1854
|
+
operationId: getDeviceAppiumSession
|
|
1855
|
+
summary: Get the Appium session running on the device
|
|
1856
|
+
description: >-
|
|
1857
|
+
Returns the id of the Appium session driving the device, if any, and the WebDriver base path
|
|
1858
|
+
this server was started with, so a client can call that session's commands. `sessionId` is
|
|
1859
|
+
`null` when no session runs; a live preview or recording hold is not a session.
|
|
1860
|
+
|
|
1861
|
+
|
|
1862
|
+
Open to watchers. Needs the `MEMBER` role; any scope will do.
|
|
1863
|
+
|
|
1864
|
+
|
|
1865
|
+
On a hub this is answered by the hub for every phone, a node's or a cloud provider's
|
|
1866
|
+
included, since the session's commands go through the hub.
|
|
1867
|
+
responses:
|
|
1868
|
+
'200':
|
|
1869
|
+
description: The session, or none.
|
|
1870
|
+
content:
|
|
1871
|
+
application/json:
|
|
1872
|
+
schema:
|
|
1873
|
+
$ref: '#/components/schemas/ControlAppiumSession'
|
|
1874
|
+
examples:
|
|
1875
|
+
running:
|
|
1876
|
+
summary: A session is running
|
|
1877
|
+
value:
|
|
1878
|
+
status: success
|
|
1879
|
+
sessionId: 3f9c2a7e-1b4d-4c8a-9e2f-6d5b8a1c0e47
|
|
1880
|
+
basePath: /wd/hub
|
|
1881
|
+
none:
|
|
1882
|
+
summary: Nothing is driving the device
|
|
1883
|
+
value:
|
|
1884
|
+
status: success
|
|
1885
|
+
sessionId: null
|
|
1886
|
+
basePath: ''
|
|
1887
|
+
'400':
|
|
1888
|
+
description: The udid is malformed.
|
|
1889
|
+
content:
|
|
1890
|
+
application/json:
|
|
1891
|
+
schema:
|
|
1892
|
+
$ref: '#/components/schemas/Error'
|
|
1893
|
+
examples:
|
|
1894
|
+
invalidUdid:
|
|
1895
|
+
summary: The udid segment is not valid percent-encoding
|
|
1896
|
+
value:
|
|
1897
|
+
success: false
|
|
1898
|
+
error: invalid_udid
|
|
1899
|
+
'401':
|
|
1900
|
+
$ref: '#/components/responses/Unauthorized'
|
|
1901
|
+
'403':
|
|
1902
|
+
$ref: '#/components/responses/ControlForbiddenRead'
|
|
1903
|
+
'404':
|
|
1904
|
+
$ref: '#/components/responses/ControlDeviceNotFound'
|
|
1905
|
+
'429':
|
|
1906
|
+
$ref: '#/components/responses/RateLimited'
|
|
1907
|
+
'503':
|
|
1908
|
+
$ref: '#/components/responses/ControlOwnershipUnavailable'
|
|
1909
|
+
/api/control/{udid}/stream/start:
|
|
1910
|
+
post:
|
|
1911
|
+
tags:
|
|
1912
|
+
- Control
|
|
1913
|
+
parameters:
|
|
1914
|
+
- $ref: '#/components/parameters/ControlUdid'
|
|
1915
|
+
operationId: startDevicePreview
|
|
1916
|
+
summary: Start the live preview and hold the device
|
|
1917
|
+
description: >-
|
|
1918
|
+
Starts the device's live preview and holds the device for you (`session_id` becomes
|
|
1919
|
+
`manual_<yourUserId>_<udid>`), so automation is not given it while you watch. iOS starts
|
|
1920
|
+
WebDriverAgent's MJPEG stream (and the go-ios tunnel on iOS 17+). Android starts an MJPEG
|
|
1921
|
+
screen capture, or, when the server's `streaming.androidH264` setting is on and the device
|
|
1922
|
+
is not being recorded, an H.264 stream for the WebSocket in `h264Path`. Missing screen
|
|
1923
|
+
dimensions are filled in and saved.
|
|
1924
|
+
|
|
1925
|
+
Who may start:
|
|
1926
|
+
|
|
1927
|
+
- a free device: anyone who can see it;
|
|
1928
|
+
|
|
1929
|
+
- a device you already hold, or running your own Appium session: you (the preview runs
|
|
1930
|
+
alongside it);
|
|
1931
|
+
|
|
1932
|
+
- a device held by someone whose preview is no longer running (left over from a restart):
|
|
1933
|
+
anyone; the old hold is cleared;
|
|
1934
|
+
|
|
1935
|
+
- otherwise `409`, naming the holder. Admins may always start.
|
|
1936
|
+
|
|
1937
|
+
Needs the `MEMBER` role and the `devices` scope.
|
|
1938
|
+
|
|
1939
|
+
On a hub, for a node's phone, the request is forwarded to the node, which holds the phone by
|
|
1940
|
+
its own rules; on success the hub also marks the phone busy and held by you at once. A cloud
|
|
1941
|
+
provider's phone answers `501`.
|
|
1942
|
+
responses:
|
|
1943
|
+
'200':
|
|
1944
|
+
description: The preview is running. `type` says which transport to use.
|
|
1945
|
+
content:
|
|
1946
|
+
application/json:
|
|
1947
|
+
schema:
|
|
1948
|
+
$ref: '#/components/schemas/ControlStreamStartResult'
|
|
1949
|
+
examples:
|
|
1950
|
+
mjpeg:
|
|
1951
|
+
summary: MJPEG (iOS, or Android without H.264)
|
|
1952
|
+
value:
|
|
1953
|
+
success: true
|
|
1954
|
+
type: mjpeg
|
|
1955
|
+
mjpegPort: 9101
|
|
1956
|
+
streamUrl: /xenon/api/control/00008110-00084CE80E51401E/stream
|
|
1957
|
+
h264:
|
|
1958
|
+
summary: H.264 (Android, setting on)
|
|
1959
|
+
value:
|
|
1960
|
+
success: true
|
|
1961
|
+
type: h264
|
|
1962
|
+
h264Path: /xenon/api/control/R5CT32ABCDE/stream/h264
|
|
1963
|
+
'400':
|
|
1964
|
+
description: The udid is malformed.
|
|
1965
|
+
content:
|
|
1966
|
+
application/json:
|
|
1967
|
+
schema:
|
|
1968
|
+
$ref: '#/components/schemas/Error'
|
|
1969
|
+
examples:
|
|
1970
|
+
invalidUdid:
|
|
1971
|
+
summary: The udid segment is not valid percent-encoding
|
|
1972
|
+
value:
|
|
1973
|
+
success: false
|
|
1974
|
+
error: invalid_udid
|
|
1975
|
+
'401':
|
|
1976
|
+
description: No credential, or one that names no user.
|
|
1977
|
+
content:
|
|
1978
|
+
application/json:
|
|
1979
|
+
schema:
|
|
1980
|
+
$ref: '#/components/schemas/Error'
|
|
1981
|
+
example:
|
|
1982
|
+
success: false
|
|
1983
|
+
error: unauthenticated
|
|
1984
|
+
'403':
|
|
1985
|
+
$ref: '#/components/responses/ControlForbiddenMutation'
|
|
1986
|
+
'404':
|
|
1987
|
+
$ref: '#/components/responses/ControlDeviceNotFound'
|
|
1988
|
+
'409':
|
|
1989
|
+
$ref: '#/components/responses/ControlHeld'
|
|
1990
|
+
'429':
|
|
1991
|
+
$ref: '#/components/responses/RateLimited'
|
|
1992
|
+
'500':
|
|
1993
|
+
description: The preview could not be started (WebDriverAgent, scrcpy or the capture failed).
|
|
1994
|
+
content:
|
|
1995
|
+
application/json:
|
|
1996
|
+
schema:
|
|
1997
|
+
$ref: '#/components/schemas/Error'
|
|
1998
|
+
example:
|
|
1999
|
+
success: false
|
|
2000
|
+
error: WDA did not become ready on port 8101 within 60000ms
|
|
2001
|
+
'501':
|
|
2002
|
+
$ref: '#/components/responses/ControlCloudPhone'
|
|
2003
|
+
'502':
|
|
2004
|
+
$ref: '#/components/responses/ControlNodeUnreachable'
|
|
2005
|
+
'503':
|
|
2006
|
+
$ref: '#/components/responses/ControlOwnershipUnavailable'
|
|
2007
|
+
'504':
|
|
2008
|
+
$ref: '#/components/responses/ControlNodeTimeout'
|
|
2009
|
+
/api/control/{udid}/stream/ticket:
|
|
2010
|
+
post:
|
|
2011
|
+
tags:
|
|
2012
|
+
- Control
|
|
2013
|
+
parameters:
|
|
2014
|
+
- $ref: '#/components/parameters/ControlUdid'
|
|
2015
|
+
operationId: createDeviceStreamTicket
|
|
2016
|
+
summary: Get a ticket to open the live preview
|
|
2017
|
+
description: >-
|
|
2018
|
+
Mints a single-use ticket, valid for 60 seconds and bound to this device and to you, for
|
|
2019
|
+
clients that cannot send headers or the cookie (an `<img>` tag, a WebSocket). It opens one
|
|
2020
|
+
of:
|
|
2021
|
+
|
|
2022
|
+
- `GET /api/control/{udid}/stream?ticket=…` (MJPEG);
|
|
2023
|
+
|
|
2024
|
+
- the WebSocket `/xenon/api/control/{udid}/stream/h264?ticket=…` (H.264 preview);
|
|
2025
|
+
|
|
2026
|
+
- the WebSocket `/xenon/api/control/{udid}/logcat?ticket=…` (live Android logs; it applies
|
|
2027
|
+
the same ownership rule as `GET logs`).
|
|
2028
|
+
|
|
2029
|
+
Mint a new ticket for each connection. Your team access is checked here, when the ticket is
|
|
2030
|
+
minted. Minting holds nothing and starts nothing; missing screen dimensions are filled in
|
|
2031
|
+
and saved in the background.
|
|
2032
|
+
|
|
2033
|
+
Needs the `MEMBER` role and the `devices` scope. Not refused for a held device: watching is
|
|
2034
|
+
a read.
|
|
2035
|
+
|
|
2036
|
+
On a hub the ticket is the hub's, for every phone (a node's included); the hub relays the
|
|
2037
|
+
node's stream or socket. For a cloud provider's phone the WebSockets close with `1008`.
|
|
2038
|
+
responses:
|
|
2039
|
+
'200':
|
|
2040
|
+
description: The ticket.
|
|
2041
|
+
content:
|
|
2042
|
+
application/json:
|
|
2043
|
+
schema:
|
|
2044
|
+
$ref: '#/components/schemas/ControlStreamTicket'
|
|
2045
|
+
example:
|
|
2046
|
+
ticket: >-
|
|
2047
|
+
eyJhbGciOiJSUzI1NiIsImtpZCI6Inhlbi0xIn0.eyJhdWQiOiJ4ZW5vbi1zdHJlYW0iLCJ1ZGlkIjoiUjVDVDMyQUJDREUifQ.c2ln
|
|
2048
|
+
expiresIn: 60
|
|
2049
|
+
'400':
|
|
2050
|
+
description: The udid is malformed.
|
|
2051
|
+
content:
|
|
2052
|
+
application/json:
|
|
2053
|
+
schema:
|
|
2054
|
+
$ref: '#/components/schemas/Error'
|
|
2055
|
+
examples:
|
|
2056
|
+
invalidUdid:
|
|
2057
|
+
summary: The udid segment is not valid percent-encoding
|
|
2058
|
+
value:
|
|
2059
|
+
success: false
|
|
2060
|
+
error: invalid_udid
|
|
2061
|
+
'401':
|
|
2062
|
+
description: No credential, or one that names no user.
|
|
2063
|
+
content:
|
|
2064
|
+
application/json:
|
|
2065
|
+
schema:
|
|
2066
|
+
$ref: '#/components/schemas/Error'
|
|
2067
|
+
example:
|
|
2068
|
+
error: unauthenticated
|
|
2069
|
+
'403':
|
|
2070
|
+
$ref: '#/components/responses/ControlForbiddenMutation'
|
|
2071
|
+
'404':
|
|
2072
|
+
$ref: '#/components/responses/ControlDeviceNotFound'
|
|
2073
|
+
'429':
|
|
2074
|
+
$ref: '#/components/responses/RateLimited'
|
|
2075
|
+
'503':
|
|
2076
|
+
$ref: '#/components/responses/ControlOwnershipUnavailable'
|
|
2077
|
+
/api/control/{udid}/stream:
|
|
2078
|
+
get:
|
|
2079
|
+
tags:
|
|
2080
|
+
- Control
|
|
2081
|
+
parameters:
|
|
2082
|
+
- $ref: '#/components/parameters/ControlUdid'
|
|
2083
|
+
- in: query
|
|
2084
|
+
name: ticket
|
|
2085
|
+
required: false
|
|
2086
|
+
schema:
|
|
2087
|
+
type: string
|
|
2088
|
+
description: >-
|
|
2089
|
+
A single-use ticket from `POST /api/control/{udid}/stream/ticket`, in place of other
|
|
2090
|
+
credentials.
|
|
2091
|
+
example: >-
|
|
2092
|
+
eyJhbGciOiJSUzI1NiIsImtpZCI6Inhlbi0xIn0.eyJhdWQiOiJ4ZW5vbi1zdHJlYW0iLCJ1ZGlkIjoiUjVDVDMyQUJDREUifQ.c2ln
|
|
2093
|
+
operationId: streamDeviceScreen
|
|
2094
|
+
summary: Watch the device's screen (MJPEG)
|
|
2095
|
+
description: >-
|
|
2096
|
+
Streams the screen as MJPEG (`multipart/x-mixed-replace`) for as long as the connection
|
|
2097
|
+
stays open. Starts the device's MJPEG capture if it isn't running (iOS checks that
|
|
2098
|
+
WebDriverAgent still answers and restarts it if not), but holds nothing: use `POST
|
|
2099
|
+
stream/start` to hold the device. Each open connection counts as a viewer; a preview with no
|
|
2100
|
+
viewers is eventually stopped.
|
|
2101
|
+
|
|
2102
|
+
|
|
2103
|
+
Authenticate as usual, or with `?ticket=` from `POST stream/ticket`, for an `<img src>` that
|
|
2104
|
+
can carry neither headers nor the cookie. A ticket works only here, only for its device, and
|
|
2105
|
+
only once.
|
|
2106
|
+
|
|
2107
|
+
|
|
2108
|
+
Open to watchers: a busy device can be watched by anyone who can see it. Needs the `MEMBER`
|
|
2109
|
+
role; any scope will do.
|
|
2110
|
+
|
|
2111
|
+
|
|
2112
|
+
On a hub, for a node's phone, the request is sent on to that node once (never retried),
|
|
2113
|
+
signed for you, and the node's answer is relayed unchanged. A cloud provider's phone answers
|
|
2114
|
+
`501`. The stream is relayed for as long as you watch.
|
|
2115
|
+
security:
|
|
2116
|
+
- AccessKeyAuth: []
|
|
2117
|
+
TokenAuth: []
|
|
2118
|
+
- BearerAuth: []
|
|
2119
|
+
- CookieAuth: []
|
|
2120
|
+
- ControlStreamTicket: []
|
|
2121
|
+
responses:
|
|
2122
|
+
'200':
|
|
2123
|
+
description: The MJPEG stream, one JPEG per part, boundary `BoundaryString`.
|
|
2124
|
+
content:
|
|
2125
|
+
multipart/x-mixed-replace:
|
|
2126
|
+
schema:
|
|
2127
|
+
type: string
|
|
2128
|
+
format: binary
|
|
2129
|
+
'400':
|
|
2130
|
+
description: The udid is malformed.
|
|
2131
|
+
content:
|
|
2132
|
+
application/json:
|
|
2133
|
+
schema:
|
|
2134
|
+
$ref: '#/components/schemas/Error'
|
|
2135
|
+
examples:
|
|
2136
|
+
invalidUdid:
|
|
2137
|
+
summary: The udid segment is not valid percent-encoding
|
|
2138
|
+
value:
|
|
2139
|
+
success: false
|
|
2140
|
+
error: invalid_udid
|
|
2141
|
+
'401':
|
|
2142
|
+
description: No credential, or a ticket that is invalid, expired, already used or for another device.
|
|
2143
|
+
content:
|
|
2144
|
+
application/json:
|
|
2145
|
+
schema:
|
|
2146
|
+
$ref: '#/components/schemas/Error'
|
|
2147
|
+
examples:
|
|
2148
|
+
ticket:
|
|
2149
|
+
summary: A bad ticket
|
|
2150
|
+
value:
|
|
2151
|
+
error: invalid ticket
|
|
2152
|
+
none:
|
|
2153
|
+
summary: No credential
|
|
2154
|
+
value:
|
|
2155
|
+
error: unauthenticated
|
|
2156
|
+
'403':
|
|
2157
|
+
$ref: '#/components/responses/ControlForbiddenRead'
|
|
2158
|
+
'404':
|
|
2159
|
+
description: >-
|
|
2160
|
+
No such device (or one outside your teams), no MJPEG source for it, or the preview
|
|
2161
|
+
proxy could not be set up.
|
|
2162
|
+
content:
|
|
2163
|
+
application/json:
|
|
2164
|
+
schema:
|
|
2165
|
+
$ref: '#/components/schemas/Error'
|
|
2166
|
+
examples:
|
|
2167
|
+
device:
|
|
2168
|
+
summary: Unknown or hidden device
|
|
2169
|
+
value:
|
|
2170
|
+
error: not_found
|
|
2171
|
+
message: Device not found
|
|
2172
|
+
noSource:
|
|
2173
|
+
summary: No MJPEG source for the device
|
|
2174
|
+
value:
|
|
2175
|
+
error: MJPEG port not found for device
|
|
2176
|
+
hint: Start the preview with POST /stream/start, then open this stream again.
|
|
2177
|
+
noProxy:
|
|
2178
|
+
summary: The preview proxy could not be set up
|
|
2179
|
+
value:
|
|
2180
|
+
error: not_found
|
|
2181
|
+
message: The preview could not be started
|
|
2182
|
+
'429':
|
|
2183
|
+
$ref: '#/components/responses/RateLimited'
|
|
2184
|
+
'500':
|
|
2185
|
+
description: The stream proxy failed.
|
|
2186
|
+
content:
|
|
2187
|
+
application/json:
|
|
2188
|
+
schema:
|
|
2189
|
+
$ref: '#/components/schemas/Error'
|
|
2190
|
+
example:
|
|
2191
|
+
error: Stream proxy error
|
|
2192
|
+
message: connect ECONNREFUSED 127.0.0.1:9101
|
|
2193
|
+
'501':
|
|
2194
|
+
$ref: '#/components/responses/ControlCloudPhone'
|
|
2195
|
+
'502':
|
|
2196
|
+
$ref: '#/components/responses/ControlNodeUnreachable'
|
|
2197
|
+
'503':
|
|
2198
|
+
description: >-
|
|
2199
|
+
The capture could not be started, its source didn't answer in time, or the device's
|
|
2200
|
+
ownership could not be checked.
|
|
2201
|
+
content:
|
|
2202
|
+
application/json:
|
|
2203
|
+
schema:
|
|
2204
|
+
$ref: '#/components/schemas/Error'
|
|
2205
|
+
examples:
|
|
2206
|
+
ios:
|
|
2207
|
+
summary: iOS stream failed to start
|
|
2208
|
+
value:
|
|
2209
|
+
error: Stream not available
|
|
2210
|
+
message: WDA did not become ready on port 8101 within 60000ms
|
|
2211
|
+
android:
|
|
2212
|
+
summary: Android capture failed to start
|
|
2213
|
+
value:
|
|
2214
|
+
error: Android stream failed
|
|
2215
|
+
message: device offline
|
|
2216
|
+
ownership:
|
|
2217
|
+
summary: Lookup failed
|
|
2218
|
+
value:
|
|
2219
|
+
success: false
|
|
2220
|
+
error: device_ownership_unavailable
|
|
2221
|
+
message: Could not verify device ownership. Try again.
|
|
2222
|
+
text/html:
|
|
2223
|
+
schema:
|
|
2224
|
+
type: string
|
|
2225
|
+
example: '[MjpegProxy] Source not available after timeout'
|
|
2226
|
+
'504':
|
|
2227
|
+
$ref: '#/components/responses/ControlNodeTimeout'
|
|
2228
|
+
/api/control/{udid}/stream/status:
|
|
2229
|
+
get:
|
|
2230
|
+
tags:
|
|
2231
|
+
- Control
|
|
2232
|
+
parameters:
|
|
2233
|
+
- $ref: '#/components/parameters/ControlUdid'
|
|
2234
|
+
operationId: getDevicePreviewStatus
|
|
2235
|
+
summary: Get the live preview's status
|
|
2236
|
+
description: >-
|
|
2237
|
+
Says whether the device's preview is running and which transport a player should use
|
|
2238
|
+
(`type`: `h264` on Android when the server's `streaming.androidH264` setting is on and the
|
|
2239
|
+
device isn't being recorded, else `mjpeg`). A device with no preview answers `200` with
|
|
2240
|
+
`status: stopped`. iOS adds WebDriverAgent's port, the start time and the last error.
|
|
2241
|
+
|
|
2242
|
+
|
|
2243
|
+
Open to watchers. Needs the `MEMBER` role; any scope will do.
|
|
2244
|
+
|
|
2245
|
+
|
|
2246
|
+
On a hub, for a node's phone, the request is sent on to that node once (never retried),
|
|
2247
|
+
signed for you, and the node's answer is relayed unchanged. A cloud provider's phone answers
|
|
2248
|
+
`501`.
|
|
2249
|
+
responses:
|
|
2250
|
+
'200':
|
|
2251
|
+
description: The status.
|
|
2252
|
+
content:
|
|
2253
|
+
application/json:
|
|
2254
|
+
schema:
|
|
2255
|
+
$ref: '#/components/schemas/ControlStreamStatus'
|
|
2256
|
+
examples:
|
|
2257
|
+
ios:
|
|
2258
|
+
summary: iOS, running
|
|
2259
|
+
value:
|
|
2260
|
+
udid: 00008110-00084CE80E51401E
|
|
2261
|
+
status: running
|
|
2262
|
+
type: mjpeg
|
|
2263
|
+
wdaPort: 8101
|
|
2264
|
+
mjpegPort: 9101
|
|
2265
|
+
startedAt: 1759586400123
|
|
2266
|
+
h264:
|
|
2267
|
+
summary: Android, H.264
|
|
2268
|
+
value:
|
|
2269
|
+
udid: R5CT32ABCDE
|
|
2270
|
+
status: running
|
|
2271
|
+
type: h264
|
|
2272
|
+
h264Path: /xenon/api/control/R5CT32ABCDE/stream/h264
|
|
2273
|
+
mjpegPort: null
|
|
2274
|
+
stopped:
|
|
2275
|
+
summary: No preview
|
|
2276
|
+
value:
|
|
2277
|
+
udid: R5CT32ABCDE
|
|
2278
|
+
status: stopped
|
|
2279
|
+
type: mjpeg
|
|
2280
|
+
'400':
|
|
2281
|
+
description: The udid is malformed.
|
|
2282
|
+
content:
|
|
2283
|
+
application/json:
|
|
2284
|
+
schema:
|
|
2285
|
+
$ref: '#/components/schemas/Error'
|
|
2286
|
+
examples:
|
|
2287
|
+
invalidUdid:
|
|
2288
|
+
summary: The udid segment is not valid percent-encoding
|
|
2289
|
+
value:
|
|
2290
|
+
success: false
|
|
2291
|
+
error: invalid_udid
|
|
2292
|
+
'401':
|
|
2293
|
+
$ref: '#/components/responses/Unauthorized'
|
|
2294
|
+
'403':
|
|
2295
|
+
$ref: '#/components/responses/ControlForbiddenRead'
|
|
2296
|
+
'404':
|
|
2297
|
+
$ref: '#/components/responses/ControlDeviceNotFound'
|
|
2298
|
+
'429':
|
|
2299
|
+
$ref: '#/components/responses/RateLimited'
|
|
2300
|
+
'501':
|
|
2301
|
+
$ref: '#/components/responses/ControlCloudPhone'
|
|
2302
|
+
'502':
|
|
2303
|
+
$ref: '#/components/responses/ControlNodeUnreachable'
|
|
2304
|
+
'503':
|
|
2305
|
+
$ref: '#/components/responses/ControlOwnershipUnavailable'
|
|
2306
|
+
'504':
|
|
2307
|
+
$ref: '#/components/responses/ControlNodeTimeout'
|
|
2308
|
+
/api/control/{udid}/stream/leave:
|
|
2309
|
+
post:
|
|
2310
|
+
tags:
|
|
2311
|
+
- Control
|
|
2312
|
+
parameters:
|
|
2313
|
+
- $ref: '#/components/parameters/ControlUdid'
|
|
2314
|
+
operationId: leaveDevicePreview
|
|
2315
|
+
summary: Stop watching the preview
|
|
2316
|
+
description: >-
|
|
2317
|
+
Tells the server a page stopped watching (navigated away, closed, reloaded). Nothing stops
|
|
2318
|
+
at once: after a 3-second grace the preview is stopped and your hold released only if nobody
|
|
2319
|
+
is watching it and no recording reads it. A `stream/start` within the grace (a reload)
|
|
2320
|
+
cancels it. Use this rather than `stream/stop` when other tabs may be watching.
|
|
2321
|
+
|
|
2322
|
+
|
|
2323
|
+
You may leave a device you hold, one with a legacy hold, or any device as an admin; another
|
|
2324
|
+
user's hold answers `403`.
|
|
2325
|
+
|
|
2326
|
+
|
|
2327
|
+
Needs the `MEMBER` role and the `devices` scope.
|
|
2328
|
+
|
|
2329
|
+
|
|
2330
|
+
On a hub, for a node's phone, the request is sent on to that node once (never retried),
|
|
2331
|
+
signed for you, and the node's answer is relayed unchanged. A cloud provider's phone answers
|
|
2332
|
+
`501`.
|
|
2333
|
+
responses:
|
|
2334
|
+
'202':
|
|
2335
|
+
description: Noted; the preview will stop after the grace if nobody is watching.
|
|
2336
|
+
content:
|
|
2337
|
+
application/json:
|
|
2338
|
+
schema:
|
|
2339
|
+
$ref: '#/components/schemas/ControlStreamLeaveResult'
|
|
2340
|
+
example:
|
|
2341
|
+
success: true
|
|
2342
|
+
pending: true
|
|
2343
|
+
'400':
|
|
2344
|
+
description: The udid is malformed.
|
|
2345
|
+
content:
|
|
2346
|
+
application/json:
|
|
2347
|
+
schema:
|
|
2348
|
+
$ref: '#/components/schemas/Error'
|
|
2349
|
+
examples:
|
|
2350
|
+
invalidUdid:
|
|
2351
|
+
summary: The udid segment is not valid percent-encoding
|
|
2352
|
+
value:
|
|
2353
|
+
success: false
|
|
2354
|
+
error: invalid_udid
|
|
2355
|
+
'401':
|
|
2356
|
+
$ref: '#/components/responses/Unauthorized'
|
|
2357
|
+
'403':
|
|
2358
|
+
description: >-
|
|
2359
|
+
Another user holds the device; or the credential lacks the `devices` scope, or a browser
|
|
2360
|
+
request failed the same-origin check.
|
|
2361
|
+
content:
|
|
2362
|
+
application/json:
|
|
2363
|
+
schema:
|
|
2364
|
+
$ref: '#/components/schemas/Error'
|
|
2365
|
+
examples:
|
|
2366
|
+
held:
|
|
2367
|
+
summary: Another user's hold
|
|
2368
|
+
value:
|
|
2369
|
+
success: false
|
|
2370
|
+
error: lock_owned_by_another_user
|
|
2371
|
+
message: This device is being controlled by another user.
|
|
2372
|
+
scope:
|
|
2373
|
+
summary: No devices scope
|
|
2374
|
+
value:
|
|
2375
|
+
error: insufficient scope
|
|
2376
|
+
'404':
|
|
2377
|
+
$ref: '#/components/responses/ControlDeviceNotFound'
|
|
2378
|
+
'429':
|
|
2379
|
+
$ref: '#/components/responses/RateLimited'
|
|
2380
|
+
'501':
|
|
2381
|
+
$ref: '#/components/responses/ControlCloudPhone'
|
|
2382
|
+
'502':
|
|
2383
|
+
$ref: '#/components/responses/ControlNodeUnreachable'
|
|
2384
|
+
'503':
|
|
2385
|
+
$ref: '#/components/responses/ControlOwnershipUnavailable'
|
|
2386
|
+
'504':
|
|
2387
|
+
$ref: '#/components/responses/ControlNodeTimeout'
|
|
2388
|
+
/api/control/{udid}/stream/stop:
|
|
2389
|
+
post:
|
|
2390
|
+
tags:
|
|
2391
|
+
- Control
|
|
2392
|
+
parameters:
|
|
2393
|
+
- $ref: '#/components/parameters/ControlUdid'
|
|
2394
|
+
operationId: stopDevicePreview
|
|
2395
|
+
summary: Stop the preview and release the device
|
|
2396
|
+
description: >-
|
|
2397
|
+
Stops the device's preview on every transport at once and releases its preview hold,
|
|
2398
|
+
including a hold left over from a restart. Every other tab watching the device loses its
|
|
2399
|
+
preview too; prefer `stream/leave`. On iOS the stream is kept while an Appium session on the
|
|
2400
|
+
phone may be using its WebDriverAgent.
|
|
2401
|
+
|
|
2402
|
+
|
|
2403
|
+
You may stop a device you hold, one with a legacy hold, or (as an admin) any device, which
|
|
2404
|
+
force-releases another user's hold. Refused with `409 device_recording` while the device is
|
|
2405
|
+
being recorded: stop the recording instead (`POST /api/recordings/{groupId}/stop`).
|
|
2406
|
+
|
|
2407
|
+
|
|
2408
|
+
Needs the `MEMBER` role and the `devices` scope.
|
|
2409
|
+
|
|
2410
|
+
|
|
2411
|
+
On a hub, for a node's phone, the request is sent on to that node once (never retried),
|
|
2412
|
+
signed for you, and the node's answer is relayed unchanged. A cloud provider's phone answers
|
|
2413
|
+
`501`. The hub also refuses with `409 device_recording` while it records the node's phone.
|
|
2414
|
+
responses:
|
|
2415
|
+
'200':
|
|
2416
|
+
description: Stopped.
|
|
2417
|
+
content:
|
|
2418
|
+
application/json:
|
|
2419
|
+
schema:
|
|
2420
|
+
$ref: '#/components/schemas/Success'
|
|
2421
|
+
example:
|
|
2422
|
+
success: true
|
|
2423
|
+
'400':
|
|
2424
|
+
description: The udid is malformed.
|
|
2425
|
+
content:
|
|
2426
|
+
application/json:
|
|
2427
|
+
schema:
|
|
2428
|
+
$ref: '#/components/schemas/Error'
|
|
2429
|
+
examples:
|
|
2430
|
+
invalidUdid:
|
|
2431
|
+
summary: The udid segment is not valid percent-encoding
|
|
2432
|
+
value:
|
|
2433
|
+
success: false
|
|
2434
|
+
error: invalid_udid
|
|
2435
|
+
'401':
|
|
2436
|
+
$ref: '#/components/responses/Unauthorized'
|
|
2437
|
+
'403':
|
|
2438
|
+
description: >-
|
|
2439
|
+
Another user holds the device and you aren't an admin; or the credential lacks the
|
|
2440
|
+
`devices` scope, or a browser request failed the same-origin check.
|
|
2441
|
+
content:
|
|
2442
|
+
application/json:
|
|
2443
|
+
schema:
|
|
2444
|
+
$ref: '#/components/schemas/Error'
|
|
2445
|
+
examples:
|
|
2446
|
+
held:
|
|
2447
|
+
summary: Another user's hold
|
|
2448
|
+
value:
|
|
2449
|
+
success: false
|
|
2450
|
+
error: lock_owned_by_another_user
|
|
2451
|
+
message: >-
|
|
2452
|
+
This device is being controlled by another user. Ask them to stop, or use an
|
|
2453
|
+
admin key to force-release.
|
|
2454
|
+
scope:
|
|
2455
|
+
summary: No devices scope
|
|
2456
|
+
value:
|
|
2457
|
+
error: insufficient scope
|
|
2458
|
+
'404':
|
|
2459
|
+
$ref: '#/components/responses/ControlDeviceNotFound'
|
|
2460
|
+
'409':
|
|
2461
|
+
description: The device is being recorded.
|
|
2462
|
+
content:
|
|
2463
|
+
application/json:
|
|
2464
|
+
schema:
|
|
2465
|
+
$ref: '#/components/schemas/Error'
|
|
2466
|
+
example:
|
|
2467
|
+
success: false
|
|
2468
|
+
error: device_recording
|
|
2469
|
+
message: This device is being recorded. Stop the recording first.
|
|
2470
|
+
groupId: 7d2e9b4a-5c1f-4e8b-a3d6-0f9c2b7e1a54
|
|
2471
|
+
'429':
|
|
2472
|
+
$ref: '#/components/responses/RateLimited'
|
|
2473
|
+
'500':
|
|
2474
|
+
description: Stopping failed.
|
|
2475
|
+
content:
|
|
2476
|
+
application/json:
|
|
2477
|
+
schema:
|
|
2478
|
+
$ref: '#/components/schemas/Error'
|
|
2479
|
+
example:
|
|
2480
|
+
success: false
|
|
2481
|
+
error: kill ESRCH
|
|
2482
|
+
'501':
|
|
2483
|
+
$ref: '#/components/responses/ControlCloudPhone'
|
|
2484
|
+
'502':
|
|
2485
|
+
$ref: '#/components/responses/ControlNodeUnreachable'
|
|
2486
|
+
'503':
|
|
2487
|
+
$ref: '#/components/responses/ControlOwnershipUnavailable'
|
|
2488
|
+
'504':
|
|
2489
|
+
$ref: '#/components/responses/ControlNodeTimeout'
|
|
2490
|
+
components:
|
|
2491
|
+
parameters:
|
|
2492
|
+
ControlUdid:
|
|
2493
|
+
in: path
|
|
2494
|
+
name: udid
|
|
2495
|
+
required: true
|
|
2496
|
+
schema:
|
|
2497
|
+
type: string
|
|
2498
|
+
description: The device udid (URL-encoded).
|
|
2499
|
+
example: 00008110-00084CE80E51401E
|
|
2500
|
+
securitySchemes:
|
|
2501
|
+
ControlStreamTicket:
|
|
2502
|
+
type: apiKey
|
|
2503
|
+
in: query
|
|
2504
|
+
name: ticket
|
|
2505
|
+
description: >-
|
|
2506
|
+
A single-use, 60-second ticket from `POST /api/control/{udid}/stream/ticket`. Accepted only
|
|
2507
|
+
by `GET /api/control/{udid}/stream` (and the preview and logcat WebSockets) for the device
|
|
2508
|
+
it was minted for.
|
|
2509
|
+
responses:
|
|
2510
|
+
ControlDeviceNotFound:
|
|
2511
|
+
description: No such device, or it is outside your teams (the two answer the same).
|
|
2512
|
+
content:
|
|
2513
|
+
application/json:
|
|
2514
|
+
schema:
|
|
2515
|
+
$ref: '#/components/schemas/Error'
|
|
2516
|
+
example:
|
|
2517
|
+
error: not_found
|
|
2518
|
+
message: Device not found
|
|
2519
|
+
ControlForbiddenMutation:
|
|
2520
|
+
description: >-
|
|
2521
|
+
The credential lacks the `devices` scope or the `MEMBER` role, or a request with the
|
|
2522
|
+
dashboard cookie failed the same-origin check.
|
|
2523
|
+
content:
|
|
2524
|
+
application/json:
|
|
2525
|
+
schema:
|
|
2526
|
+
$ref: '#/components/schemas/Error'
|
|
2527
|
+
examples:
|
|
2528
|
+
scope:
|
|
2529
|
+
summary: No devices scope
|
|
2530
|
+
value:
|
|
2531
|
+
error: insufficient scope
|
|
2532
|
+
role:
|
|
2533
|
+
summary: Role below MEMBER
|
|
2534
|
+
value:
|
|
2535
|
+
error: requires role >= MEMBER
|
|
2536
|
+
csrf:
|
|
2537
|
+
summary: Cross-site browser request
|
|
2538
|
+
value:
|
|
2539
|
+
error: 'CSRF: Origin/Referer mismatch'
|
|
2540
|
+
ControlForbiddenRead:
|
|
2541
|
+
description: The caller's role is below `MEMBER`.
|
|
2542
|
+
content:
|
|
2543
|
+
application/json:
|
|
2544
|
+
schema:
|
|
2545
|
+
$ref: '#/components/schemas/Error'
|
|
2546
|
+
example:
|
|
2547
|
+
error: requires role >= MEMBER
|
|
2548
|
+
ControlHeld:
|
|
2549
|
+
description: >-
|
|
2550
|
+
Another user holds the device (their preview or recording), or it runs another user's Appium
|
|
2551
|
+
session. Admins are never refused. On a hub, a node's refusal is relayed with the holder's
|
|
2552
|
+
name filled in.
|
|
2553
|
+
content:
|
|
2554
|
+
application/json:
|
|
2555
|
+
schema:
|
|
2556
|
+
$ref: '#/components/schemas/ControlDenied'
|
|
2557
|
+
examples:
|
|
2558
|
+
held:
|
|
2559
|
+
summary: Another user's preview or recording
|
|
2560
|
+
value:
|
|
2561
|
+
success: false
|
|
2562
|
+
error: device_held_by_another_user
|
|
2563
|
+
message: >-
|
|
2564
|
+
Device is being controlled by Priya Raman. Ask them to release it, or use an admin
|
|
2565
|
+
key to force-release.
|
|
2566
|
+
holder:
|
|
2567
|
+
userId: c7a1e5d2-3f4b-4a9e-8c21-5b6d7e8f9a01
|
|
2568
|
+
name: Priya Raman
|
|
2569
|
+
session:
|
|
2570
|
+
summary: Another user's Appium session
|
|
2571
|
+
value:
|
|
2572
|
+
success: false
|
|
2573
|
+
error: device_in_use_by_session
|
|
2574
|
+
message: Device is in use by an Appium session owned by Priya Raman.
|
|
2575
|
+
holder:
|
|
2576
|
+
userId: c7a1e5d2-3f4b-4a9e-8c21-5b6d7e8f9a01
|
|
2577
|
+
name: Priya Raman
|
|
2578
|
+
ControlOwnershipUnavailable:
|
|
2579
|
+
description: >-
|
|
2580
|
+
The device or its session's owner couldn't be looked up, so access couldn't be checked.
|
|
2581
|
+
Never treated as allowed; try again.
|
|
2582
|
+
content:
|
|
2583
|
+
application/json:
|
|
2584
|
+
schema:
|
|
2585
|
+
$ref: '#/components/schemas/Error'
|
|
2586
|
+
example:
|
|
2587
|
+
success: false
|
|
2588
|
+
error: device_ownership_unavailable
|
|
2589
|
+
message: Could not verify device ownership. Try again.
|
|
2590
|
+
ControlCloudPhone:
|
|
2591
|
+
description: 'On a hub: the phone is a cloud provider''s, and device control isn''t available for it.'
|
|
2592
|
+
content:
|
|
2593
|
+
application/json:
|
|
2594
|
+
schema:
|
|
2595
|
+
$ref: '#/components/schemas/Error'
|
|
2596
|
+
example:
|
|
2597
|
+
success: false
|
|
2598
|
+
error: not_available_for_cloud_phone
|
|
2599
|
+
message: This phone is a cloud provider's. Device control isn't available for it here.
|
|
2600
|
+
ControlNodeUnreachable:
|
|
2601
|
+
description: 'On a hub: the phone''s node couldn''t be reached.'
|
|
2602
|
+
content:
|
|
2603
|
+
application/json:
|
|
2604
|
+
schema:
|
|
2605
|
+
$ref: '#/components/schemas/Error'
|
|
2606
|
+
example:
|
|
2607
|
+
success: false
|
|
2608
|
+
error: node_unreachable
|
|
2609
|
+
message: 'Xenon could not reach node http://10.0.4.21:4723 for this phone: ECONNREFUSED'
|
|
2610
|
+
ControlNodeTimeout:
|
|
2611
|
+
description: 'On a hub: the phone''s node didn''t start answering in time (60 s; 15 minutes for an install).'
|
|
2612
|
+
content:
|
|
2613
|
+
application/json:
|
|
2614
|
+
schema:
|
|
2615
|
+
$ref: '#/components/schemas/Error'
|
|
2616
|
+
example:
|
|
2617
|
+
success: false
|
|
2618
|
+
error: node_timeout
|
|
2619
|
+
message: Node http://10.0.4.21:4723 didn't answer within 60 s.
|
|
2620
|
+
schemas:
|
|
2621
|
+
ControlPoint:
|
|
2622
|
+
type: object
|
|
2623
|
+
required:
|
|
2624
|
+
- x
|
|
2625
|
+
- 'y'
|
|
2626
|
+
properties:
|
|
2627
|
+
x:
|
|
2628
|
+
type: number
|
|
2629
|
+
description: Horizontal position in screen points.
|
|
2630
|
+
example: 196
|
|
2631
|
+
'y':
|
|
2632
|
+
type: number
|
|
2633
|
+
description: Vertical position in screen points.
|
|
2634
|
+
example: 420
|
|
2635
|
+
ControlSwipeRequest:
|
|
2636
|
+
type: object
|
|
2637
|
+
required:
|
|
2638
|
+
- x
|
|
2639
|
+
- 'y'
|
|
2640
|
+
- endX
|
|
2641
|
+
- endY
|
|
2642
|
+
properties:
|
|
2643
|
+
x:
|
|
2644
|
+
type: number
|
|
2645
|
+
description: Start, horizontal.
|
|
2646
|
+
example: 196
|
|
2647
|
+
'y':
|
|
2648
|
+
type: number
|
|
2649
|
+
description: Start, vertical.
|
|
2650
|
+
example: 700
|
|
2651
|
+
endX:
|
|
2652
|
+
type: number
|
|
2653
|
+
description: End, horizontal.
|
|
2654
|
+
example: 196
|
|
2655
|
+
endY:
|
|
2656
|
+
type: number
|
|
2657
|
+
description: End, vertical.
|
|
2658
|
+
example: 200
|
|
2659
|
+
duration:
|
|
2660
|
+
type: number
|
|
2661
|
+
default: 1000
|
|
2662
|
+
description: Milliseconds.
|
|
2663
|
+
example: 300
|
|
2664
|
+
ControlTouchAndHoldRequest:
|
|
2665
|
+
type: object
|
|
2666
|
+
required:
|
|
2667
|
+
- x
|
|
2668
|
+
- 'y'
|
|
2669
|
+
properties:
|
|
2670
|
+
x:
|
|
2671
|
+
type: number
|
|
2672
|
+
description: Horizontal position in screen points.
|
|
2673
|
+
example: 196
|
|
2674
|
+
'y':
|
|
2675
|
+
type: number
|
|
2676
|
+
description: Vertical position in screen points.
|
|
2677
|
+
example: 420
|
|
2678
|
+
duration:
|
|
2679
|
+
type: number
|
|
2680
|
+
default: 1000
|
|
2681
|
+
description: How long to hold, in milliseconds.
|
|
2682
|
+
example: 1500
|
|
2683
|
+
ControlTextRequest:
|
|
2684
|
+
type: object
|
|
2685
|
+
required:
|
|
2686
|
+
- text
|
|
2687
|
+
properties:
|
|
2688
|
+
text:
|
|
2689
|
+
type: string
|
|
2690
|
+
example: qa.user@example.com
|
|
2691
|
+
ControlKeyEventRequest:
|
|
2692
|
+
type: object
|
|
2693
|
+
required:
|
|
2694
|
+
- keyCode
|
|
2695
|
+
properties:
|
|
2696
|
+
keyCode:
|
|
2697
|
+
oneOf:
|
|
2698
|
+
- type: integer
|
|
2699
|
+
- type: string
|
|
2700
|
+
description: >-
|
|
2701
|
+
Android: a key code number or `KEYCODE_…` name. iOS: a key or button name (`home`,
|
|
2702
|
+
`volumeUp`, `enter`, `backspace`…).
|
|
2703
|
+
example: 4
|
|
2704
|
+
ControlScreenshot:
|
|
2705
|
+
type: object
|
|
2706
|
+
properties:
|
|
2707
|
+
screenshot:
|
|
2708
|
+
type: string
|
|
2709
|
+
format: byte
|
|
2710
|
+
description: The image, base64 (PNG, or JPEG from a running Android preview).
|
|
2711
|
+
ControlDisplayState:
|
|
2712
|
+
type: object
|
|
2713
|
+
properties:
|
|
2714
|
+
state:
|
|
2715
|
+
type: string
|
|
2716
|
+
enum:
|
|
2717
|
+
- 'on'
|
|
2718
|
+
- 'off'
|
|
2719
|
+
- doze
|
|
2720
|
+
- unknown
|
|
2721
|
+
description: '`doze` is an always-on display; `unknown` means it could not be read.'
|
|
2722
|
+
ControlClipboard:
|
|
2723
|
+
type: object
|
|
2724
|
+
required:
|
|
2725
|
+
- content
|
|
2726
|
+
properties:
|
|
2727
|
+
content:
|
|
2728
|
+
type: string
|
|
2729
|
+
example: https://example.com/reset?code=4821
|
|
2730
|
+
ControlInstallPathRequest:
|
|
2731
|
+
type: object
|
|
2732
|
+
required:
|
|
2733
|
+
- appPath
|
|
2734
|
+
properties:
|
|
2735
|
+
appPath:
|
|
2736
|
+
type: string
|
|
2737
|
+
description: A path on this server's file system.
|
|
2738
|
+
example: /opt/builds/shop-2.14.0-debug.apk
|
|
2739
|
+
ControlInstallLibraryAppRequest:
|
|
2740
|
+
type: object
|
|
2741
|
+
required:
|
|
2742
|
+
- appId
|
|
2743
|
+
properties:
|
|
2744
|
+
appId:
|
|
2745
|
+
type: string
|
|
2746
|
+
description: The app's id in the app library.
|
|
2747
|
+
example: b3c1f0e2-6a8d-4b6e-9a51-2f7d0c9e4a13
|
|
2748
|
+
ControlUploadInstallRequest:
|
|
2749
|
+
type: object
|
|
2750
|
+
required:
|
|
2751
|
+
- app
|
|
2752
|
+
properties:
|
|
2753
|
+
app:
|
|
2754
|
+
type: string
|
|
2755
|
+
format: binary
|
|
2756
|
+
description: >-
|
|
2757
|
+
The app: `.apk` for Android, `.ipa` or zipped `.app` for iOS. At most 4 GiB. The file
|
|
2758
|
+
name's extension is kept.
|
|
2759
|
+
ControlInstallResult:
|
|
2760
|
+
type: object
|
|
2761
|
+
properties:
|
|
2762
|
+
success:
|
|
2763
|
+
type: boolean
|
|
2764
|
+
example: true
|
|
2765
|
+
message:
|
|
2766
|
+
type: string
|
|
2767
|
+
example: Installed Shop
|
|
2768
|
+
ControlUninstallRequest:
|
|
2769
|
+
type: object
|
|
2770
|
+
required:
|
|
2771
|
+
- bundleId
|
|
2772
|
+
properties:
|
|
2773
|
+
bundleId:
|
|
2774
|
+
type: string
|
|
2775
|
+
description: Package name (Android) or bundle id (iOS).
|
|
2776
|
+
example: com.example.shop
|
|
2777
|
+
ControlLogs:
|
|
2778
|
+
type: object
|
|
2779
|
+
properties:
|
|
2780
|
+
logs:
|
|
2781
|
+
type: string
|
|
2782
|
+
ControlShellRequest:
|
|
2783
|
+
type: object
|
|
2784
|
+
required:
|
|
2785
|
+
- command
|
|
2786
|
+
properties:
|
|
2787
|
+
command:
|
|
2788
|
+
type: string
|
|
2789
|
+
description: One allowlisted command (see the description).
|
|
2790
|
+
example: dumpsys battery
|
|
2791
|
+
ControlShellResult:
|
|
2792
|
+
type: object
|
|
2793
|
+
description: Exactly one of `output` and `error`.
|
|
2794
|
+
properties:
|
|
2795
|
+
output:
|
|
2796
|
+
type: string
|
|
2797
|
+
error:
|
|
2798
|
+
type: string
|
|
2799
|
+
description: Why the command was refused or failed.
|
|
2800
|
+
ControlStatusError:
|
|
2801
|
+
type: object
|
|
2802
|
+
properties:
|
|
2803
|
+
status:
|
|
2804
|
+
type: string
|
|
2805
|
+
enum:
|
|
2806
|
+
- error
|
|
2807
|
+
message:
|
|
2808
|
+
type: string
|
|
2809
|
+
example: OCR worker failed to start
|
|
2810
|
+
ControlRect:
|
|
2811
|
+
type: object
|
|
2812
|
+
properties:
|
|
2813
|
+
x:
|
|
2814
|
+
type: number
|
|
2815
|
+
example: 142
|
|
2816
|
+
'y':
|
|
2817
|
+
type: number
|
|
2818
|
+
example: 210
|
|
2819
|
+
width:
|
|
2820
|
+
type: number
|
|
2821
|
+
example: 46
|
|
2822
|
+
height:
|
|
2823
|
+
type: number
|
|
2824
|
+
example: 26
|
|
2825
|
+
ControlInspectorNode:
|
|
2826
|
+
type: object
|
|
2827
|
+
description: One element of the UI tree.
|
|
2828
|
+
properties:
|
|
2829
|
+
name:
|
|
2830
|
+
type: string
|
|
2831
|
+
example: Sign in
|
|
2832
|
+
type:
|
|
2833
|
+
type: string
|
|
2834
|
+
example: XCUIElementTypeButton
|
|
2835
|
+
text:
|
|
2836
|
+
type: string
|
|
2837
|
+
label:
|
|
2838
|
+
type: string
|
|
2839
|
+
value:
|
|
2840
|
+
type: string
|
|
2841
|
+
enabled:
|
|
2842
|
+
type: boolean
|
|
2843
|
+
visible:
|
|
2844
|
+
type: boolean
|
|
2845
|
+
rect:
|
|
2846
|
+
$ref: '#/components/schemas/ControlRect'
|
|
2847
|
+
xpath:
|
|
2848
|
+
type: string
|
|
2849
|
+
example: //XCUIElementTypeButton[@name="Sign in"]
|
|
2850
|
+
suggestedLocators:
|
|
2851
|
+
type: array
|
|
2852
|
+
items:
|
|
2853
|
+
type: object
|
|
2854
|
+
properties:
|
|
2855
|
+
strategy:
|
|
2856
|
+
type: string
|
|
2857
|
+
example: accessibility id
|
|
2858
|
+
value:
|
|
2859
|
+
type: string
|
|
2860
|
+
example: Sign in
|
|
2861
|
+
unique:
|
|
2862
|
+
type: boolean
|
|
2863
|
+
example: true
|
|
2864
|
+
score:
|
|
2865
|
+
type: integer
|
|
2866
|
+
minimum: 0
|
|
2867
|
+
maximum: 100
|
|
2868
|
+
description: Robustness, 0 to 100.
|
|
2869
|
+
example: 95
|
|
2870
|
+
suggestedActions:
|
|
2871
|
+
type: array
|
|
2872
|
+
items:
|
|
2873
|
+
type: object
|
|
2874
|
+
properties:
|
|
2875
|
+
action:
|
|
2876
|
+
type: string
|
|
2877
|
+
example: click
|
|
2878
|
+
snippet:
|
|
2879
|
+
type: string
|
|
2880
|
+
example: await driver.$('~Sign in').click();
|
|
2881
|
+
description:
|
|
2882
|
+
type: string
|
|
2883
|
+
example: Tap the button
|
|
2884
|
+
children:
|
|
2885
|
+
type: array
|
|
2886
|
+
items:
|
|
2887
|
+
$ref: '#/components/schemas/ControlInspectorNode'
|
|
2888
|
+
attributes:
|
|
2889
|
+
type: object
|
|
2890
|
+
additionalProperties: true
|
|
2891
|
+
ControlInspectorSnapshot:
|
|
2892
|
+
type: object
|
|
2893
|
+
properties:
|
|
2894
|
+
udid:
|
|
2895
|
+
type: string
|
|
2896
|
+
example: 00008110-00084CE80E51401E
|
|
2897
|
+
platform:
|
|
2898
|
+
type: string
|
|
2899
|
+
example: ios
|
|
2900
|
+
timestamp:
|
|
2901
|
+
type: string
|
|
2902
|
+
format: date-time
|
|
2903
|
+
example: '2026-10-04T14:05:22.118Z'
|
|
2904
|
+
screenshot:
|
|
2905
|
+
type: string
|
|
2906
|
+
format: byte
|
|
2907
|
+
description: The screenshot, base64.
|
|
2908
|
+
hierarchy:
|
|
2909
|
+
$ref: '#/components/schemas/ControlInspectorNode'
|
|
2910
|
+
hierarchySource:
|
|
2911
|
+
type: string
|
|
2912
|
+
enum:
|
|
2913
|
+
- appium-session
|
|
2914
|
+
- device
|
|
2915
|
+
description: '`appium-session` when the tree is a running session''s page source.'
|
|
2916
|
+
sessionId:
|
|
2917
|
+
type: string
|
|
2918
|
+
nullable: true
|
|
2919
|
+
description: The session the tree came from; null when read from the device.
|
|
2920
|
+
example: null
|
|
2921
|
+
metadata:
|
|
2922
|
+
type: object
|
|
2923
|
+
properties:
|
|
2924
|
+
screenWidth:
|
|
2925
|
+
type: integer
|
|
2926
|
+
example: 393
|
|
2927
|
+
screenHeight:
|
|
2928
|
+
type: integer
|
|
2929
|
+
example: 852
|
|
2930
|
+
ControlOmniScanResult:
|
|
2931
|
+
type: object
|
|
2932
|
+
properties:
|
|
2933
|
+
status:
|
|
2934
|
+
type: string
|
|
2935
|
+
enum:
|
|
2936
|
+
- success
|
|
2937
|
+
value:
|
|
2938
|
+
type: object
|
|
2939
|
+
description: The analysis.
|
|
2940
|
+
properties:
|
|
2941
|
+
timestamp:
|
|
2942
|
+
type: string
|
|
2943
|
+
format: date-time
|
|
2944
|
+
ocr:
|
|
2945
|
+
type: object
|
|
2946
|
+
properties:
|
|
2947
|
+
text:
|
|
2948
|
+
type: string
|
|
2949
|
+
words:
|
|
2950
|
+
type: array
|
|
2951
|
+
items:
|
|
2952
|
+
type: object
|
|
2953
|
+
properties:
|
|
2954
|
+
text:
|
|
2955
|
+
type: string
|
|
2956
|
+
confidence:
|
|
2957
|
+
type: number
|
|
2958
|
+
description: 0 to 100.
|
|
2959
|
+
bbox:
|
|
2960
|
+
type: object
|
|
2961
|
+
properties:
|
|
2962
|
+
x0:
|
|
2963
|
+
type: number
|
|
2964
|
+
y0:
|
|
2965
|
+
type: number
|
|
2966
|
+
x1:
|
|
2967
|
+
type: number
|
|
2968
|
+
y1:
|
|
2969
|
+
type: number
|
|
2970
|
+
ai_insights:
|
|
2971
|
+
type: object
|
|
2972
|
+
nullable: true
|
|
2973
|
+
additionalProperties: true
|
|
2974
|
+
description: The AI provider's analysis; null if it was skipped or failed.
|
|
2975
|
+
ControlTestLocatorRequest:
|
|
2976
|
+
type: object
|
|
2977
|
+
required:
|
|
2978
|
+
- strategy
|
|
2979
|
+
- selector
|
|
2980
|
+
properties:
|
|
2981
|
+
strategy:
|
|
2982
|
+
type: string
|
|
2983
|
+
enum:
|
|
2984
|
+
- '-custom:ai-text'
|
|
2985
|
+
- '-custom:ai-icon'
|
|
2986
|
+
example: '-custom:ai-text'
|
|
2987
|
+
selector:
|
|
2988
|
+
type: string
|
|
2989
|
+
description: The text to find, or a description of the icon.
|
|
2990
|
+
example: Sign in
|
|
2991
|
+
ControlTestLocatorResult:
|
|
2992
|
+
type: object
|
|
2993
|
+
properties:
|
|
2994
|
+
status:
|
|
2995
|
+
type: string
|
|
2996
|
+
enum:
|
|
2997
|
+
- success
|
|
2998
|
+
value:
|
|
2999
|
+
type: array
|
|
3000
|
+
items:
|
|
3001
|
+
type: object
|
|
3002
|
+
properties:
|
|
3003
|
+
id:
|
|
3004
|
+
type: string
|
|
3005
|
+
description: A virtual element id.
|
|
3006
|
+
example: omni_ocr_1759586722118_k3j9x
|
|
3007
|
+
text:
|
|
3008
|
+
type: string
|
|
3009
|
+
description: The matched word (`ai-text` only).
|
|
3010
|
+
rect:
|
|
3011
|
+
$ref: '#/components/schemas/ControlRect'
|
|
3012
|
+
confidence:
|
|
3013
|
+
type: number
|
|
3014
|
+
description: 0 to 1.
|
|
3015
|
+
example: 0.962
|
|
3016
|
+
ControlAppiumSession:
|
|
3017
|
+
type: object
|
|
3018
|
+
properties:
|
|
3019
|
+
status:
|
|
3020
|
+
type: string
|
|
3021
|
+
enum:
|
|
3022
|
+
- success
|
|
3023
|
+
sessionId:
|
|
3024
|
+
type: string
|
|
3025
|
+
nullable: true
|
|
3026
|
+
description: The Appium session driving the device; null when none is.
|
|
3027
|
+
basePath:
|
|
3028
|
+
type: string
|
|
3029
|
+
description: This server's WebDriver base path (`''` for the root).
|
|
3030
|
+
example: /wd/hub
|
|
3031
|
+
ControlStreamStartResult:
|
|
3032
|
+
type: object
|
|
3033
|
+
properties:
|
|
3034
|
+
success:
|
|
3035
|
+
type: boolean
|
|
3036
|
+
example: true
|
|
3037
|
+
type:
|
|
3038
|
+
type: string
|
|
3039
|
+
enum:
|
|
3040
|
+
- mjpeg
|
|
3041
|
+
- h264
|
|
3042
|
+
mjpegPort:
|
|
3043
|
+
type: integer
|
|
3044
|
+
description: 'MJPEG only: the local capture port.'
|
|
3045
|
+
example: 9101
|
|
3046
|
+
streamUrl:
|
|
3047
|
+
type: string
|
|
3048
|
+
description: 'MJPEG only: where to watch.'
|
|
3049
|
+
example: /xenon/api/control/00008110-00084CE80E51401E/stream
|
|
3050
|
+
h264Path:
|
|
3051
|
+
type: string
|
|
3052
|
+
description: 'H.264 only: the WebSocket path (open it with a ticket).'
|
|
3053
|
+
example: /xenon/api/control/R5CT32ABCDE/stream/h264
|
|
3054
|
+
ControlStreamTicket:
|
|
3055
|
+
type: object
|
|
3056
|
+
properties:
|
|
3057
|
+
ticket:
|
|
3058
|
+
type: string
|
|
3059
|
+
description: Single use, bound to this device and to you.
|
|
3060
|
+
expiresIn:
|
|
3061
|
+
type: integer
|
|
3062
|
+
description: Seconds.
|
|
3063
|
+
example: 60
|
|
3064
|
+
ControlStreamStatus:
|
|
3065
|
+
type: object
|
|
3066
|
+
properties:
|
|
3067
|
+
udid:
|
|
3068
|
+
type: string
|
|
3069
|
+
example: 00008110-00084CE80E51401E
|
|
3070
|
+
status:
|
|
3071
|
+
type: string
|
|
3072
|
+
example: running
|
|
3073
|
+
description: >-
|
|
3074
|
+
`stopped` when no preview runs; otherwise the stream's state (`starting`, `running`,
|
|
3075
|
+
`error`, …).
|
|
3076
|
+
type:
|
|
3077
|
+
type: string
|
|
3078
|
+
enum:
|
|
3079
|
+
- mjpeg
|
|
3080
|
+
- h264
|
|
3081
|
+
description: The transport a player should use.
|
|
3082
|
+
h264Path:
|
|
3083
|
+
type: string
|
|
3084
|
+
description: Android with H.264 only.
|
|
3085
|
+
mjpegPort:
|
|
3086
|
+
type: integer
|
|
3087
|
+
nullable: true
|
|
3088
|
+
wdaPort:
|
|
3089
|
+
type: integer
|
|
3090
|
+
description: iOS only.
|
|
3091
|
+
startedAt:
|
|
3092
|
+
type: integer
|
|
3093
|
+
description: iOS only. Epoch milliseconds.
|
|
3094
|
+
lastError:
|
|
3095
|
+
type: string
|
|
3096
|
+
description: iOS only.
|
|
3097
|
+
ControlStreamLeaveResult:
|
|
3098
|
+
type: object
|
|
3099
|
+
properties:
|
|
3100
|
+
success:
|
|
3101
|
+
type: boolean
|
|
3102
|
+
example: true
|
|
3103
|
+
pending:
|
|
3104
|
+
type: boolean
|
|
3105
|
+
example: true
|
|
3106
|
+
ControlDenied:
|
|
3107
|
+
type: object
|
|
3108
|
+
required:
|
|
3109
|
+
- error
|
|
3110
|
+
properties:
|
|
3111
|
+
success:
|
|
3112
|
+
type: boolean
|
|
3113
|
+
example: false
|
|
3114
|
+
error:
|
|
3115
|
+
type: string
|
|
3116
|
+
enum:
|
|
3117
|
+
- device_held_by_another_user
|
|
3118
|
+
- device_in_use_by_session
|
|
3119
|
+
message:
|
|
3120
|
+
type: string
|
|
3121
|
+
holder:
|
|
3122
|
+
type: object
|
|
3123
|
+
description: Who holds the device, when known.
|
|
3124
|
+
properties:
|
|
3125
|
+
userId:
|
|
3126
|
+
type: string
|
|
3127
|
+
name:
|
|
3128
|
+
type: string
|
|
3129
|
+
description: The holder's name, when it could be looked up.
|