mobile-debug-mcp 0.21.5 → 0.23.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.
Files changed (90) hide show
  1. package/AGENTS.md +74 -0
  2. package/README.md +24 -5
  3. package/dist/interact/classify.js +35 -0
  4. package/dist/interact/index.js +220 -13
  5. package/dist/network/index.js +232 -0
  6. package/dist/observe/ios.js +10 -3
  7. package/dist/server-core.js +822 -0
  8. package/dist/server.js +6 -693
  9. package/dist/utils/resolve-device.js +15 -3
  10. package/docs/CHANGELOG.md +10 -1
  11. package/docs/tools/interact.md +69 -30
  12. package/package.json +3 -3
  13. package/skills/README.md +35 -0
  14. package/skills/test-authoring/SKILL.md +57 -0
  15. package/skills/test-authoring/references/repo-test-layout.md +47 -0
  16. package/skills/test-authoring/references/test-authoring-workflow.md +73 -0
  17. package/skills/test-authoring/references/test-quality-checklist.md +39 -0
  18. package/src/interact/classify.ts +64 -0
  19. package/src/interact/index.ts +250 -13
  20. package/src/network/index.ts +268 -0
  21. package/src/observe/ios.ts +12 -3
  22. package/src/server-core.ts +879 -0
  23. package/src/server.ts +8 -754
  24. package/src/types.ts +10 -1
  25. package/src/utils/resolve-device.ts +19 -3
  26. package/test/device/automated/observe/capture_screenshot.android.smoke.ts +30 -0
  27. package/test/device/automated/observe/capture_screenshot.ios.smoke.ts +30 -0
  28. package/test/{observe/device → device/automated/observe}/get_logs.android.smoke.ts +1 -1
  29. package/test/{observe/device → device/automated/observe}/get_logs.ios.smoke.ts +1 -1
  30. package/test/device/automated/observe/get_ui_tree.android.smoke.ts +31 -0
  31. package/test/device/automated/observe/get_ui_tree.ios.smoke.ts +31 -0
  32. package/test/device/index.ts +52 -0
  33. package/test/{interact/device/smoke-test.ts → device/manual/interact/app_lifecycle.manual.ts} +5 -5
  34. package/test/{manage/device/run-build-install-ios.ts → device/manual/manage/build_install_ios.manual.ts} +1 -1
  35. package/test/{manage/device → device/manual/manage}/install.integration.ts +6 -6
  36. package/test/{manage/device/run-install-android.ts → device/manual/manage/install_android.manual.ts} +1 -1
  37. package/test/{manage/device/run-install-ios.ts → device/manual/manage/install_ios.manual.ts} +1 -1
  38. package/test/device/manual/observe/capture_screenshot.manual.ts +29 -0
  39. package/test/{helpers/run-get-logs.ts → device/manual/observe/get_logs.manual.ts} +1 -1
  40. package/test/device/manual/observe/get_ui_tree.manual.ts +29 -0
  41. package/test/{observe/device/logstream-real.ts → device/manual/observe/logstream.manual.ts} +1 -1
  42. package/test/{observe/device/run-screen-fingerprint.ts → device/manual/observe/screen_fingerprint.manual.ts} +1 -1
  43. package/test/{observe/device/run-scroll-test-android.ts → device/manual/observe/scroll_to_element_android.manual.ts} +1 -1
  44. package/test/{observe/device/test-ui-tree.ts → device/manual/observe/ui_tree.manual.ts} +6 -6
  45. package/test/unit/index.ts +47 -27
  46. package/test/unit/interact/classify_action_outcome.test.ts +110 -0
  47. package/test/unit/interact/handler_shapes.test.ts +55 -0
  48. package/test/unit/interact/tap_element.test.ts +170 -0
  49. package/test/unit/interact/wait_for_screen_change.test.ts +34 -0
  50. package/test/{interact/unit → unit/interact}/wait_for_ui_contract.test.ts +11 -10
  51. package/test/unit/interact/wait_for_ui_selector_matching.test.ts +76 -0
  52. package/test/unit/manage/handler_shapes.test.ts +43 -0
  53. package/test/unit/network/get_network_activity.test.ts +181 -0
  54. package/test/{observe/unit → unit/observe}/capture_debug_snapshot.test.ts +5 -1
  55. package/test/{observe/unit → unit/observe}/find_element.test.ts +12 -6
  56. package/test/unit/observe/get_screen_fingerprint.test.ts +71 -0
  57. package/test/unit/observe/ios-getlogs.test.ts +53 -0
  58. package/test/unit/observe/scroll_to_element.test.ts +127 -0
  59. package/test/unit/server/contract.test.ts +45 -0
  60. package/test/unit/server/response_shapes.test.ts +93 -0
  61. package/test/unit/system/adb_version.test.ts +35 -0
  62. package/test/unit/system/get_system_status.test.ts +20 -0
  63. package/test/unit/system/system_status.test.ts +141 -0
  64. package/test/{utils → unit/utils}/detect_java.test.ts +1 -1
  65. package/test/unit/utils/exec.test.ts +51 -0
  66. package/test/unit/utils/resolve_device.test.ts +63 -0
  67. package/tsconfig.json +2 -2
  68. package/test/interact/device/run-real-test.ts +0 -3
  69. package/test/interact/unit/wait_for_screen_change.test.ts +0 -32
  70. package/test/interact/unit/wait_for_ui.test.ts +0 -76
  71. package/test/interact/unit/wait_for_ui_new.test.ts +0 -57
  72. package/test/observe/device/wait_for_element_real.ts +0 -3
  73. package/test/observe/unit/get_screen_fingerprint.test.ts +0 -69
  74. package/test/observe/unit/ios-getlogs.test.ts +0 -67
  75. package/test/observe/unit/scroll_to_element.test.ts +0 -129
  76. package/test/observe/unit/wait_for_element_mock.ts +0 -2
  77. package/test/observe/unit/wait_for_ui_edge_cases.test.ts +0 -41
  78. package/test/observe/unit/wait_for_ui_stability.test.ts +0 -30
  79. package/test/system/adb_version.test.ts +0 -25
  80. package/test/system/get_system_status.test.ts +0 -52
  81. package/test/system/system_status.test.ts +0 -109
  82. /package/test/{manage/unit → unit/manage}/build.test.ts +0 -0
  83. /package/test/{manage/unit → unit/manage}/build_and_install.test.ts +0 -0
  84. /package/test/{manage/unit → unit/manage}/detection.test.ts +0 -0
  85. /package/test/{manage/unit → unit/manage}/diagnostics.test.ts +0 -0
  86. /package/test/{manage/unit → unit/manage}/install.test.ts +0 -0
  87. /package/test/{manage/unit → unit/manage}/mcp_disable_autodetect.test.ts +0 -0
  88. /package/test/{observe/unit → unit/observe}/get_logs.test.ts +0 -0
  89. /package/test/{observe/unit → unit/observe}/logparse.test.ts +0 -0
  90. /package/test/{observe/unit → unit/observe}/logstream.test.ts +0 -0
@@ -0,0 +1,93 @@
1
+ import assert from 'assert'
2
+ import { handleToolCall } from '../../../src/server-core.js'
3
+ import { ToolsManage } from '../../../src/manage/index.js'
4
+ import { ToolsInteract } from '../../../src/interact/index.js'
5
+ import { ToolsObserve } from '../../../src/observe/index.js'
6
+
7
+ async function run() {
8
+ const originalInstallAppHandler = (ToolsManage as any).installAppHandler
9
+ const originalWaitForUIHandler = (ToolsInteract as any).waitForUIHandler
10
+ const originalTapElementHandler = (ToolsInteract as any).tapElementHandler
11
+ const originalCaptureScreenshotHandler = (ToolsObserve as any).captureScreenshotHandler
12
+ const originalGetUITreeHandler = (ToolsObserve as any).getUITreeHandler
13
+
14
+ try {
15
+ ;(ToolsManage as any).installAppHandler = async () => ({
16
+ device: { platform: 'android', id: 'emulator-5554', osVersion: '14', model: 'Pixel', simulator: true },
17
+ installed: true,
18
+ output: 'Success'
19
+ })
20
+
21
+ const installResponse = await handleToolCall('install_app', { platform: 'android', projectType: 'native', appPath: '/tmp/app.apk' })
22
+ assert.strictEqual((installResponse as any).content.length, 1)
23
+ const installPayload = JSON.parse((installResponse as any).content[0].text)
24
+ assert.strictEqual(installPayload.installed, true)
25
+ assert.strictEqual(installPayload.output, 'Success')
26
+ assert.strictEqual(installPayload.device.id, 'emulator-5554')
27
+
28
+ ;(ToolsInteract as any).waitForUIHandler = async () => ({
29
+ status: 'success',
30
+ matched: 1,
31
+ element: { text: 'Ready', bounds: [0, 0, 10, 10], index: 0, elementId: 'el_ready' },
32
+ metrics: { latency_ms: 12, poll_count: 1, attempts: 1 }
33
+ })
34
+
35
+ const waitForUIResponse = await handleToolCall('wait_for_ui', { selector: { text: 'Ready' } })
36
+ const waitForUIPayload = JSON.parse((waitForUIResponse as any).content[0].text)
37
+ assert.strictEqual(waitForUIPayload.status, 'success')
38
+ assert.strictEqual(waitForUIPayload.metrics.poll_count, 1)
39
+ assert.strictEqual(waitForUIPayload.element.text, 'Ready')
40
+ assert.strictEqual(waitForUIPayload.element.elementId, 'el_ready')
41
+
42
+ ;(ToolsInteract as any).tapElementHandler = async () => ({
43
+ success: true,
44
+ elementId: 'el_ready',
45
+ action: 'tap'
46
+ })
47
+
48
+ const tapElementResponse = await handleToolCall('tap_element', { elementId: 'el_ready' })
49
+ const tapElementPayload = JSON.parse((tapElementResponse as any).content[0].text)
50
+ assert.strictEqual(tapElementPayload.success, true)
51
+ assert.strictEqual(tapElementPayload.elementId, 'el_ready')
52
+ assert.strictEqual(tapElementPayload.action, 'tap')
53
+
54
+ ;(ToolsObserve as any).captureScreenshotHandler = async () => ({
55
+ device: { platform: 'ios', id: 'booted', osVersion: '18.0', model: 'Simulator', simulator: true },
56
+ screenshot: Buffer.from('png-data').toString('base64'),
57
+ screenshot_mime: 'image/png',
58
+ resolution: { width: 390, height: 844 }
59
+ })
60
+
61
+ const screenshotResponse = await handleToolCall('capture_screenshot', { platform: 'ios' })
62
+ assert.strictEqual((screenshotResponse as any).content.length, 2)
63
+ const screenshotMeta = JSON.parse((screenshotResponse as any).content[0].text)
64
+ assert.strictEqual((screenshotResponse as any).content[1].type, 'image')
65
+ assert.strictEqual((screenshotResponse as any).content[1].mimeType, 'image/png')
66
+ assert.strictEqual(screenshotMeta.result.resolution.width, 390)
67
+
68
+ ;(ToolsObserve as any).getUITreeHandler = async () => ({
69
+ device: { platform: 'android', id: 'mock', osVersion: '14', model: 'Pixel', simulator: true },
70
+ resolution: { width: 1080, height: 2400 },
71
+ elements: [{ text: 'Login', depth: 0, center: { x: 50, y: 20 } }]
72
+ })
73
+
74
+ const uiTreeResponse = await handleToolCall('get_ui_tree', { platform: 'android' })
75
+ const uiTreePayload = JSON.parse((uiTreeResponse as any).content[0].text)
76
+ assert.strictEqual(uiTreePayload.elements.length, 1)
77
+ assert.strictEqual(uiTreePayload.resolution.height, 2400)
78
+ assert.strictEqual(uiTreePayload.elements[0].text, 'Login')
79
+
80
+ console.log('server response-shape tests passed')
81
+ } finally {
82
+ ;(ToolsManage as any).installAppHandler = originalInstallAppHandler
83
+ ;(ToolsInteract as any).waitForUIHandler = originalWaitForUIHandler
84
+ ;(ToolsInteract as any).tapElementHandler = originalTapElementHandler
85
+ ;(ToolsObserve as any).captureScreenshotHandler = originalCaptureScreenshotHandler
86
+ ;(ToolsObserve as any).getUITreeHandler = originalGetUITreeHandler
87
+ }
88
+ }
89
+
90
+ run().catch((error) => {
91
+ console.error(error)
92
+ process.exit(1)
93
+ })
@@ -0,0 +1,35 @@
1
+ import assert from 'assert'
2
+ import fs from 'fs/promises'
3
+ import os from 'os'
4
+ import path from 'path'
5
+ import * as systemStatus from '../../../src/system/index.js'
6
+
7
+ async function run() {
8
+ const binDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-system-adb-'))
9
+ const adbPath = path.join(binDir, 'adb')
10
+ const originalAdbPath = process.env.ADB_PATH
11
+
12
+ try {
13
+ await fs.writeFile(adbPath, `#!/bin/sh
14
+ if [ "$1" = "--version" ]; then
15
+ printf 'Android Debug Bridge version 1.0.41\nRevision 8f3b7\n'
16
+ exit 0
17
+ fi
18
+ if [ "$1" = "devices" ]; then
19
+ printf 'List of devices attached\n'
20
+ exit 0
21
+ fi
22
+ exit 0
23
+ `, { mode: 0o755 })
24
+ process.env.ADB_PATH = adbPath
25
+ const res = await systemStatus.getSystemStatus()
26
+ assert.strictEqual(res.adbVersion, 'Android Debug Bridge version 1.0.41')
27
+ console.log('adb version parsing test passed')
28
+ } finally {
29
+ if (originalAdbPath === undefined) delete process.env.ADB_PATH
30
+ else process.env.ADB_PATH = originalAdbPath
31
+ await fs.rm(binDir, { recursive: true, force: true }).catch(() => {})
32
+ }
33
+ }
34
+
35
+ run().catch((error) => { console.error(error); process.exit(1) })
@@ -0,0 +1,20 @@
1
+ import assert from 'assert'
2
+ import { ensureAdbAvailable } from '../../../src/utils/android/utils.js'
3
+ import { getXcrunCmd } from '../../../src/utils/ios/utils.js'
4
+ import { getSystemStatus } from '../../../src/system/index.js'
5
+
6
+ async function run() {
7
+ const payload = await getSystemStatus()
8
+ assert(typeof payload.success === 'boolean')
9
+ assert(Array.isArray(payload.issues))
10
+
11
+ const adb = ensureAdbAvailable()
12
+ assert(adb && typeof adb.ok === 'boolean')
13
+
14
+ const cmd = getXcrunCmd()
15
+ assert(typeof cmd === 'string')
16
+
17
+ console.log('get_system_status unit tests passed')
18
+ }
19
+
20
+ run().catch((error) => { console.error(error); process.exit(1) })
@@ -0,0 +1,141 @@
1
+ import assert from 'assert'
2
+ import fs from 'fs/promises'
3
+ import os from 'os'
4
+ import path from 'path'
5
+
6
+ import * as systemStatus from '../../../src/system/index.js'
7
+
8
+ async function writeFakeCommands(binDir: string) {
9
+ const adbPath = path.join(binDir, 'adb')
10
+ const xcrunPath = path.join(binDir, 'xcrun')
11
+
12
+ await fs.writeFile(adbPath, `#!/bin/sh
13
+ if [ "$1" = "--version" ]; then
14
+ if [ "\${ADB_VERSION_STATUS:-0}" != "0" ]; then
15
+ printf '%s' "\${ADB_VERSION_OUTPUT:-not found}" >&2
16
+ exit "\${ADB_VERSION_STATUS}"
17
+ fi
18
+ printf '%s' "\${ADB_VERSION_OUTPUT:-Android Debug Bridge version 8.1.0}"
19
+ exit 0
20
+ fi
21
+ if [ "$1" = "devices" ]; then
22
+ printf '%s' "\${ADB_DEVICES_OUTPUT:-List of devices attached}"
23
+ exit 0
24
+ fi
25
+ if [ "$1" = "logcat" ]; then
26
+ printf '%s' "\${ADB_LOGCAT_OUTPUT:-I/Tag: ok}"
27
+ exit 0
28
+ fi
29
+ if [ "$1" = "shell" ] && [ "$2" = "pm" ] && [ "$3" = "path" ]; then
30
+ printf '%s' "\${ADB_PM_PATH_OUTPUT:-package:/data/app/com.example/base.apk}"
31
+ exit 0
32
+ fi
33
+ printf '%s' "\${ADB_DEFAULT_OUTPUT:-OK}"
34
+ exit 0
35
+ `, { mode: 0o755 })
36
+
37
+ await fs.writeFile(xcrunPath, `#!/bin/sh
38
+ if [ "$1" = "--version" ]; then
39
+ if [ "\${XCRUN_VERSION_STATUS:-0}" != "0" ]; then
40
+ printf '%s' "\${XCRUN_VERSION_OUTPUT:-not found}" >&2
41
+ exit "\${XCRUN_VERSION_STATUS}"
42
+ fi
43
+ printf '%s' "\${XCRUN_VERSION_OUTPUT:-xcrun version 123}"
44
+ exit 0
45
+ fi
46
+ if [ "$1" = "simctl" ] && [ "$2" = "list" ] && [ "$3" = "devices" ] && [ "$4" = "booted" ] && [ "$5" = "--json" ]; then
47
+ printf '%s' "\${SIMCTL_LIST_OUTPUT:-{\"devices\":{}}}"
48
+ exit 0
49
+ fi
50
+ printf '%s' "\${XCRUN_DEFAULT_OUTPUT:-ok}"
51
+ exit 0
52
+ `, { mode: 0o755 })
53
+
54
+ return { adbPath, xcrunPath }
55
+ }
56
+
57
+ function setScenario(env: Record<string, string | undefined>) {
58
+ for (const [key, value] of Object.entries(env)) {
59
+ if (value === undefined) delete process.env[key]
60
+ else process.env[key] = value
61
+ }
62
+ }
63
+
64
+ async function run() {
65
+ const binDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-system-status-'))
66
+ const originalEnv = {
67
+ ADB_PATH: process.env.ADB_PATH,
68
+ XCRUN_PATH: process.env.XCRUN_PATH,
69
+ ADB_VERSION_OUTPUT: process.env.ADB_VERSION_OUTPUT,
70
+ ADB_VERSION_STATUS: process.env.ADB_VERSION_STATUS,
71
+ ADB_DEVICES_OUTPUT: process.env.ADB_DEVICES_OUTPUT,
72
+ ADB_LOGCAT_OUTPUT: process.env.ADB_LOGCAT_OUTPUT,
73
+ ADB_PM_PATH_OUTPUT: process.env.ADB_PM_PATH_OUTPUT,
74
+ XCRUN_VERSION_OUTPUT: process.env.XCRUN_VERSION_OUTPUT,
75
+ XCRUN_VERSION_STATUS: process.env.XCRUN_VERSION_STATUS,
76
+ SIMCTL_LIST_OUTPUT: process.env.SIMCTL_LIST_OUTPUT,
77
+ }
78
+
79
+ try {
80
+ const { adbPath, xcrunPath } = await writeFakeCommands(binDir)
81
+ process.env.ADB_PATH = adbPath
82
+ process.env.XCRUN_PATH = xcrunPath
83
+
84
+ setScenario({
85
+ ADB_VERSION_STATUS: '0',
86
+ ADB_VERSION_OUTPUT: '8.1.0\n',
87
+ ADB_DEVICES_OUTPUT: 'List of devices attached\nemulator-5554\tdevice product:sdk\n',
88
+ ADB_LOGCAT_OUTPUT: 'I/Tag: ok\n',
89
+ XCRUN_VERSION_STATUS: '0',
90
+ XCRUN_VERSION_OUTPUT: 'xcrun version 123\n',
91
+ SIMCTL_LIST_OUTPUT: JSON.stringify({ devices: { runtime: [{ state: 'Booted' }] } }),
92
+ })
93
+
94
+ const healthy = await systemStatus.getSystemStatus()
95
+ assert.strictEqual(healthy.success, true)
96
+ assert.strictEqual(healthy.adbAvailable, true)
97
+ assert.strictEqual(typeof healthy.adbVersion, 'string')
98
+
99
+ setScenario({
100
+ ADB_VERSION_STATUS: '1',
101
+ ADB_VERSION_OUTPUT: 'not found',
102
+ ADB_DEVICES_OUTPUT: 'List of devices attached\n',
103
+ XCRUN_VERSION_STATUS: '0',
104
+ XCRUN_VERSION_OUTPUT: 'xcrun version\n',
105
+ })
106
+ const missingAdb = await systemStatus.getSystemStatus()
107
+ assert.strictEqual(missingAdb.success, false)
108
+ assert(missingAdb.issues.some((issue: string) => issue.includes('ADB')))
109
+
110
+ setScenario({
111
+ ADB_VERSION_STATUS: '0',
112
+ ADB_VERSION_OUTPUT: '8.1.0\n',
113
+ ADB_DEVICES_OUTPUT: 'List of devices attached\nserial1\tunauthorized\nserial2\toffline\n',
114
+ XCRUN_VERSION_STATUS: '0',
115
+ XCRUN_VERSION_OUTPUT: 'xcrun version\n',
116
+ })
117
+ const unauthorized = await systemStatus.getSystemStatus()
118
+ assert.strictEqual(unauthorized.success, false)
119
+ assert(unauthorized.issues.some((issue: string) => issue.includes('unauthorized')))
120
+ assert(unauthorized.issues.some((issue: string) => issue.includes('offline')))
121
+
122
+ setScenario({
123
+ ADB_VERSION_STATUS: '0',
124
+ ADB_VERSION_OUTPUT: '8.1.0\n',
125
+ ADB_DEVICES_OUTPUT: 'List of devices attached\nemulator-5554\tdevice product:sdk\n',
126
+ XCRUN_VERSION_STATUS: '1',
127
+ XCRUN_VERSION_OUTPUT: 'not found',
128
+ })
129
+ const missingXcrun = await systemStatus.getSystemStatus()
130
+ assert.strictEqual(missingXcrun.iosAvailable, false)
131
+ assert.strictEqual(missingXcrun.adbAvailable, true)
132
+ assert.strictEqual(Array.isArray(missingXcrun.issues), true)
133
+
134
+ console.log('system_status checks passed')
135
+ } finally {
136
+ setScenario(originalEnv)
137
+ await fs.rm(binDir, { recursive: true, force: true }).catch(() => {})
138
+ }
139
+ }
140
+
141
+ run().catch((error) => { console.error(error); process.exit(1) })
@@ -1,6 +1,6 @@
1
1
  import fs from 'fs'
2
2
  import path from 'path'
3
- import { detectJavaHome } from '../../src/utils/java.js'
3
+ import { detectJavaHome } from '../../../src/utils/java.js'
4
4
 
5
5
  async function run() {
6
6
  // Create a temporary fake JDK that reports Java 17
@@ -0,0 +1,51 @@
1
+ import assert from 'assert'
2
+ import fs from 'fs/promises'
3
+ import os from 'os'
4
+ import path from 'path'
5
+ import { execAdbWithDiagnostics } from '../../../src/utils/diagnostics.js'
6
+ import { execCmd } from '../../../src/utils/exec.js'
7
+
8
+ async function run() {
9
+ const envResult = await execCmd(process.execPath, ['-e', 'console.log(process.env.MCP_EXEC_TEST); console.error("warn");'], {
10
+ env: { MCP_EXEC_TEST: 'hello' }
11
+ })
12
+ assert.strictEqual(envResult.exitCode, 0)
13
+ assert.strictEqual(envResult.stdout, 'hello')
14
+ assert.strictEqual(envResult.stderr, 'warn')
15
+
16
+ const timeoutResult = await execCmd(process.execPath, ['-e', 'setTimeout(() => console.log("late"), 200)'], {
17
+ timeout: 50
18
+ })
19
+ assert.strictEqual(timeoutResult.exitCode, null)
20
+ assert.strictEqual(timeoutResult.stdout, '')
21
+
22
+ const binDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-exec-adb-'))
23
+ const adbPath = path.join(binDir, 'adb')
24
+ const script = `#!/bin/sh
25
+ echo "device not found" >&2
26
+ exit 1
27
+ `
28
+ const originalAdbPath = process.env.ADB_PATH
29
+
30
+ try {
31
+ await fs.writeFile(adbPath, script, { mode: 0o755 })
32
+ process.env.ADB_PATH = adbPath
33
+
34
+ const adbResult = execAdbWithDiagnostics(['shell', 'getprop'], 'emulator-5554')
35
+ assert.strictEqual(adbResult.runResult.command, adbPath)
36
+ assert.deepStrictEqual(adbResult.runResult.args.slice(0, 2), ['-s', 'emulator-5554'])
37
+ assert.match(adbResult.runResult.stderr, /device not found/)
38
+ assert(adbResult.runResult.suggestedFixes?.some((fix) => fix.includes('adb devices')))
39
+
40
+ console.log('exec utility tests passed')
41
+ } finally {
42
+ if (typeof originalAdbPath === 'undefined') delete process.env.ADB_PATH
43
+ else process.env.ADB_PATH = originalAdbPath
44
+ await fs.rm(binDir, { recursive: true, force: true }).catch(() => {})
45
+ }
46
+ }
47
+
48
+ run().catch((error) => {
49
+ console.error(error)
50
+ process.exit(1)
51
+ })
@@ -0,0 +1,63 @@
1
+ import assert from 'assert'
2
+ import {
3
+ listDevices,
4
+ resolveTargetDevice,
5
+ _setDeviceListersForTests,
6
+ _resetDeviceListersForTests
7
+ } from '../../../src/utils/resolve-device.js'
8
+
9
+ async function run() {
10
+ try {
11
+ _setDeviceListersForTests({
12
+ listAndroidDevices: async () => ([
13
+ { id: 'android-1', platform: 'android', osVersion: '14', model: 'Pixel 8', simulator: true },
14
+ { id: 'android-2', platform: 'android', osVersion: '13', model: 'Pixel 7', simulator: false, appInstalled: true } as any
15
+ ]),
16
+ listIOSDevices: async () => ([
17
+ { id: 'ios-1', platform: 'ios', osVersion: '18.0', model: 'iPhone 15', simulator: true, booted: false } as any,
18
+ { id: 'ios-2', platform: 'ios', osVersion: '17.4', model: 'iPhone 14', simulator: true, booted: true } as any
19
+ ])
20
+ })
21
+
22
+ const combined = await listDevices()
23
+ assert.strictEqual(combined.length, 4)
24
+
25
+ const explicit = await resolveTargetDevice({ platform: 'android', deviceId: 'android-1' })
26
+ assert.strictEqual(explicit.id, 'android-1')
27
+
28
+ const preferredPhysical = await resolveTargetDevice({ platform: 'android', prefer: 'physical' })
29
+ assert.strictEqual(preferredPhysical.id, 'android-2')
30
+
31
+ const installedForApp = await resolveTargetDevice({ platform: 'android', appId: 'com.example.app' })
32
+ assert.strictEqual(installedForApp.id, 'android-2')
33
+
34
+ const bootedIOS = await resolveTargetDevice({ platform: 'ios' })
35
+ assert.strictEqual(bootedIOS.id, 'ios-2')
36
+
37
+ _setDeviceListersForTests({
38
+ listAndroidDevices: async () => ([
39
+ { id: 'android-a', platform: 'android', osVersion: '14.0', model: 'Pixel 8', simulator: false },
40
+ { id: 'android-b', platform: 'android', osVersion: '14.0', model: 'Pixel 8 Pro', simulator: false }
41
+ ])
42
+ })
43
+
44
+ await assert.rejects(
45
+ () => resolveTargetDevice({ platform: 'android' }),
46
+ (error: unknown) => {
47
+ assert(error instanceof Error)
48
+ assert.match(error.message, /Multiple matching devices found/)
49
+ assert.strictEqual(Array.isArray((error as any).devices), true)
50
+ return true
51
+ }
52
+ )
53
+
54
+ console.log('resolve-device tests passed')
55
+ } finally {
56
+ _resetDeviceListersForTests()
57
+ }
58
+ }
59
+
60
+ run().catch((error) => {
61
+ console.error(error)
62
+ process.exit(1)
63
+ })
package/tsconfig.json CHANGED
@@ -9,6 +9,6 @@
9
9
  "skipLibCheck": true,
10
10
  "esModuleInterop": true
11
11
  },
12
- "exclude": ["smoke-test.ts", "test-ui-tree.ts", "test/**/*.ts"],
12
+ "exclude": ["test/**/*.ts"],
13
13
  "include": ["src/**/*"]
14
- }
14
+ }
@@ -1,3 +0,0 @@
1
- // wait_for_element device runner removed
2
- console.log('wait_for_element device runner removed');
3
- process.exit(0);
@@ -1,32 +0,0 @@
1
- import { ToolsInteract } from '../../../src/interact/index.js'
2
- import * as Observe from '../../../src/observe/index.js'
3
-
4
- const original = (Observe as any).ToolsObserve.getScreenFingerprintHandler
5
-
6
- async function runTests() {
7
- console.log('Starting tests for wait_for_screen_change...')
8
-
9
- // Test 1: Immediate change
10
- let seq1: Array<string | null> = ['B','B']
11
- ;(Observe as any).ToolsObserve.getScreenFingerprintHandler = async () => ({ fingerprint: seq1.length ? seq1.shift() : null })
12
- const start1 = Date.now()
13
- const res1 = await ToolsInteract.waitForScreenChangeHandler({ platform: 'android', previousFingerprint: 'A', timeoutMs: 2000, pollIntervalMs: 50 })
14
- const elapsed1 = Date.now() - start1
15
- console.log('Test 1: Immediate change ->', (res1 && (res1 as any).success === true && (res1 as any).newFingerprint === 'B') ? 'PASS' : 'FAIL', 'Elapsed:', elapsed1, 'ms')
16
-
17
- // Test 2: Transient nulls then stable change
18
- let seq2: Array<string | null> = [null, null, 'B', 'B']
19
- ;(Observe as any).ToolsObserve.getScreenFingerprintHandler = async () => ({ fingerprint: seq2.length ? seq2.shift() : 'B' })
20
- const res2 = await ToolsInteract.waitForScreenChangeHandler({ platform: 'android', previousFingerprint: 'A', timeoutMs: 3000, pollIntervalMs: 50 })
21
- console.log('Test 2: Transient nulls ->', (res2 && (res2 as any).success === true && (res2 as any).newFingerprint === 'B') ? 'PASS' : 'FAIL')
22
-
23
- // Test 3: Timeout
24
- ;(Observe as any).ToolsObserve.getScreenFingerprintHandler = async () => ({ fingerprint: 'A' })
25
- const res3 = await ToolsInteract.waitForScreenChangeHandler({ platform: 'android', previousFingerprint: 'A', timeoutMs: 300, pollIntervalMs: 50 })
26
- console.log('Test 3: Timeout ->', (res3 && (res3 as any).success === false && (res3 as any).reason === 'timeout') ? 'PASS' : 'FAIL')
27
-
28
- // Restore original
29
- ;(Observe as any).ToolsObserve.getScreenFingerprintHandler = original
30
- }
31
-
32
- runTests().catch(console.error)
@@ -1,76 +0,0 @@
1
- import { ToolsInteract } from '../../../src/interact/index.js'
2
- import * as Observe from '../../../src/observe/index.js'
3
-
4
- async function runTests() {
5
- console.log('Starting wait_for_ui unit tests...')
6
-
7
- const origFind = (ToolsInteract as any).findElementHandler
8
- const origReadLog = (Observe as any).ToolsObserve.readLogStreamHandler
9
- const origGetLogs = (Observe as any).ToolsObserve.getLogsHandler
10
- const origGetFp = (Observe as any).ToolsObserve.getScreenFingerprintHandler
11
- const origResolveObserve = (Observe as any).ToolsObserve.resolveObserve
12
- const origGetScreenFp = (Observe as any).ToolsObserve.getScreenFingerprintHandler
13
-
14
- try {
15
- // Timeout / snapshot case: ensure snapshot captured when condition not met
16
- const origCapture = (Observe as any).ToolsObserve.captureDebugSnapshotHandler
17
- ;(Observe as any).ToolsObserve.captureDebugSnapshotHandler = async ({ reason }: any) => ({ reason, fingerprint: 'snap-123', ui_tree: null, logs: [] })
18
- // make findElement always fail
19
- (ToolsInteract as any).findElementHandler = async () => ({ found: false })
20
- const resTimeout = await ToolsInteract.waitForUIHandler({ type: 'ui', query: 'WillNeverExist', timeoutMs: 500, pollIntervalMs: 100, platform: 'android' })
21
- const okTimeout = resTimeout && !(resTimeout as any).success && (resTimeout as any).snapshot && (resTimeout as any).snapshot.fingerprint === 'snap-123' && (resTimeout as any).telemetry && (resTimeout as any).telemetry.pollCount > 0
22
- console.log('Timeout Snapshot Test:', okTimeout ? 'PASS' : 'FAIL', JSON.stringify((resTimeout as any).telemetry || {}, null, 2))
23
- ;(Observe as any).ToolsObserve.captureDebugSnapshotHandler = origCapture
24
-
25
- // UI condition: findElement returns found on 2nd call
26
- let calls = 0
27
- ;(ToolsInteract as any).findElementHandler = async (args) => {
28
- calls++
29
- const query = (args && (args.query || args)) || ''
30
- if (calls >= 2) return { found: true, element: { text: query } }
31
- return { found: false }
32
- }
33
-
34
- const resUi = await ToolsInteract.waitForUIHandler({ type: 'ui', query: 'Generate Session', timeoutMs: 3000, pollIntervalMs: 100, platform: 'android' })
35
- const okUi = resUi && (resUi as any).success && (resUi as any).telemetry && (resUi as any).telemetry.pollCount > 0 && (resUi as any).telemetry.timeToMatch >= 0
36
- console.log('UI Test:', okUi ? 'PASS' : 'FAIL', JSON.stringify((resUi as any).telemetry || {}, null, 2))
37
-
38
- // Log condition: stream empty, snapshot contains matching line
39
- ;(Observe as any).ToolsObserve.readLogStreamHandler = async () => ({ entries: [ { message: 'nothing' } ] })
40
- let glCalls = 0
41
- ;(Observe as any).ToolsObserve.getLogsHandler = async () => {
42
- glCalls++
43
- if (glCalls === 1) return { device: {}, logs: ['INFO start'] }
44
- return { device: {}, logs: ['INFO start', 'ERROR Exception occurred', 'Server: Boom'] }
45
- }
46
-
47
- const resLog = await ToolsInteract.waitForUIHandler({ type: 'log', query: 'Server', timeoutMs: 3000, pollIntervalMs: 100, platform: 'android' })
48
- const okLog = resLog && (resLog as any).success && (resLog as any).telemetry && (resLog as any).telemetry.pollCount > 0 && (resLog as any).telemetry.matchSource === 'log-snapshot'
49
- console.log('Log Test:', okLog ? 'PASS' : 'FAIL', JSON.stringify((resLog as any).telemetry || {}, null, 2))
50
-
51
- // Screen condition: fingerprint changes after a few polls
52
- let seq = ['A', 'A', 'B']
53
- ;(Observe as any).ToolsObserve.getScreenFingerprintHandler = async () => ({ fingerprint: seq.length ? seq.shift() : null })
54
- const resScreen = await ToolsInteract.waitForUIHandler({ type: 'screen', timeoutMs: 3000, pollIntervalMs: 100, platform: 'android' })
55
- const okScreen = resScreen && (resScreen as any).success && (resScreen as any).telemetry && (resScreen as any).telemetry.matchSource === 'screen-fingerprint'
56
- console.log('Screen Test:', okScreen ? 'PASS' : 'FAIL', JSON.stringify((resScreen as any).telemetry || {}, null, 2))
57
-
58
- // Idle condition: stable fingerprints observed
59
- let idleSeq = ['X', 'X', 'X']
60
- ;(Observe as any).ToolsObserve.getScreenFingerprintHandler = async () => ({ fingerprint: idleSeq.length ? idleSeq.shift() : 'X' })
61
- const resIdle = await ToolsInteract.waitForUIHandler({ type: 'idle', timeoutMs: 3000, pollIntervalMs: 100, platform: 'android' })
62
- const okIdle = resIdle && (resIdle as any).success && (resIdle as any).telemetry && (resIdle as any).telemetry.matchSource === 'idle-stable'
63
- console.log('Idle Test:', okIdle ? 'PASS' : 'FAIL', JSON.stringify((resIdle as any).telemetry || {}, null, 2))
64
-
65
- } finally {
66
- ;(ToolsInteract as any).findElementHandler = origFind
67
- ;(Observe as any).ToolsObserve.readLogStreamHandler = origReadLog
68
- ;(Observe as any).ToolsObserve.getLogsHandler = origGetLogs
69
- ;(Observe as any).ToolsObserve.getScreenFingerprintHandler = origGetFp
70
- ;(Observe as any).ToolsObserve.resolveObserve = origResolveObserve
71
- ;(Observe as any).ToolsObserve.getScreenFingerprintHandler = origGetScreenFp
72
- ;(Observe as any).ToolsObserve.getScreenFingerprintHandler = origGetScreenFp
73
- }
74
- }
75
-
76
- runTests().catch(console.error)
@@ -1,57 +0,0 @@
1
- import { ToolsInteract } from '../../../src/interact/index.js'
2
- import * as Observe from '../../../src/observe/index.js'
3
-
4
- async function run() {
5
- console.log('Starting new wait_for_ui unit tests...')
6
- const origGetUITree = (Observe as any).ToolsObserve.getUITreeHandler
7
-
8
- try {
9
- // Test 1: exact text match -> exists
10
- (Observe as any).ToolsObserve.getUITreeHandler = async () => ({ elements: [ { text: 'Hello', resourceId: 'rid1', contentDescription: 'acc1', type: 'Button', bounds: [0,0,10,10], visible: true, clickable: false, enabled: true } ] })
11
- const r1 = await ToolsInteract.waitForUIHandler({ selector: { text: 'Hello' }, condition: 'exists', timeout_ms: 1000, poll_interval_ms: 50, platform: 'android' })
12
- const ok1 = r1 && r1.status === 'success' && r1.matched === 1 && r1.element && r1.element.text === 'Hello'
13
- console.log('Exact match exists:', ok1 ? 'PASS' : 'FAIL', JSON.stringify(r1, null, 2))
14
-
15
- // Test 2: contains matching
16
- (Observe as any).ToolsObserve.getUITreeHandler = async () => ({ elements: [ { text: 'Welcome User', resourceId: 'rid2', contentDescription: 'acc2', type: 'TextView', bounds: [0,0,50,10], visible: true } ] })
17
- const r2 = await ToolsInteract.waitForUIHandler({ selector: { text: 'User', contains: true }, condition: 'exists', timeout_ms: 1000, poll_interval_ms: 50, platform: 'android' })
18
- const ok2 = r2 && r2.status === 'success' && r2.matched === 1 && r2.element && r2.element.text && r2.element.text.includes('Welcome')
19
- console.log('Contains match:', ok2 ? 'PASS' : 'FAIL', JSON.stringify(r2, null, 2))
20
-
21
- // Test 3: visible condition
22
- (Observe as any).ToolsObserve.getUITreeHandler = async () => ({ elements: [ { text: 'Hidden', resourceId: 'rid3', bounds: [0,0,0,0], visible: false } ] })
23
- const r3 = await ToolsInteract.waitForUIHandler({ selector: { text: 'Hidden' }, condition: 'visible', timeout_ms: 300, poll_interval_ms: 50, platform: 'android' })
24
- const ok3 = r3 && r3.status === 'timeout' && r3.error && r3.error.code === 'ELEMENT_NOT_FOUND'
25
- console.log('Visible negative (hidden element):', ok3 ? 'PASS' : 'FAIL', JSON.stringify(r3, null, 2))
26
-
27
- // Test 4: clickable condition
28
- (Observe as any).ToolsObserve.getUITreeHandler = async () => ({ elements: [ { text: 'TapMe', resourceId: 'rid4', bounds: [0,0,20,20], visible: true, clickable: true, enabled: true } ] })
29
- const r4 = await ToolsInteract.waitForUIHandler({ selector: { text: 'TapMe' }, condition: 'clickable', timeout_ms: 1000, poll_interval_ms: 50, platform: 'android' })
30
- const ok4 = r4 && r4.status === 'success' && r4.matched === 1 && r4.element && r4.element.index === 0
31
- console.log('Clickable match:', ok4 ? 'PASS' : 'FAIL', JSON.stringify(r4, null, 2))
32
-
33
- // Test 5: retry behavior - first attempt times out, second attempt succeeds
34
- const start = Date.now()
35
- let seqTree = async () => {
36
- const now = Date.now()
37
- // for first ~400ms return no elements, afterwards return match
38
- if (now - start < 400) return { elements: [] }
39
- return { elements: [ { text: 'Retried', resourceId: 'rid5', bounds: [0,0,10,10], visible: true } ] }
40
- }
41
- ;(Observe as any).ToolsObserve.getUITreeHandler = seqTree
42
- const r5 = await ToolsInteract.waitForUIHandler({ selector: { text: 'Retried' }, condition: 'exists', timeout_ms: 200, poll_interval_ms: 50, match: undefined, retry: { max_attempts: 3, backoff_ms: 150 }, platform: 'android' })
43
- const ok5 = r5 && r5.status === 'success' && r5.metrics && r5.metrics.attempts >= 2
44
- console.log('Retry behavior:', ok5 ? 'PASS' : 'FAIL', JSON.stringify(r5, null, 2))
45
-
46
- // Test 6: timeout with no selector match -> correct error code
47
- (Observe as any).ToolsObserve.getUITreeHandler = async () => ({ elements: [] })
48
- const r6 = await ToolsInteract.waitForUIHandler({ selector: { text: 'Nope' }, condition: 'exists', timeout_ms: 300, poll_interval_ms: 50, retry: { max_attempts: 1 }, platform: 'android' })
49
- const ok6 = r6 && r6.status === 'timeout' && r6.error && r6.error.code === 'ELEMENT_NOT_FOUND'
50
- console.log('Timeout no match:', ok6 ? 'PASS' : 'FAIL', JSON.stringify(r6, null, 2))
51
-
52
- } finally {
53
- (Observe as any).ToolsObserve.getUITreeHandler = origGetUITree
54
- }
55
- }
56
-
57
- run().catch(err => { console.error('wait_for_ui_new tests failed:', err); process.exit(1) })
@@ -1,3 +0,0 @@
1
- // wait_for_element device runner removed
2
- console.log('wait_for_element device test removed');
3
- process.exit(0);