device-devtools-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +327 -0
- package/LICENSE +21 -0
- package/README.md +491 -0
- package/VERSION +1 -0
- package/bin/device-devtools-mcp.js +106 -0
- package/bin/devicetools +315 -0
- package/config.example.json +63 -0
- package/integrations/agent-pointer.sh +48 -0
- package/integrations/claude/SKILL.md +8 -0
- package/integrations/cursor/devicetools.mdc +8 -0
- package/integrations/gemini/GEMINI.md +5 -0
- package/integrations/mcp/README.md +184 -0
- package/integrations/mcp/mcp.json +10 -0
- package/integrations/mcp/reference.sh +119 -0
- package/integrations/mcp/selftest.sh +158 -0
- package/integrations/mcp/server.sh +343 -0
- package/package.json +50 -0
- package/scripts/android/app.sh +241 -0
- package/scripts/android/back.sh +73 -0
- package/scripts/android/controls.sh +56 -0
- package/scripts/android/devices.sh +77 -0
- package/scripts/android/doctor.sh +253 -0
- package/scripts/android/lib.sh +699 -0
- package/scripts/android/logs.sh +289 -0
- package/scripts/android/permission.sh +119 -0
- package/scripts/android/settings.sh +63 -0
- package/scripts/android/setup.sh +97 -0
- package/scripts/android/tree.awk +166 -0
- package/scripts/android/tree.sh +127 -0
- package/scripts/android/type.sh +191 -0
- package/scripts/common/find.sh +95 -0
- package/scripts/common/key.sh +65 -0
- package/scripts/common/lib.sh +14 -0
- package/scripts/common/measure.sh +170 -0
- package/scripts/common/open.sh +131 -0
- package/scripts/common/screenshot.sh +102 -0
- package/scripts/common/scroll.sh +177 -0
- package/scripts/common/snapshot.sh +69 -0
- package/scripts/common/swipe.sh +171 -0
- package/scripts/common/tap.sh +226 -0
- package/scripts/common/wait.sh +212 -0
- package/scripts/common/waypoint.sh +141 -0
- package/scripts/dispatch.sh +21 -0
- package/scripts/flow.sh +266 -0
- package/scripts/init.sh +101 -0
- package/scripts/ios/app.sh +404 -0
- package/scripts/ios/back.sh +95 -0
- package/scripts/ios/controls.sh +68 -0
- package/scripts/ios/devices.sh +80 -0
- package/scripts/ios/doctor.sh +386 -0
- package/scripts/ios/lib.sh +864 -0
- package/scripts/ios/logs.sh +272 -0
- package/scripts/ios/permission.sh +108 -0
- package/scripts/ios/settings.sh +76 -0
- package/scripts/ios/setup.sh +175 -0
- package/scripts/ios/tree.sh +128 -0
- package/scripts/ios/type.sh +178 -0
- package/scripts/lib.sh +1032 -0
- package/scripts/links.tsv +44 -0
- package/scripts/relink.sh +121 -0
- package/scripts/run.sh +415 -0
- package/scripts/selftest.sh +1709 -0
- package/scripts/snapshot.awk +362 -0
- package/scripts/verify-npm-package.js +133 -0
- package/tests/fixtures/ios-contacts-list.expected +52 -0
- package/tests/fixtures/ios-contacts-list.rows +140 -0
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "device-devtools-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for a phone: read a native app's screen as measurable elements, act on them, and refuse rather than guess",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"model-context-protocol",
|
|
8
|
+
"ios",
|
|
9
|
+
"android",
|
|
10
|
+
"xcuitest",
|
|
11
|
+
"webdriveragent",
|
|
12
|
+
"uiautomator2",
|
|
13
|
+
"accessibility",
|
|
14
|
+
"devtools"
|
|
15
|
+
],
|
|
16
|
+
"repository": "hung977/device-devtools-mcp",
|
|
17
|
+
"homepage": "https://github.com/hung977/device-devtools-mcp#readme",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/hung977/device-devtools-mcp/issues"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"mcpName": "io.github.hung977/device-devtools-mcp",
|
|
23
|
+
"type": "commonjs",
|
|
24
|
+
"bin": {
|
|
25
|
+
"device-devtools-mcp": "bin/device-devtools-mcp.js"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18"
|
|
29
|
+
},
|
|
30
|
+
"os": [
|
|
31
|
+
"darwin",
|
|
32
|
+
"linux"
|
|
33
|
+
],
|
|
34
|
+
"files": [
|
|
35
|
+
"bin/",
|
|
36
|
+
"scripts/",
|
|
37
|
+
"integrations/",
|
|
38
|
+
"tests/",
|
|
39
|
+
"config.example.json",
|
|
40
|
+
"AGENTS.md",
|
|
41
|
+
"VERSION",
|
|
42
|
+
"LICENSE",
|
|
43
|
+
"README.md"
|
|
44
|
+
],
|
|
45
|
+
"scripts": {
|
|
46
|
+
"test": "scripts/selftest.sh && integrations/mcp/selftest.sh",
|
|
47
|
+
"prepack": "scripts/relink.sh --write",
|
|
48
|
+
"verify-npm-package": "node scripts/verify-npm-package.js"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# app.sh — the app under test's lifecycle.
|
|
3
|
+
#
|
|
4
|
+
# app.sh installed <id> is some other app present?
|
|
5
|
+
# app.sh state
|
|
6
|
+
# app.sh launch | kill | relaunch
|
|
7
|
+
# app.sh reset
|
|
8
|
+
# app.sh install [apk]
|
|
9
|
+
# app.sh build
|
|
10
|
+
#
|
|
11
|
+
# `reset` means something stronger here than it does on iOS. Android has
|
|
12
|
+
# `pm clear`, which discards the app's data and cache in place, so a reset is
|
|
13
|
+
# one command and does not need a copy of the package to reinstall from. It is
|
|
14
|
+
# still destructive: accounts are logged out and stored state is gone.
|
|
15
|
+
#
|
|
16
|
+
# `relaunch` restarts the process and keeps stored data. A test that navigates
|
|
17
|
+
# somewhere and stops there passes once and fails on the second run, which reads
|
|
18
|
+
# like flakiness and is not. Either end where you started, or use `reset`.
|
|
19
|
+
#
|
|
20
|
+
# --snapshot prints the screen this left behind and renumbers the uids, so the
|
|
21
|
+
# read that would have been the next call is folded into this one. In a measured
|
|
22
|
+
# session — log in, open Settings — twelve calls did the work and six of them
|
|
23
|
+
# were snapshots taken only to find out what the previous action produced.
|
|
24
|
+
#
|
|
25
|
+
# exit 0 done
|
|
26
|
+
# exit 1 usage error
|
|
27
|
+
# exit 2 environment or configuration problem
|
|
28
|
+
# exit 3 device unreachable — run doctor.sh
|
|
29
|
+
# exit 4 the command ran but the app did not end up in the expected state
|
|
30
|
+
|
|
31
|
+
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
|
|
32
|
+
|
|
33
|
+
CMD="${1-}"
|
|
34
|
+
case "$CMD" in
|
|
35
|
+
-h|--help) awk 'NR > 1 { if (!/^#/) exit; sub(/^# ?/, ""); print }' "${BASH_SOURCE[0]}"; exit 0 ;;
|
|
36
|
+
esac
|
|
37
|
+
[ -n "$CMD" ] || die "missing verb (usage: app.sh state|launch|kill|relaunch|reset|install|build)" 1
|
|
38
|
+
shift
|
|
39
|
+
|
|
40
|
+
# --snapshot IS ANSWERED ONCE, HERE, AND NOT BY EVERY VERB SEPARATELY.
|
|
41
|
+
#
|
|
42
|
+
# Each verb below counts its own arguments and refuses a stray one, which is
|
|
43
|
+
# right and is also why a flag that applies to all of them cannot be added to
|
|
44
|
+
# all of them: it would have to be subtracted from eight separate `[ $# -eq 0 ]`
|
|
45
|
+
# guards. Taken out of the list up front, every verb sees exactly what it saw
|
|
46
|
+
# before, and the screen is printed at the end of whichever one ran.
|
|
47
|
+
DT_ARGV=()
|
|
48
|
+
for a in ${@+"$@"}; do
|
|
49
|
+
case "$a" in
|
|
50
|
+
--snapshot) act_snapshot_on ;;
|
|
51
|
+
*) DT_ARGV+=("$a") ;;
|
|
52
|
+
esac
|
|
53
|
+
done
|
|
54
|
+
set -- ${DT_ARGV[@]+"${DT_ARGV[@]}"}
|
|
55
|
+
|
|
56
|
+
load_config
|
|
57
|
+
|
|
58
|
+
PKG="$(cfg '.app.package' '')"
|
|
59
|
+
[ -n "$PKG" ] || die "app.package is not set in $DT_CONFIG — every verb here needs it" 2
|
|
60
|
+
|
|
61
|
+
installed() {
|
|
62
|
+
pkg_installed "$PKG"
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
running() {
|
|
66
|
+
[ -n "$(adb_dev shell pidof "$PKG" 2>/dev/null | tr -d '\r ')" ]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# The focused window, as one line, tested afterwards rather than piped into a
|
|
70
|
+
# second grep.
|
|
71
|
+
#
|
|
72
|
+
# `dumpsys | grep -m1 X | grep -q Y` looks equivalent and is not: `grep -q`
|
|
73
|
+
# exits the moment it matches, `grep -m1` likewise, and the SIGPIPE that gives
|
|
74
|
+
# the upstream command becomes a pipeline failure under `set -o pipefail`. The
|
|
75
|
+
# function then reports "not in the foreground" for an app that is — measured
|
|
76
|
+
# here as a launch that worked being reported as a launch that timed out.
|
|
77
|
+
#
|
|
78
|
+
# TWO QUESTIONS, NOT ONE. The focused window is the fast answer and it is wrong
|
|
79
|
+
# for popups: open the overflow menu and the focus moves to a PopupWindow whose
|
|
80
|
+
# name does not carry the package, so an app plainly on screen with its own menu
|
|
81
|
+
# open reports as backgrounded. Found while walking the app, which recorded two taps as
|
|
82
|
+
# "left the app" when they had opened a menu.
|
|
83
|
+
#
|
|
84
|
+
# So the resumed activity is asked as well. That is the activity Android has
|
|
85
|
+
# actually resumed, which a popup belonging to the app does not change.
|
|
86
|
+
foreground() {
|
|
87
|
+
local line
|
|
88
|
+
line="$(adb_dev shell dumpsys window 2>/dev/null | tr -d '\r' | awk '/mCurrentFocus/ { print; exit }')"
|
|
89
|
+
case "$line" in
|
|
90
|
+
*"$PKG"*) return 0 ;;
|
|
91
|
+
esac
|
|
92
|
+
line="$(adb_dev shell dumpsys activity activities 2>/dev/null | tr -d '\r' \
|
|
93
|
+
| awk '/topResumedActivity|mResumedActivity/ { print; exit }')"
|
|
94
|
+
case "$line" in
|
|
95
|
+
*"$PKG"*) return 0 ;;
|
|
96
|
+
*) return 1 ;;
|
|
97
|
+
esac
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# state_name — one of the four words every message here uses, so that "what is
|
|
101
|
+
# the app doing" always reads the same way whichever verb reports it.
|
|
102
|
+
state_name() {
|
|
103
|
+
if ! installed; then printf 'not installed'
|
|
104
|
+
elif foreground; then printf 'foreground'
|
|
105
|
+
elif running; then printf 'running in background'
|
|
106
|
+
else printf 'not running'
|
|
107
|
+
fi
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# The launcher activity is resolved rather than configured. Hardcoding it in the
|
|
111
|
+
# config means every launch breaks the day the app renames its entry point, and
|
|
112
|
+
# the package manager already knows the answer.
|
|
113
|
+
launcher_activity() {
|
|
114
|
+
local a
|
|
115
|
+
a="$(cfg '.app.activity' '')"
|
|
116
|
+
if [ -n "$a" ]; then printf '%s' "$a"; return 0; fi
|
|
117
|
+
a="$(adb_dev shell cmd package resolve-activity --brief "$PKG" 2>/dev/null \
|
|
118
|
+
| tr -d '\r' | grep -m1 "^$PKG/" || true)"
|
|
119
|
+
[ -n "$a" ] || die "cannot find a launcher activity for $PKG — set app.activity in $DT_CONFIG" 2
|
|
120
|
+
printf '%s' "$a"
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# Waiting for a state rather than sleeping through it. Launch was measured
|
|
124
|
+
# taking anywhere from under a second to several, and a fixed sleep is either
|
|
125
|
+
# wasted time or a race.
|
|
126
|
+
wait_for() {
|
|
127
|
+
local want="$1" limit="${2:-15}" i=0
|
|
128
|
+
while [ "$i" -lt "$((limit * 4))" ]; do
|
|
129
|
+
case "$want" in
|
|
130
|
+
up) foreground && return 0 ;;
|
|
131
|
+
down) running || return 0 ;;
|
|
132
|
+
esac
|
|
133
|
+
sleep 0.25
|
|
134
|
+
i=$((i + 1))
|
|
135
|
+
done
|
|
136
|
+
return 1
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
do_launch() {
|
|
140
|
+
local act
|
|
141
|
+
act="$(launcher_activity)"
|
|
142
|
+
adb_dev shell am start -W -n "$act" >/dev/null 2>&1 \
|
|
143
|
+
|| die "am start $act failed — run $(as_cmd doctor)" 3
|
|
144
|
+
wait_for up || die "launched $PKG but it did not reach the foreground within 15s" 4
|
|
145
|
+
# The driver's session refers to the process that has just been replaced, so
|
|
146
|
+
# it is dropped rather than left to fail on the next verb with a message about
|
|
147
|
+
# sessions that says nothing about what actually happened.
|
|
148
|
+
ua2_drop_session
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
case "$CMD" in
|
|
152
|
+
# installed <package> — is some OTHER app on this device?
|
|
153
|
+
#
|
|
154
|
+
# Every other verb here acts on the app under test. This one answers about a
|
|
155
|
+
# package the test merely depends on: a browser for a deep link, a maps app
|
|
156
|
+
# for a hand-off. It exists because a CI image is not a phone, and a test that
|
|
157
|
+
# needs Chrome on an emulator that ships none should say so and be skipped
|
|
158
|
+
# rather than fail as though the product were broken.
|
|
159
|
+
#
|
|
160
|
+
# exit 4 for absent rather than exit 1, because "not there" is an answer.
|
|
161
|
+
installed)
|
|
162
|
+
[ $# -eq 1 ] || die "app.sh installed needs exactly one package name" 1
|
|
163
|
+
if pkg_installed "$1"; then
|
|
164
|
+
printf 'OK %s — installed\n' "$1"
|
|
165
|
+
else
|
|
166
|
+
die "$1 is not installed on $(device_serial)" 4
|
|
167
|
+
fi ;;
|
|
168
|
+
|
|
169
|
+
state)
|
|
170
|
+
[ $# -eq 0 ] || die "app.sh state takes no arguments" 1
|
|
171
|
+
printf 'OK %s — %s\n' "$PKG" "$(state_name)" ;;
|
|
172
|
+
|
|
173
|
+
launch)
|
|
174
|
+
[ $# -eq 0 ] || die "app.sh launch takes no arguments" 1
|
|
175
|
+
installed || die "$PKG is not installed — run $(as_cmd app install)" 4
|
|
176
|
+
do_launch
|
|
177
|
+
printf 'OK launched %s — %s\n' "$PKG" "$(state_name)" ;;
|
|
178
|
+
|
|
179
|
+
kill)
|
|
180
|
+
[ $# -eq 0 ] || die "app.sh kill takes no arguments" 1
|
|
181
|
+
adb_dev shell am force-stop "$PKG" >/dev/null 2>&1 \
|
|
182
|
+
|| die "force-stop failed — run $(as_cmd doctor)" 3
|
|
183
|
+
ua2_drop_session
|
|
184
|
+
wait_for down 10 || die "force-stop returned but $PKG is still running" 4
|
|
185
|
+
printf 'OK killed %s — %s\n' "$PKG" "$(state_name)" ;;
|
|
186
|
+
|
|
187
|
+
relaunch)
|
|
188
|
+
[ $# -eq 0 ] || die "app.sh relaunch takes no arguments" 1
|
|
189
|
+
installed || die "$PKG is not installed — run $(as_cmd app install)" 4
|
|
190
|
+
adb_dev shell am force-stop "$PKG" >/dev/null 2>&1 || true
|
|
191
|
+
ua2_drop_session
|
|
192
|
+
wait_for down 10 || true
|
|
193
|
+
do_launch
|
|
194
|
+
printf 'OK relaunched %s — %s (stored data kept; use reset to discard it)\n' "$PKG" "$(state_name)" ;;
|
|
195
|
+
|
|
196
|
+
reset)
|
|
197
|
+
[ $# -eq 0 ] || die "app.sh reset takes no arguments" 1
|
|
198
|
+
installed || die "$PKG is not installed — run $(as_cmd app install)" 4
|
|
199
|
+
out="$(adb_dev shell pm clear "$PKG" 2>&1 | tr -d '\r')"
|
|
200
|
+
case "$out" in
|
|
201
|
+
*Success*) ;;
|
|
202
|
+
*) die "pm clear $PKG did not report success — $(first_line "$out")" 4 ;;
|
|
203
|
+
esac
|
|
204
|
+
ua2_drop_session
|
|
205
|
+
printf 'OK reset %s — data and cache cleared, app is %s\n' "$PKG" "$(state_name)" ;;
|
|
206
|
+
|
|
207
|
+
install)
|
|
208
|
+
[ $# -le 1 ] || die "app.sh install takes at most a path to an apk" 1
|
|
209
|
+
APK="${1-}"
|
|
210
|
+
[ -n "$APK" ] || APK="$(cfg '.app.apk' '')"
|
|
211
|
+
[ -n "$APK" ] || die "install needs an apk — pass a path or set app.apk in $DT_CONFIG" 2
|
|
212
|
+
APK="$(expand_path "$APK")"
|
|
213
|
+
[ -f "$APK" ] || die "no such apk: $APK" 2
|
|
214
|
+
# -r reinstalls over an existing copy and keeps data; -g grants the runtime
|
|
215
|
+
# permissions declared in the manifest, so a first run does not stop on a
|
|
216
|
+
# permission dialog that has nothing to do with the test.
|
|
217
|
+
out="$(adb_dev install -r -g "$APK" 2>&1 | tr -d '\r' | tail -3)"
|
|
218
|
+
case "$out" in
|
|
219
|
+
*Success*) ;;
|
|
220
|
+
*) die "install failed — $(printf '%s' "$out" | grep -m1 -i 'failure\|error' || printf '%s' "$out" | tail -1)" 2 ;;
|
|
221
|
+
esac
|
|
222
|
+
installed || die "install reported success but $PKG is not present" 4
|
|
223
|
+
ua2_drop_session
|
|
224
|
+
printf 'OK installed %s from %s\n' "$PKG" "$APK" ;;
|
|
225
|
+
|
|
226
|
+
build)
|
|
227
|
+
[ $# -eq 0 ] || die "app.sh build takes no arguments" 1
|
|
228
|
+
BUILD_CMD="$(cfg '.app.build_command' '')"
|
|
229
|
+
[ -n "$BUILD_CMD" ] || die "app.build_command is not set in $DT_CONFIG" 2
|
|
230
|
+
# The build's own output is the one place a wall of text is the right answer:
|
|
231
|
+
# a failing Gradle build is diagnosed from its log and nothing else. It goes
|
|
232
|
+
# to stderr so that stdout stays the one-line contract.
|
|
233
|
+
( eval "$BUILD_CMD" ) >&2 || die "build failed — see the output above" 2
|
|
234
|
+
printf 'OK built %s\n' "$PKG" ;;
|
|
235
|
+
|
|
236
|
+
*)
|
|
237
|
+
die "unknown verb '$CMD' (usage: app.sh state|launch|kill|relaunch|reset|install|build)" 1 ;;
|
|
238
|
+
esac
|
|
239
|
+
|
|
240
|
+
# The screen the verb left behind, when it was asked for.
|
|
241
|
+
act_snapshot_now
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# back.sh — go back one screen.
|
|
3
|
+
#
|
|
4
|
+
# back.sh [--method nav|swipe]
|
|
5
|
+
#
|
|
6
|
+
# Android has a real system back action, so unlike iOS this is not a heuristic.
|
|
7
|
+
# `nav` sends the system back event and is the default. `swipe` performs a
|
|
8
|
+
# left-edge swipe, which is what gesture navigation sends and is worth having
|
|
9
|
+
# for the apps that handle the gesture but swallow the key event.
|
|
10
|
+
#
|
|
11
|
+
# The flag exists on both platforms so a test case does not have to know which
|
|
12
|
+
# one it is running on. What it selects differs, because the platforms differ,
|
|
13
|
+
# and pretending otherwise is how a cross-platform abstraction starts lying.
|
|
14
|
+
#
|
|
15
|
+
# It then verifies that the screen actually changed. A back that does nothing is
|
|
16
|
+
# worse than a back that fails: the test carries on believing it moved.
|
|
17
|
+
#
|
|
18
|
+
# --snapshot prints the screen this left behind and renumbers the uids, so the
|
|
19
|
+
# read that would have been the next call is folded into this one. In a measured
|
|
20
|
+
# session — log in, open Settings — twelve calls did the work and six of them
|
|
21
|
+
# were snapshots taken only to find out what the previous action produced.
|
|
22
|
+
#
|
|
23
|
+
# exit 0 went back, and the screen changed
|
|
24
|
+
# exit 1 usage error
|
|
25
|
+
# exit 3 UiAutomator2 unreachable — run doctor.sh
|
|
26
|
+
# exit 4 nothing to go back to, or the screen did not change
|
|
27
|
+
|
|
28
|
+
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
|
|
29
|
+
|
|
30
|
+
METHOD=nav
|
|
31
|
+
while [ $# -gt 0 ]; do
|
|
32
|
+
case "$1" in
|
|
33
|
+
--method)
|
|
34
|
+
[ $# -ge 2 ] || die "--method needs nav or swipe" 1
|
|
35
|
+
case "$2" in nav|swipe) METHOD="$2" ;; *) die "--method must be nav or swipe, got: $2" 1 ;; esac
|
|
36
|
+
shift 2 ;;
|
|
37
|
+
--snapshot) act_snapshot_on; shift ;;
|
|
38
|
+
-h|--help) awk 'NR > 1 { if (!/^#/) exit; sub(/^# ?/, ""); print }' "${BASH_SOURCE[0]}"; exit 0 ;;
|
|
39
|
+
*) die "unknown argument: $1 (usage: back.sh [--method nav|swipe])" 1 ;;
|
|
40
|
+
esac
|
|
41
|
+
done
|
|
42
|
+
|
|
43
|
+
load_config
|
|
44
|
+
|
|
45
|
+
src="$(fetch_source)"
|
|
46
|
+
before="$(drv_fingerprint "$src")"
|
|
47
|
+
act_before
|
|
48
|
+
read -r WIN_W WIN_H <<< "$(window_size "$src")"
|
|
49
|
+
|
|
50
|
+
if [ "$METHOD" = nav ]; then
|
|
51
|
+
resp="$(session_post '/back' '{}')"
|
|
52
|
+
err="$(session_error "$resp")"
|
|
53
|
+
[ -z "$err" ] || die "system back failed — $err" 3
|
|
54
|
+
used="system back"
|
|
55
|
+
else
|
|
56
|
+
# Kept off x=0 exactly: some devices treat the very edge as a bezel touch and
|
|
57
|
+
# discard it. The travel ends past the middle so the gesture is unambiguous.
|
|
58
|
+
FY=$(( WIN_H / 2 ))
|
|
59
|
+
TX=$(( WIN_W * 60 / 100 ))
|
|
60
|
+
err="$(drv_drag 2 "$FY" "$TX" "$FY" 50 200 || true)"
|
|
61
|
+
[ -z "$err" ] || die "edge swipe failed — $err" 3
|
|
62
|
+
used="edge swipe from 2,$FY"
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
after="$(drv_fingerprint "$(fetch_source)")"
|
|
66
|
+
[ "$before" != "$after" ] \
|
|
67
|
+
|| die "went back using the $used but the screen did not change — there may be nothing to go back to" 4
|
|
68
|
+
|
|
69
|
+
# back already refuses when the screen did not move, so the SCREEN line here
|
|
70
|
+
# is not the verdict — it is which screen you have arrived at, and what the app
|
|
71
|
+
# said on the way.
|
|
72
|
+
act_report BACK "$used"
|
|
73
|
+
journal_append back "" "" "" "" "" 0 0 0 0 "$DT_ACT_HASH"
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# controls.sh — the things on this screen you can actually act on.
|
|
3
|
+
#
|
|
4
|
+
# controls.sh [--all]
|
|
5
|
+
#
|
|
6
|
+
# One line per interactive element, in find.sh's columns:
|
|
7
|
+
#
|
|
8
|
+
# type | id | label | text | x,y,w,h
|
|
9
|
+
#
|
|
10
|
+
# This is usually a better first question than `tree`. A screen with 150
|
|
11
|
+
# elements has perhaps a dozen you can tap, and those dozen are what a test is
|
|
12
|
+
# written against; the rest is layout.
|
|
13
|
+
#
|
|
14
|
+
# Android answers this exactly: `clickable` is published on the element. iOS has
|
|
15
|
+
# to infer it from the element's type, so the two adapters disagree slightly in
|
|
16
|
+
# what they can promise, and each says which.
|
|
17
|
+
#
|
|
18
|
+
# --all also includes elements that are focusable or long-clickable but not
|
|
19
|
+
# clickable — scroll containers, and controls that only respond to a hold.
|
|
20
|
+
#
|
|
21
|
+
# exit 0 at least one control
|
|
22
|
+
# exit 1 usage error
|
|
23
|
+
# exit 3 UiAutomator2 unreachable — run doctor.sh
|
|
24
|
+
# exit 4 nothing interactive on screen
|
|
25
|
+
|
|
26
|
+
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
|
|
27
|
+
|
|
28
|
+
ALL=0
|
|
29
|
+
while [ $# -gt 0 ]; do
|
|
30
|
+
case "$1" in
|
|
31
|
+
--all) ALL=1; shift ;;
|
|
32
|
+
-h|--help) awk 'NR > 1 { if (!/^#/) exit; sub(/^# ?/, ""); print }' "${BASH_SOURCE[0]}"; exit 0 ;;
|
|
33
|
+
*) die "unknown argument: $1 (usage: controls.sh [--all])" 1 ;;
|
|
34
|
+
esac
|
|
35
|
+
done
|
|
36
|
+
|
|
37
|
+
load_config
|
|
38
|
+
src="$(fetch_source)"
|
|
39
|
+
|
|
40
|
+
# Columns of the table: depth|class|id|label|text|x|y|w|h|vis|en|pw|click|hint
|
|
41
|
+
rows="$(printf '%s' "$src" | awk -F'|' -v all="$ALL" '
|
|
42
|
+
$1 == "WINDOW" || $1 == "" { next }
|
|
43
|
+
$10 != 1 || $8 <= 0 || $9 <= 0 { next }
|
|
44
|
+
{
|
|
45
|
+
interactive = ($13 == 1)
|
|
46
|
+
# Text inputs are acted on by typing rather than by tapping, and a field
|
|
47
|
+
# that is not marked clickable is still very much a control.
|
|
48
|
+
if (!interactive && $2 ~ /Edit/) interactive = 1
|
|
49
|
+
if (!interactive && all == 1 && $11 == 1) interactive = 1
|
|
50
|
+
if (!interactive) next
|
|
51
|
+
printf "%s|%s|%s|%s|%s,%s,%s,%s\n", $2, $3, $4, $5, $6, $7, $8, $9
|
|
52
|
+
}')"
|
|
53
|
+
|
|
54
|
+
[ -n "$rows" ] || die "nothing interactive on screen — is the app in the foreground?" 4
|
|
55
|
+
|
|
56
|
+
printf '%s\n' "$rows" | awk -F'|' '{ printf "%s | %s | %s | %s | %s\n", $1, $2, $3, $4, $5 }'
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# devices.sh — what is attached, one per line.
|
|
3
|
+
#
|
|
4
|
+
# devices.sh [--ready]
|
|
5
|
+
#
|
|
6
|
+
# Output, pipe-separated so it parses without a JSON reader:
|
|
7
|
+
#
|
|
8
|
+
# <serial> | <kind> | <name> | <state>
|
|
9
|
+
#
|
|
10
|
+
# kind is emulator or device. state is ready, unauthorized, offline or
|
|
11
|
+
# no-permissions — anything other than ready means the device is attached but
|
|
12
|
+
# cannot be driven, and saying which is the difference between a five-second fix
|
|
13
|
+
# and an afternoon.
|
|
14
|
+
#
|
|
15
|
+
# --ready lists only the ones that can be driven.
|
|
16
|
+
#
|
|
17
|
+
# It reads `adb devices -l` and nothing else: no `adb shell` per device, because
|
|
18
|
+
# this runs before anything is known to be healthy and a shell into a wedged
|
|
19
|
+
# device is exactly where this would hang.
|
|
20
|
+
#
|
|
21
|
+
# exit 0 at least one device listed
|
|
22
|
+
# exit 1 usage error
|
|
23
|
+
# exit 2 adb is not installed
|
|
24
|
+
# exit 4 nothing attached (or nothing ready, with --ready)
|
|
25
|
+
|
|
26
|
+
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
|
|
27
|
+
|
|
28
|
+
READY_ONLY=0
|
|
29
|
+
while [ $# -gt 0 ]; do
|
|
30
|
+
case "$1" in
|
|
31
|
+
--ready) READY_ONLY=1; shift ;;
|
|
32
|
+
-h|--help) awk 'NR > 1 { if (!/^#/) exit; sub(/^# ?/, ""); print }' "${BASH_SOURCE[0]}"; exit 0 ;;
|
|
33
|
+
*) die "unknown argument: $1 (usage: devices.sh [--ready])" 1 ;;
|
|
34
|
+
esac
|
|
35
|
+
done
|
|
36
|
+
|
|
37
|
+
load_config
|
|
38
|
+
command -v adb >/dev/null 2>&1 || die "adb is not on PATH — see README.md" 2
|
|
39
|
+
|
|
40
|
+
n=0
|
|
41
|
+
while IFS= read -r line; do
|
|
42
|
+
[ -n "$line" ] || continue
|
|
43
|
+
case "$line" in "List of devices"*|"* daemon"*) continue ;; esac
|
|
44
|
+
|
|
45
|
+
serial="${line%% *}"
|
|
46
|
+
rest="${line#"$serial"}"
|
|
47
|
+
raw_state="$(printf '%s' "$rest" | awk '{print $1}')"
|
|
48
|
+
|
|
49
|
+
case "$raw_state" in
|
|
50
|
+
device) state=ready ;;
|
|
51
|
+
unauthorized) state=unauthorized ;;
|
|
52
|
+
offline) state=offline ;;
|
|
53
|
+
no) state=no-permissions ;; # "no permissions; see [http://…]"
|
|
54
|
+
*) state="$raw_state" ;;
|
|
55
|
+
esac
|
|
56
|
+
|
|
57
|
+
# The serial prefix is enough to tell an emulator apart here. Elsewhere the
|
|
58
|
+
# adapter also checks ro.kernel.qemu, because a renamed serial could mislead
|
|
59
|
+
# something that matters; a listing is not that.
|
|
60
|
+
case "$serial" in emulator-*) kind=emulator ;; *) kind=device ;; esac
|
|
61
|
+
|
|
62
|
+
name="$(printf '%s' "$rest" | tr ' ' '\n' | awk -F: '$1=="model" { print $2; exit }')"
|
|
63
|
+
[ -n "$name" ] || name="$serial"
|
|
64
|
+
name="$(printf '%s' "$name" | tr '_' ' ')"
|
|
65
|
+
|
|
66
|
+
[ "$READY_ONLY" -eq 0 ] || [ "$state" = ready ] || continue
|
|
67
|
+
|
|
68
|
+
printf '%s | %s | %s | %s\n' "$serial" "$kind" "$name" "$state"
|
|
69
|
+
n=$(( n + 1 ))
|
|
70
|
+
done <<< "$(adb devices -l 2>/dev/null | tr -d '\r')"
|
|
71
|
+
|
|
72
|
+
if [ "$n" -eq 0 ]; then
|
|
73
|
+
if [ "$READY_ONLY" -eq 1 ]; then
|
|
74
|
+
die "no device is ready — run $(as_cmd devices) without --ready to see what is attached" 4
|
|
75
|
+
fi
|
|
76
|
+
die "nothing attached — connect a device or start an emulator" 4
|
|
77
|
+
fi
|