mobile-debug-mcp 0.13.0 → 0.15.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 (103) hide show
  1. package/README.md +2 -2
  2. package/dist/android/interact.js +13 -1
  3. package/dist/android/observe.js +13 -0
  4. package/dist/cli/ios/run-ios-smoke.js +2 -2
  5. package/dist/cli/ios/run-ios-ui-tree-tap.js +2 -2
  6. package/dist/interact/android.js +91 -0
  7. package/dist/interact/index.js +37 -0
  8. package/dist/interact/ios.js +120 -0
  9. package/dist/interact/shared/fingerprint.js +72 -0
  10. package/dist/interact/shared/scroll_to_element.js +98 -0
  11. package/dist/ios/interact.js +52 -1
  12. package/dist/ios/observe.js +12 -0
  13. package/dist/manage/android.js +162 -0
  14. package/dist/manage/index.js +364 -0
  15. package/dist/manage/ios.js +353 -0
  16. package/dist/observe/android.js +351 -0
  17. package/dist/observe/fingerprint.js +1 -0
  18. package/dist/observe/index.js +85 -0
  19. package/dist/observe/ios.js +320 -0
  20. package/dist/observe/test/device/logstream-real.js +34 -0
  21. package/dist/observe/test/device/run-screen-fingerprint.js +29 -0
  22. package/dist/observe/test/device/run-scroll-test-android.js +22 -0
  23. package/dist/observe/test/device/test-ui-tree.js +67 -0
  24. package/dist/observe/test/device/wait_for_element_real.js +69 -0
  25. package/dist/observe/test/unit/get_screen_fingerprint.test.js +54 -0
  26. package/dist/observe/test/unit/logparse.test.js +39 -0
  27. package/dist/observe/test/unit/logstream.test.js +41 -0
  28. package/dist/observe/test/unit/scroll_to_element.test.js +113 -0
  29. package/dist/observe/test/unit/wait_for_element_mock.js +92 -0
  30. package/dist/server.js +54 -9
  31. package/dist/shared/fingerprint.js +72 -0
  32. package/dist/shared/scroll_to_element.js +98 -0
  33. package/dist/tools/interact.js +19 -22
  34. package/dist/tools/manage.js +2 -2
  35. package/dist/tools/observe.js +45 -43
  36. package/dist/tools/scroll_to_element.js +98 -0
  37. package/dist/utils/android/utils.js +429 -0
  38. package/dist/utils/cli/idb/check-idb.js +84 -0
  39. package/dist/utils/cli/idb/idb-helper.js +91 -0
  40. package/dist/utils/cli/idb/install-idb.js +82 -0
  41. package/dist/utils/cli/ios/preflight-ios.js +155 -0
  42. package/dist/utils/cli/ios/run-ios-smoke.js +28 -0
  43. package/dist/utils/cli/ios/run-ios-ui-tree-tap.js +29 -0
  44. package/dist/utils/diagnostics.js +1 -1
  45. package/dist/utils/ios/utils.js +301 -0
  46. package/dist/utils/resolve-device.js +2 -2
  47. package/docs/CHANGELOG.md +11 -0
  48. package/docs/tools/TOOLS.md +3 -3
  49. package/docs/tools/interact.md +31 -0
  50. package/docs/tools/observe.md +24 -0
  51. package/package.json +1 -1
  52. package/src/{android/interact.ts → interact/android.ts} +15 -2
  53. package/src/interact/index.ts +47 -0
  54. package/src/{ios/interact.ts → interact/ios.ts} +58 -3
  55. package/src/interact/shared/fingerprint.ts +73 -0
  56. package/src/interact/shared/scroll_to_element.ts +110 -0
  57. package/src/{android/manage.ts → manage/android.ts} +2 -2
  58. package/src/{tools/manage.ts → manage/index.ts} +7 -4
  59. package/src/{ios/manage.ts → manage/ios.ts} +1 -1
  60. package/src/{android/observe.ts → observe/android.ts} +14 -26
  61. package/src/observe/index.ts +92 -0
  62. package/src/{ios/observe.ts → observe/ios.ts} +17 -35
  63. package/src/server.ts +57 -10
  64. package/src/{android → utils/android}/utils.ts +2 -2
  65. package/src/{cli → utils/cli}/ios/run-ios-smoke.ts +2 -2
  66. package/src/{cli → utils/cli}/ios/run-ios-ui-tree-tap.ts +3 -3
  67. package/src/utils/diagnostics.ts +1 -1
  68. package/src/{ios → utils/ios}/utils.ts +2 -2
  69. package/src/utils/resolve-device.ts +2 -2
  70. package/test/{device/interact → interact/device}/smoke-test.ts +3 -4
  71. package/test/{device/manage → manage/device}/run-install-android.ts +1 -1
  72. package/test/{device/manage → manage/device}/run-install-ios.ts +1 -1
  73. package/test/{device/manage → manage/device}/run-install-kmp.ts +1 -1
  74. package/test/{unit/manage → manage/unit}/build.test.ts +1 -1
  75. package/test/{unit/manage → manage/unit}/build_and_install.test.ts +1 -1
  76. package/test/{unit/manage → manage/unit}/detection.test.ts +1 -1
  77. package/test/{unit/manage → manage/unit}/diagnostics.test.ts +2 -2
  78. package/test/{unit/manage → manage/unit}/install.test.ts +1 -1
  79. package/test/{unit/manage → manage/unit}/mcp_disable_autodetect.test.ts +1 -1
  80. package/test/{device/observe → observe/device}/logstream-real.ts +1 -1
  81. package/test/observe/device/run-screen-fingerprint.ts +36 -0
  82. package/test/observe/device/run-scroll-test-android.ts +24 -0
  83. package/test/{device/observe → observe/device}/test-ui-tree.ts +2 -2
  84. package/test/{device/observe → observe/device}/wait_for_element_real.ts +2 -2
  85. package/test/observe/unit/get_screen_fingerprint.test.ts +69 -0
  86. package/test/{unit/observe → observe/unit}/logparse.test.ts +1 -1
  87. package/test/{unit/observe → observe/unit}/logstream.test.ts +1 -1
  88. package/test/observe/unit/scroll_to_element.test.ts +129 -0
  89. package/test/{unit/observe → observe/unit}/wait_for_element_mock.ts +3 -3
  90. package/test/unit/index.ts +12 -11
  91. package/src/tools/interact.ts +0 -45
  92. package/src/tools/observe.ts +0 -82
  93. package/test/device/README.md +0 -49
  94. package/test/device/index.ts +0 -27
  95. package/test/device/utils/test-dist.ts +0 -41
  96. package/test/unit/utils/detect-java.test.ts +0 -22
  97. /package/src/{cli → utils/cli}/idb/check-idb.ts +0 -0
  98. /package/src/{cli → utils/cli}/idb/idb-helper.ts +0 -0
  99. /package/src/{cli → utils/cli}/idb/install-idb.ts +0 -0
  100. /package/src/{cli → utils/cli}/ios/preflight-ios.ts +0 -0
  101. /package/test/{device/interact → interact/device}/run-real-test.ts +0 -0
  102. /package/test/{device/manage → manage/device}/install.integration.ts +0 -0
  103. /package/test/{device/manage → manage/device}/run-build-install-ios.ts +0 -0
@@ -1,8 +1,8 @@
1
1
  import { spawn } from 'child_process'
2
- import { DeviceInfo, UIElement } from "../types.js"
2
+ import { DeviceInfo, UIElement } from "../../types.js"
3
3
  import { promises as fsPromises, existsSync } from 'fs'
4
4
  import path from 'path'
5
- import { detectJavaHome } from '../utils/java.js'
5
+ import { detectJavaHome } from '../java.js'
6
6
 
7
7
  export function getAdbCmd() { return process.env.ADB_PATH || 'adb' }
8
8
 
@@ -1,5 +1,5 @@
1
- import { iOSObserve } from '../../ios/observe.js';
2
- import { iOSManage } from '../../ios/manage.js';
1
+ import { iOSObserve } from '../../../observe/index.js';
2
+ import { iOSManage } from '../../../manage/index.js';
3
3
 
4
4
  async function main() {
5
5
  const appId = process.argv[2] || 'com.apple.springboard';
@@ -1,5 +1,5 @@
1
- import { iOSObserve } from '../../ios/observe.js';
2
- import { iOSInteract } from '../../ios/interact.js';
1
+ import { iOSObserve } from '../../../observe/index.js';
2
+ import { iOSInteract } from '../../../interact/index.js';
3
3
 
4
4
  async function main() {
5
5
  const deviceId = 'booted';
@@ -18,7 +18,7 @@ async function main() {
18
18
  process.exit(3);
19
19
  }
20
20
 
21
- const clickable = tree.elements.find(e => e.clickable) || tree.elements[0];
21
+ const clickable = tree.elements.find((e: any) => e.clickable) || tree.elements[0];
22
22
  console.log('Using element:', clickable.text || '(no text)', 'clickable=', clickable.clickable, 'center=', clickable.center);
23
23
  const [x,y] = clickable.center || [0,0];
24
24
 
@@ -37,7 +37,7 @@ export class DiagnosticError extends Error {
37
37
 
38
38
  // Exec ADB with diagnostics — moved from src/android/diagnostics.ts
39
39
  import { spawnSync } from 'child_process'
40
- import { getAdbCmd } from '../android/utils.js'
40
+ import { getAdbCmd } from './android/utils.js'
41
41
 
42
42
  export function execAdbWithDiagnostics(args: string[], deviceId?: string) {
43
43
  const adbArgs = deviceId ? ['-s', deviceId, ...args] : args
@@ -1,8 +1,8 @@
1
1
  import { execFile, spawn, execSync, spawnSync } from "child_process"
2
- import { DeviceInfo } from "../types.js"
2
+ import { DeviceInfo } from "../../types.js"
3
3
  import { promises as fsPromises } from 'fs'
4
4
  import path from 'path'
5
- import { makeEnvSnapshot } from '../utils/diagnostics.js'
5
+ import { makeEnvSnapshot } from '../diagnostics.js'
6
6
 
7
7
  export function getXcrunCmd() { return process.env.XCRUN_PATH || 'xcrun' }
8
8
 
@@ -1,6 +1,6 @@
1
1
  import { DeviceInfo } from "../types.js"
2
- import { listAndroidDevices } from "../android/utils.js"
3
- import { listIOSDevices } from "../ios/utils.js"
2
+ import { listAndroidDevices } from "./android/utils.js"
3
+ import { listIOSDevices } from "./ios/utils.js"
4
4
 
5
5
  export interface ResolveOptions {
6
6
  platform: "android" | "ios"
@@ -1,7 +1,6 @@
1
- import { AndroidObserve } from "../../src/android/observe.js";
2
- import { AndroidInteract } from "../../src/android/interact.js";
3
- import { iOSObserve } from "../../src/ios/observe.js";
4
- import { iOSInteract } from "../../src/ios/interact.js";
1
+ import { AndroidObserve, iOSObserve } from "../../src/observe/index.js";
2
+ import { AndroidInteract } from "../../src/interact/index.js";
3
+ import { iOSInteract } from "../../src/interact/index.js";
5
4
  import fs from "fs/promises";
6
5
 
7
6
  const androidObserve = new AndroidObserve();
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { AndroidManage } from '../../../dist/android/manage.js'
2
+ import { AndroidManage } from '../../../dist/utils/android/manage.js'
3
3
 
4
4
  async function main() {
5
5
  const [, , appPath, deviceId] = process.argv
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { iOSManage } from '../../../dist/ios/manage.js'
2
+ import { iOSManage } from '../../../dist/utils/ios/manage.js'
3
3
 
4
4
  async function main() {
5
5
  const [, , appPath, deviceId] = process.argv
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { ToolsManage } from '../../../dist/tools/manage.js'
2
+ import { ToolsManage } from '../../../dist/manage/index.js'
3
3
  import path from 'path'
4
4
 
5
5
  async function main() {
@@ -57,7 +57,7 @@ process.exit(0)
57
57
  // Prefer explicit XCODEBUILD_PATH to ensure deterministic behavior
58
58
  process.env.XCODEBUILD_PATH = xcodePath
59
59
 
60
- const { ToolsManage } = await import('../../../src/tools/manage.js')
60
+ const { ToolsManage } = await import('../../../src/manage/index.js')
61
61
 
62
62
  try {
63
63
  const ares = await ToolsManage.buildAppHandler({ platform: 'android', projectPath: androidProject })
@@ -93,7 +93,7 @@ process.exit(0)
93
93
  process.env.PATH = `${binDir}:${origPath}`
94
94
  process.env.XCRUN_PATH = simctlPath
95
95
 
96
- const { ToolsManage } = await import('../../../src/tools/manage.js')
96
+ const { ToolsManage } = await import('../../../src/manage/index.js')
97
97
 
98
98
  try {
99
99
  // Android build_and_install
@@ -2,7 +2,7 @@ import assert from 'assert'
2
2
  import fs from 'fs/promises'
3
3
  import os from 'os'
4
4
  import path from 'path'
5
- import { detectProjectPlatform } from '../../../src/tools/manage.js'
5
+ import { detectProjectPlatform } from '../../../src/manage/index.js'
6
6
 
7
7
  export async function run() {
8
8
  const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-detect-'))
@@ -26,7 +26,7 @@ process.exit(1)
26
26
  // Prefix PATH so our fake adb is preferred but keep original PATH to allow /usr/bin/env node to work
27
27
  process.env.PATH = `${binDir}:${origPath}`
28
28
 
29
- const { AndroidManage } = await import('../../../src/android/manage.js')
29
+ const { AndroidManage } = await import('../../../src/manage/index.js')
30
30
 
31
31
  try {
32
32
  const { dir, file: apk } = await makeTempFile('.apk')
@@ -70,7 +70,7 @@ process.exit(1)
70
70
  process.env.PATH = `${binDir2}:${origPath2}`
71
71
 
72
72
  try {
73
- const { iOSManage } = await import('../../../src/ios/manage.js')
73
+ const { iOSManage } = await import('../../../src/manage/index.js')
74
74
  const im = new iOSManage()
75
75
  const res2 = await im.startApp('com.example.myapp')
76
76
  console.log('ios diag res', res2)
@@ -36,7 +36,7 @@ exit 0
36
36
 
37
37
  // Import the module under test after PATH/ADB_PATH is adjusted
38
38
  console.log('DEBUG install.test ADB_PATH=', process.env.ADB_PATH, 'PATH starts with=', process.env.PATH?.split(':')[0])
39
- const { AndroidManage } = await import('../../../src/android/manage.js?test=install')
39
+ const { AndroidManage } = await import('../../../src/manage/index.js?test=install')
40
40
 
41
41
  try {
42
42
  // Test: install with .apk file should call adb install
@@ -15,7 +15,7 @@ export async function run() {
15
15
  const orig = process.env.MCP_DISABLE_AUTODETECT
16
16
  process.env.MCP_DISABLE_AUTODETECT = '1'
17
17
 
18
- const { ToolsManage } = await import('../../../src/tools/manage.js')
18
+ const { ToolsManage } = await import('../../../src/manage/index.js')
19
19
 
20
20
  try {
21
21
  // platform and projectType are now mandatory; calling without them should return a missing-params error
@@ -1,4 +1,4 @@
1
- import { AndroidObserve } from '../../src/android/observe.js'
1
+ import { AndroidObserve } from '../../src/observe/index.js'
2
2
 
3
3
  async function sleep(ms: number) { return new Promise(r => setTimeout(r, ms)) }
4
4
 
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Device E2E: get_screen_fingerprint
4
+ * Usage: RUN_DEVICE_TESTS=true npx tsx run-screen-fingerprint.ts [android|ios] [deviceId]
5
+ */
6
+
7
+ import { AndroidObserve } from '../../../src/observe/index.js'
8
+ import { iOSObserve } from '../../../src/observe/index.js'
9
+
10
+ async function main() {
11
+ const args = process.argv.slice(2)
12
+ const platform = (args[0] || 'android').toLowerCase()
13
+ const deviceId = args[1]
14
+
15
+ console.log(`Running screen fingerprint test for ${platform}${deviceId ? ` on ${deviceId}` : ''}`)
16
+
17
+ try {
18
+ const obs = platform === 'ios' ? new iOSObserve() : new AndroidObserve()
19
+ const id = platform === 'ios' ? (deviceId || 'booted') : deviceId
20
+ const res = await (obs as any).getScreenFingerprint(id)
21
+
22
+ if (res.error || !res.fingerprint) {
23
+ console.error('❌ Failed to compute fingerprint:', res.error)
24
+ process.exit(1)
25
+ }
26
+
27
+ console.log('Fingerprint:', res.fingerprint)
28
+ console.log('Activity:', res.activity || '<n/a>')
29
+ process.exit(0)
30
+ } catch (err) {
31
+ console.error('❌ Test failed:', err instanceof Error ? err.message : String(err))
32
+ process.exit(1)
33
+ }
34
+ }
35
+
36
+ main()
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ import { AndroidInteract } from '../../../dist/interact/index.js'
3
+
4
+
5
+ // Usage: tsx test/device/observe/run-scroll-test-android.ts <deviceId> <appId> <selectorText>
6
+ const args = process.argv.slice(2)
7
+ const DEVICE_ID = args[0] || process.env.DEVICE_ID || 'emulator-5554'
8
+ const SELECTOR = args[2] || process.env.SELECTOR || 'Generate Session'
9
+
10
+ async function main() {
11
+ console.log('Starting app if not running...')
12
+ // Best-effort tap to wake device/emulator
13
+ try { const tmp = new AndroidInteract(); await tmp.tap(10,10, DEVICE_ID).catch(()=>{}) } catch {}
14
+ await new Promise(r => setTimeout(r, 1000))
15
+
16
+ console.log('Running scroll_to_element for selector:', SELECTOR)
17
+ // Use ToolsInteract from dist to call the handler
18
+ const ToolsInteract = (await import('../../../dist/interact/index.js')).ToolsInteract
19
+
20
+ const res = await (ToolsInteract as any).scrollToElementHandler({ platform: 'android', selector: { text: SELECTOR }, direction: 'down', maxScrolls: 10, scrollAmount: 0.7, deviceId: DEVICE_ID })
21
+ console.log('Result:', JSON.stringify(res, null, 2))
22
+ }
23
+
24
+ main().catch(console.error)
@@ -9,8 +9,8 @@
9
9
  * npx tsx test-ui-tree.ts ios booted
10
10
  */
11
11
 
12
- import { AndroidObserve } from '../../src/android/observe.js';
13
- import { iOSObserve } from '../../src/ios/observe.js';
12
+ import { AndroidObserve } from '../../src/observe/index.js';
13
+ import { iOSObserve } from '../../src/observe/index.js';
14
14
 
15
15
  async function main() {
16
16
  const args = process.argv.slice(2);
@@ -1,5 +1,5 @@
1
- import { AndroidInteract } from "../../src/android/interact.js";
2
- import { AndroidObserve } from "../../src/android/observe.js";
1
+ import { AndroidInteract } from "../../src/interact/index.js";
2
+ import { AndroidObserve } from "../../src/observe/index.js";
3
3
 
4
4
  // Usage: npx tsx test/wait_for_element_real.ts <deviceId> <appId>
5
5
  const args = process.argv.slice(2);
@@ -0,0 +1,69 @@
1
+ import { AndroidObserve } from '../../../src/observe/index.js'
2
+
3
+ async function run() {
4
+ console.log('Starting get_screen_fingerprint unit tests...')
5
+
6
+ const origGet = (AndroidObserve as any).prototype.getUITree
7
+ const origCurrent = (AndroidObserve as any).prototype.getCurrentScreen
8
+
9
+ // Test 1: stable identical screens produce same fingerprint
10
+ ;(AndroidObserve as any).prototype.getUITree = async function() {
11
+ return {
12
+ device: { platform: 'android', id: 'mock', osVersion: '12', model: 'Pixel', simulator: true },
13
+ screen: '',
14
+ resolution: { width: 1080, height: 1920 },
15
+ elements: [
16
+ { text: 'Title', type: 'TextView', contentDescription: null, clickable: false, enabled: true, visible: true, bounds: [0,0,1080,100], resourceId: 'id/title' },
17
+ { text: 'Sign in', type: 'Button', contentDescription: null, clickable: true, enabled: true, visible: true, bounds: [0,200,200,260], resourceId: 'id/signin' }
18
+ ]
19
+ }
20
+ }
21
+
22
+ ;(AndroidObserve as any).prototype.getCurrentScreen = async function() {
23
+ return { device: { platform: 'android', id: 'mock', osVersion: '12', model: 'Pixel', simulator: true }, package: 'com.example', activity: 'com.example.MainActivity', shortActivity: 'MainActivity' }
24
+ }
25
+
26
+ const ai = new AndroidObserve()
27
+ const a = await ai.getScreenFingerprint('mock')
28
+ const b = await ai.getScreenFingerprint('mock')
29
+ console.log('Test 1:', a.fingerprint === b.fingerprint ? 'PASS' : 'FAIL')
30
+
31
+ // Test 2: change in UI text changes fingerprint
32
+ ;(AndroidObserve as any).prototype.getUITree = async function() {
33
+ return {
34
+ device: { platform: 'android', id: 'mock', osVersion: '12', model: 'Pixel', simulator: true },
35
+ screen: '',
36
+ resolution: { width: 1080, height: 1920 },
37
+ elements: [
38
+ { text: 'Title', type: 'TextView', contentDescription: null, clickable: false, enabled: true, visible: true, bounds: [0,0,1080,100], resourceId: 'id/title' },
39
+ { text: 'Profile', type: 'Button', contentDescription: null, clickable: true, enabled: true, visible: true, bounds: [0,200,200,260], resourceId: 'id/signin' }
40
+ ]
41
+ }
42
+ }
43
+
44
+ const c = await ai.getScreenFingerprint('mock')
45
+ console.log('Test 2:', a.fingerprint !== c.fingerprint ? 'PASS' : 'FAIL')
46
+
47
+ // Test 3: dynamic text ignored (timestamp) should not change fingerprint
48
+ ;(AndroidObserve as any).prototype.getUITree = async function() {
49
+ return {
50
+ device: { platform: 'android', id: 'mock', osVersion: '12', model: 'Pixel', simulator: true },
51
+ screen: '',
52
+ resolution: { width: 1080, height: 1920 },
53
+ elements: [
54
+ { text: 'Title', type: 'TextView', contentDescription: null, clickable: false, enabled: true, visible: true, bounds: [0,0,1080,100], resourceId: 'id/title' },
55
+ { text: 'Sign in', type: 'Button', contentDescription: null, clickable: true, enabled: true, visible: true, bounds: [0,200,200,260], resourceId: 'id/signin' },
56
+ { text: '12:34', type: 'TextView', contentDescription: null, clickable: false, enabled: true, visible: true, bounds: [900,10,1080,40], resourceId: null }
57
+ ]
58
+ }
59
+ }
60
+
61
+ const d = await ai.getScreenFingerprint('mock')
62
+ console.log('Test 3:', a.fingerprint === d.fingerprint ? 'PASS' : 'FAIL')
63
+
64
+ // Restore
65
+ ;(AndroidObserve as any).prototype.getUITree = origGet
66
+ ;(AndroidObserve as any).prototype.getCurrentScreen = origCurrent
67
+ }
68
+
69
+ run().catch(console.error)
@@ -1,4 +1,4 @@
1
- import { parseLogLine } from '../../../src/android/utils.js'
1
+ import { parseLogLine } from '../../../src/utils/android/utils.js'
2
2
 
3
3
  function assert(cond: boolean, msg?: string) { if (!cond) throw new Error(msg || 'Assertion failed') }
4
4
 
@@ -1,7 +1,7 @@
1
1
  import { promises as fs } from 'fs'
2
2
  import os from 'os'
3
3
  import path from 'path'
4
- import { AndroidObserve } from '../../../src/android/observe.js'
4
+ import { AndroidObserve } from '../../../src/observe/index.js'
5
5
 
6
6
  async function run() {
7
7
  const tmp = os.tmpdir()
@@ -0,0 +1,129 @@
1
+ import { ToolsInteract } from '../../../src/interact/index.js'
2
+ import { ToolsObserve } from '../../../src/observe/index.js'
3
+
4
+ const origGet = (ToolsObserve as any).getUITreeHandler
5
+ const origSwipe = (ToolsInteract as any).swipeHandler
6
+
7
+ async function runTests() {
8
+ // Use a stable logger to avoid test harness replacing console.log between calls
9
+ console.log = (...args: any[]) => { try { process.stdout.write(args.map(a => (typeof a === 'string' ? a : JSON.stringify(a))).join(' ') + '\n') } catch {} }
10
+ console.log('Starting tests for scroll_to_element...')
11
+
12
+ // Test 1: Element found immediately
13
+ console.log('\nTest 1: Element found immediately')
14
+ (ToolsObserve as any).getUITreeHandler = async () => ({
15
+ device: { platform: 'android', id: 'mock', osVersion: '12', model: 'Pixel', simulator: true },
16
+ screen: '',
17
+ resolution: { width: 1080, height: 1920 },
18
+ elements: [{
19
+ text: 'Target',
20
+ type: 'Button',
21
+ contentDescription: null,
22
+ clickable: true,
23
+ enabled: true,
24
+ visible: true,
25
+ bounds: [0, 0, 100, 100],
26
+ resourceId: null
27
+ }]
28
+ })
29
+
30
+ const res1 = await ToolsInteract.scrollToElementHandler({ platform: 'android', selector: { text: 'Target' }, direction: 'down', maxScrolls: 5, scrollAmount: 0.7 })
31
+ console.log('Result:', res1.success === true ? 'PASS' : 'FAIL')
32
+ console.log('scrollsPerformed:', (res1 as any).scrollsPerformed)
33
+
34
+ // Test 2: Element found after scrolling
35
+ console.log('\nTest 2: Element found after scrolling')
36
+ let calls = 0
37
+ (ToolsObserve as any).getUITreeHandler = async () => {
38
+ calls++
39
+ if (calls < 3) {
40
+ return {
41
+ device: { platform: 'android', id: 'mock', osVersion: '12', model: 'Pixel', simulator: true },
42
+ screen: '',
43
+ resolution: { width: 1080, height: 1920 },
44
+ elements: []
45
+ }
46
+ }
47
+ return {
48
+ device: { platform: 'android', id: 'mock', osVersion: '12', model: 'Pixel', simulator: true },
49
+ screen: '',
50
+ resolution: { width: 1080, height: 1920 },
51
+ elements: [{
52
+ text: 'Target',
53
+ type: 'Button',
54
+ contentDescription: null,
55
+ clickable: true,
56
+ enabled: true,
57
+ visible: true,
58
+ bounds: [0, 0, 100, 100],
59
+ resourceId: null
60
+ }]
61
+ }
62
+ }
63
+
64
+ // Stub swipe so it doesn't try to call adb/idb
65
+ (ToolsInteract as any).swipeHandler = async () => ({ success: true })
66
+
67
+ const res2 = await ToolsInteract.scrollToElementHandler({ platform: 'android', selector: { text: 'Target' }, direction: 'down', maxScrolls: 5, scrollAmount: 0.7 })
68
+ console.log('Result:', res2.success === true ? 'PASS' : 'FAIL')
69
+ console.log('calls:', calls, calls >= 3 ? 'PASS' : 'FAIL')
70
+
71
+ // Test 3: UI unchanged stops early
72
+ console.log('\nTest 3: UI unchanged stops early')
73
+ (ToolsObserve as any).getUITreeHandler = async () => ({
74
+ device: { platform: 'android', id: 'mock', osVersion: '12', model: 'Pixel', simulator: true },
75
+ screen: '',
76
+ resolution: { width: 1080, height: 1920 },
77
+ elements: []
78
+ })
79
+
80
+ (ToolsInteract as any).swipeHandler = async () => ({ success: true })
81
+
82
+ const res3 = await ToolsInteract.scrollToElementHandler({ platform: 'android', selector: { text: 'Missing' }, direction: 'down', maxScrolls: 5, scrollAmount: 0.7 })
83
+ console.log('Result:', res3.success === false && (res3 as any).attempts === 1 ? 'PASS' : 'FAIL')
84
+ console.log('Reason:', (res3 as any).reason || JSON.stringify(res3))
85
+
86
+ // Test 4: Offscreen element scrolls into view
87
+ console.log('\nTest 4: Offscreen element scrolls into view')
88
+ const ai = new (await import('../../../src/interact/index.js')).AndroidInteract()
89
+ const origObserveGet = ai['observe'].getUITree
90
+ const origAiSwipe = ai.swipe
91
+ let swiped = false
92
+ let swipeCalled = 0
93
+ ;(ai['observe'] as any).getUITree = async () => {
94
+ if (!swiped) {
95
+ return {
96
+ device: { platform: 'android', id: 'mock', osVersion: '12', model: 'Pixel', simulator: true },
97
+ screen: '',
98
+ resolution: { width: 1080, height: 1920 },
99
+ elements: [ { text: null, type: 'android.view.View', resourceId: null, contentDescription: null, bounds: [0,0,1080,200], visible: true } ]
100
+ }
101
+ }
102
+ return {
103
+ device: { platform: 'android', id: 'mock', osVersion: '12', model: 'Pixel', simulator: true },
104
+ screen: '',
105
+ resolution: { width: 1080, height: 1920 },
106
+ elements: [{ text: 'OffscreenTarget', type: 'android.widget.Button', contentDescription: null, clickable: true, enabled: true, visible: true, bounds: [100,400,300,460], resourceId: null }]
107
+ }
108
+ }
109
+ ;(ai as any).swipe = async () => { swipeCalled++; swiped = true; return { success: true } }
110
+
111
+ const r4 = await ai.scrollToElement({ text: 'OffscreenTarget' }, 'down', 3, 0.7, 'mock')
112
+ const ok4 = r4 && (r4 as any).success === true && (r4 as any).scrollsPerformed === 1 && swipeCalled === 1
113
+ console.log('Result:', ok4 ? 'PASS' : 'FAIL')
114
+ console.log(' success:', (r4 as any).success, 'scrollsPerformed:', (r4 as any).scrollsPerformed, 'swipeCalled:', swipeCalled)
115
+
116
+ ;(ai['observe'] as any).getUITree = origObserveGet
117
+ ;(ai as any).swipe = origAiSwipe
118
+
119
+ // Restore
120
+ (ToolsObserve as any).getUITreeHandler = origGet
121
+ ;(ToolsInteract as any).swipeHandler = origSwipe
122
+ }
123
+
124
+ // Ensure console.log is a function (some test runners replace it)
125
+ if (typeof console.log !== 'function') {
126
+ console.log = (...args: any[]) => { try { process.stdout.write(args.map(a => (typeof a === 'string' ? a : JSON.stringify(a))).join(' ') + '\n') } catch { /* swallow */ } }
127
+ }
128
+
129
+ runTests().catch(console.error)
@@ -1,5 +1,5 @@
1
- import { AndroidInteract } from '../../../src/android/interact.js';
2
- import { AndroidObserve } from '../../../src/android/observe.js';
1
+ import { AndroidInteract } from '../../../src/interact/index.js';
2
+ import { AndroidObserve } from '../../../src/observe/index.js';
3
3
 
4
4
  const originalGetUITree = (AndroidObserve as any).prototype.getUITree;
5
5
 
@@ -95,7 +95,7 @@ async function runTests() {
95
95
  const elapsed4 = Date.now() - start4;
96
96
  console.log("Result:", result4.found === false && result4.error === "ADB Connection Failed" ? "PASS" : "FAIL");
97
97
  console.log("Error Message:", result4.error);
98
- console.log("Elapsed time (should be < 500ms):", elapsed4, elapsed4 < 500 ? "PASS" : "FAIL");
98
+ console.log("Elapsed time (should be < 1000ms):", elapsed4, elapsed4 < 1000 ? "PASS" : "FAIL");
99
99
 
100
100
  // Restore
101
101
  (AndroidObserve as any).prototype.getUITree = originalGetUITree;
@@ -1,13 +1,14 @@
1
- // Aggregator entrypoint for unit tests
2
- import './utils/detect-java.test.ts'
3
- import './observe/logparse.test.ts'
4
- import './observe/logstream.test.ts'
5
- import './observe/wait_for_element_mock.ts'
6
- import './manage/install.test.ts'
7
- import './manage/build.test.ts'
8
- import './manage/build_and_install.test.ts'
9
- import './manage/diagnostics.test.ts'
10
- import './manage/detection.test.ts'
11
- import './manage/mcp_disable_autodetect.test.ts'
1
+ // Aggregator entrypoint for unit tests (updated to new test layout)
2
+ import '../observe/unit/logparse.test.ts'
3
+ import '../observe/unit/logstream.test.ts'
4
+ import '../observe/unit/wait_for_element_mock.ts'
5
+ import '../observe/unit/get_screen_fingerprint.test.ts'
6
+
7
+ import '../manage/unit/install.test.ts'
8
+ import '../manage/unit/build.test.ts'
9
+ import '../manage/unit/build_and_install.test.ts'
10
+ import '../manage/unit/diagnostics.test.ts'
11
+ import '../manage/unit/detection.test.ts'
12
+ import '../manage/unit/mcp_disable_autodetect.test.ts'
12
13
 
13
14
  console.log('Unit tests loaded.')
@@ -1,45 +0,0 @@
1
- import { resolveTargetDevice } from '../utils/resolve-device.js'
2
- import { AndroidInteract } from '../android/interact.js'
3
- import { iOSInteract } from '../ios/interact.js'
4
-
5
- export class ToolsInteract {
6
-
7
- static async waitForElementHandler({ platform, text, timeout, deviceId }: { platform: 'android' | 'ios', text: string, timeout?: number, deviceId?: string }) {
8
- const effectiveTimeout = timeout ?? 10000
9
- if (platform === 'android') {
10
- const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
11
- return await new AndroidInteract().waitForElement(text, effectiveTimeout, resolved.id)
12
- } else {
13
- const resolved = await resolveTargetDevice({ platform: 'ios', deviceId })
14
- return await new iOSInteract().waitForElement(text, effectiveTimeout, resolved.id)
15
- }
16
- }
17
-
18
- static async tapHandler({ platform, x, y, deviceId }: { platform?: 'android' | 'ios', x: number, y: number, deviceId?: string }) {
19
- const effectivePlatform = platform || 'android'
20
- if (effectivePlatform === 'android') {
21
- const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
22
- return await new AndroidInteract().tap(x, y, resolved.id)
23
- } else {
24
- const resolved = await resolveTargetDevice({ platform: 'ios', deviceId })
25
- return await new iOSInteract().tap(x, y, resolved.id)
26
- }
27
- }
28
-
29
- static async swipeHandler({ x1, y1, x2, y2, duration, deviceId }: { x1: number, y1: number, x2: number, y2: number, duration: number, deviceId?: string }) {
30
- const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
31
- return await new AndroidInteract().swipe(x1, y1, x2, y2, duration, resolved.id)
32
- }
33
-
34
- static async typeTextHandler({ text, deviceId }: { text: string, deviceId?: string }) {
35
- const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
36
- return await new AndroidInteract().typeText(text, resolved.id)
37
- }
38
-
39
- static async pressBackHandler({ deviceId }: { deviceId?: string }) {
40
- const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
41
- return await new AndroidInteract().pressBack(resolved.id)
42
- }
43
-
44
- }
45
-