mobile-debug-mcp 0.21.5 → 0.22.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 (84) hide show
  1. package/AGENTS.md +74 -0
  2. package/README.md +24 -5
  3. package/dist/interact/index.js +220 -13
  4. package/dist/observe/ios.js +10 -3
  5. package/dist/server-core.js +707 -0
  6. package/dist/server.js +6 -693
  7. package/dist/utils/resolve-device.js +15 -3
  8. package/docs/CHANGELOG.md +6 -1
  9. package/docs/tools/interact.md +69 -30
  10. package/package.json +3 -3
  11. package/skills/README.md +35 -0
  12. package/skills/test-authoring/SKILL.md +57 -0
  13. package/skills/test-authoring/references/repo-test-layout.md +47 -0
  14. package/skills/test-authoring/references/test-authoring-workflow.md +73 -0
  15. package/skills/test-authoring/references/test-quality-checklist.md +39 -0
  16. package/src/interact/index.ts +250 -13
  17. package/src/observe/ios.ts +12 -3
  18. package/src/server-core.ts +762 -0
  19. package/src/server.ts +8 -754
  20. package/src/types.ts +10 -1
  21. package/src/utils/resolve-device.ts +19 -3
  22. package/test/device/automated/observe/capture_screenshot.android.smoke.ts +30 -0
  23. package/test/device/automated/observe/capture_screenshot.ios.smoke.ts +30 -0
  24. package/test/{observe/device → device/automated/observe}/get_logs.android.smoke.ts +1 -1
  25. package/test/{observe/device → device/automated/observe}/get_logs.ios.smoke.ts +1 -1
  26. package/test/device/automated/observe/get_ui_tree.android.smoke.ts +31 -0
  27. package/test/device/automated/observe/get_ui_tree.ios.smoke.ts +31 -0
  28. package/test/device/index.ts +52 -0
  29. package/test/{interact/device/smoke-test.ts → device/manual/interact/app_lifecycle.manual.ts} +5 -5
  30. package/test/{manage/device/run-build-install-ios.ts → device/manual/manage/build_install_ios.manual.ts} +1 -1
  31. package/test/{manage/device → device/manual/manage}/install.integration.ts +6 -6
  32. package/test/{manage/device/run-install-android.ts → device/manual/manage/install_android.manual.ts} +1 -1
  33. package/test/{manage/device/run-install-ios.ts → device/manual/manage/install_ios.manual.ts} +1 -1
  34. package/test/device/manual/observe/capture_screenshot.manual.ts +29 -0
  35. package/test/{helpers/run-get-logs.ts → device/manual/observe/get_logs.manual.ts} +1 -1
  36. package/test/device/manual/observe/get_ui_tree.manual.ts +29 -0
  37. package/test/{observe/device/logstream-real.ts → device/manual/observe/logstream.manual.ts} +1 -1
  38. package/test/{observe/device/run-screen-fingerprint.ts → device/manual/observe/screen_fingerprint.manual.ts} +1 -1
  39. package/test/{observe/device/run-scroll-test-android.ts → device/manual/observe/scroll_to_element_android.manual.ts} +1 -1
  40. package/test/{observe/device/test-ui-tree.ts → device/manual/observe/ui_tree.manual.ts} +6 -6
  41. package/test/unit/index.ts +47 -27
  42. package/test/unit/interact/handler_shapes.test.ts +55 -0
  43. package/test/unit/interact/tap_element.test.ts +170 -0
  44. package/test/unit/interact/wait_for_screen_change.test.ts +34 -0
  45. package/test/{interact/unit → unit/interact}/wait_for_ui_contract.test.ts +11 -10
  46. package/test/unit/interact/wait_for_ui_selector_matching.test.ts +76 -0
  47. package/test/unit/manage/handler_shapes.test.ts +43 -0
  48. package/test/{observe/unit → unit/observe}/capture_debug_snapshot.test.ts +5 -1
  49. package/test/{observe/unit → unit/observe}/find_element.test.ts +12 -6
  50. package/test/unit/observe/get_screen_fingerprint.test.ts +71 -0
  51. package/test/unit/observe/ios-getlogs.test.ts +53 -0
  52. package/test/unit/observe/scroll_to_element.test.ts +127 -0
  53. package/test/unit/server/contract.test.ts +45 -0
  54. package/test/unit/server/response_shapes.test.ts +93 -0
  55. package/test/unit/system/adb_version.test.ts +35 -0
  56. package/test/unit/system/get_system_status.test.ts +20 -0
  57. package/test/unit/system/system_status.test.ts +141 -0
  58. package/test/{utils → unit/utils}/detect_java.test.ts +1 -1
  59. package/test/unit/utils/exec.test.ts +51 -0
  60. package/test/unit/utils/resolve_device.test.ts +63 -0
  61. package/tsconfig.json +2 -2
  62. package/test/interact/device/run-real-test.ts +0 -3
  63. package/test/interact/unit/wait_for_screen_change.test.ts +0 -32
  64. package/test/interact/unit/wait_for_ui.test.ts +0 -76
  65. package/test/interact/unit/wait_for_ui_new.test.ts +0 -57
  66. package/test/observe/device/wait_for_element_real.ts +0 -3
  67. package/test/observe/unit/get_screen_fingerprint.test.ts +0 -69
  68. package/test/observe/unit/ios-getlogs.test.ts +0 -67
  69. package/test/observe/unit/scroll_to_element.test.ts +0 -129
  70. package/test/observe/unit/wait_for_element_mock.ts +0 -2
  71. package/test/observe/unit/wait_for_ui_edge_cases.test.ts +0 -41
  72. package/test/observe/unit/wait_for_ui_stability.test.ts +0 -30
  73. package/test/system/adb_version.test.ts +0 -25
  74. package/test/system/get_system_status.test.ts +0 -52
  75. package/test/system/system_status.test.ts +0 -109
  76. /package/test/{manage/unit → unit/manage}/build.test.ts +0 -0
  77. /package/test/{manage/unit → unit/manage}/build_and_install.test.ts +0 -0
  78. /package/test/{manage/unit → unit/manage}/detection.test.ts +0 -0
  79. /package/test/{manage/unit → unit/manage}/diagnostics.test.ts +0 -0
  80. /package/test/{manage/unit → unit/manage}/install.test.ts +0 -0
  81. /package/test/{manage/unit → unit/manage}/mcp_disable_autodetect.test.ts +0 -0
  82. /package/test/{observe/unit → unit/observe}/get_logs.test.ts +0 -0
  83. /package/test/{observe/unit → unit/observe}/logparse.test.ts +0 -0
  84. /package/test/{observe/unit → unit/observe}/logstream.test.ts +0 -0
package/src/types.ts CHANGED
@@ -132,6 +132,16 @@ export interface TapResponse {
132
132
  error?: string;
133
133
  }
134
134
 
135
+ export interface TapElementResponse {
136
+ success: boolean;
137
+ elementId: string;
138
+ action: 'tap';
139
+ error?: {
140
+ code: 'element_not_found' | 'element_not_visible' | 'element_not_enabled' | 'tap_failed';
141
+ message: string;
142
+ };
143
+ }
144
+
135
145
  export interface SwipeResponse {
136
146
  device: DeviceInfo;
137
147
  success: boolean;
@@ -161,4 +171,3 @@ export interface InstallAppResponse {
161
171
  error?: string;
162
172
  diagnostics?: any;
163
173
  }
164
-
@@ -18,14 +18,30 @@ function parseNumericVersion(v: string): number {
18
18
  return major + minor / 100
19
19
  }
20
20
 
21
+ let androidDeviceLister = listAndroidDevices
22
+ let iosDeviceLister = listIOSDevices
23
+
24
+ export function _setDeviceListersForTests(overrides: {
25
+ listAndroidDevices?: typeof listAndroidDevices
26
+ listIOSDevices?: typeof listIOSDevices
27
+ }) {
28
+ if (overrides.listAndroidDevices) androidDeviceLister = overrides.listAndroidDevices
29
+ if (overrides.listIOSDevices) iosDeviceLister = overrides.listIOSDevices
30
+ }
31
+
32
+ export function _resetDeviceListersForTests() {
33
+ androidDeviceLister = listAndroidDevices
34
+ iosDeviceLister = listIOSDevices
35
+ }
36
+
21
37
  export async function listDevices(platform?: "android" | "ios", appId?: string): Promise<DeviceInfo[]> {
22
38
  if (!platform || platform === "android") {
23
- const android = await listAndroidDevices(appId)
39
+ const android = await androidDeviceLister(appId)
24
40
  if (platform === "android") return android
25
- const ios = await listIOSDevices(appId)
41
+ const ios = await iosDeviceLister(appId)
26
42
  return [...android, ...ios]
27
43
  }
28
- return listIOSDevices(appId)
44
+ return iosDeviceLister(appId)
29
45
  }
30
46
 
31
47
  export async function resolveTargetDevice(opts: ResolveOptions): Promise<DeviceInfo> {
@@ -0,0 +1,30 @@
1
+ import fs from 'fs'
2
+ import { execSync } from 'child_process'
3
+
4
+ function log(msg: string) { console.log(msg) }
5
+
6
+ if (process.env.SKIP_DEVICE_TESTS === '1') {
7
+ log('SKIP_DEVICE_TESTS=1 detected - skipping android screenshot smoke test')
8
+ process.exit(0)
9
+ }
10
+
11
+ const helperScript = 'test/device/manual/observe/capture_screenshot.manual.ts'
12
+ if (!fs.existsSync(helperScript)) {
13
+ console.error(`Missing ${helperScript}. Run 'npm run build' first or ensure the helper exists.`)
14
+ process.exit(1)
15
+ }
16
+
17
+ try {
18
+ const out = execSync(`tsx ${helperScript} --platform android --id default`, { encoding: 'utf8', maxBuffer: 10 * 1024 * 1024, timeout: 30000 })
19
+ const parsed = JSON.parse(out)
20
+
21
+ if (!parsed?.resolution || parsed.resolution.width <= 0 || parsed.resolution.height <= 0) throw new Error('Invalid screenshot resolution')
22
+ if (typeof parsed.screenshotBytes !== 'number' || parsed.screenshotBytes <= 0) throw new Error('Screenshot payload missing')
23
+
24
+ log('Android capture_screenshot smoke test: PASS')
25
+ process.exit(0)
26
+ } catch (err: any) {
27
+ console.error('Android capture_screenshot smoke test: FAIL')
28
+ console.error(err && err.message ? err.message : err)
29
+ process.exit(2)
30
+ }
@@ -0,0 +1,30 @@
1
+ import fs from 'fs'
2
+ import { execSync } from 'child_process'
3
+
4
+ function log(msg: string) { console.log(msg) }
5
+
6
+ if (process.env.SKIP_DEVICE_TESTS === '1') {
7
+ log('SKIP_DEVICE_TESTS=1 detected - skipping ios screenshot smoke test')
8
+ process.exit(0)
9
+ }
10
+
11
+ const helperScript = 'test/device/manual/observe/capture_screenshot.manual.ts'
12
+ if (!fs.existsSync(helperScript)) {
13
+ console.error(`Missing ${helperScript}. Run 'npm run build' first or ensure the helper exists.`)
14
+ process.exit(1)
15
+ }
16
+
17
+ try {
18
+ const out = execSync(`tsx ${helperScript} --platform ios --id booted`, { encoding: 'utf8', maxBuffer: 10 * 1024 * 1024, timeout: 30000 })
19
+ const parsed = JSON.parse(out)
20
+
21
+ if (!parsed?.resolution || parsed.resolution.width <= 0 || parsed.resolution.height <= 0) throw new Error('Invalid screenshot resolution')
22
+ if (typeof parsed.screenshotBytes !== 'number' || parsed.screenshotBytes <= 0) throw new Error('Screenshot payload missing')
23
+
24
+ log('iOS capture_screenshot smoke test: PASS')
25
+ process.exit(0)
26
+ } catch (err: any) {
27
+ console.error('iOS capture_screenshot smoke test: FAIL')
28
+ console.error(err && err.message ? err.message : err)
29
+ process.exit(2)
30
+ }
@@ -9,7 +9,7 @@ if (process.env.SKIP_DEVICE_TESTS === '1') {
9
9
  }
10
10
 
11
11
  // Ensure helper script exists
12
- const helperScript = 'test/helpers/run-get-logs.ts'
12
+ const helperScript = 'test/device/manual/observe/get_logs.manual.ts'
13
13
  if (!fs.existsSync(helperScript)) {
14
14
  console.error(`Missing ${helperScript}. Run 'npm run build' first or ensure the helper exists.`)
15
15
  process.exit(1)
@@ -8,7 +8,7 @@ if (process.env.SKIP_DEVICE_TESTS === '1') {
8
8
  process.exit(0)
9
9
  }
10
10
 
11
- const helperScript = 'test/helpers/run-get-logs.ts'
11
+ const helperScript = 'test/device/manual/observe/get_logs.manual.ts'
12
12
  if (!fs.existsSync(helperScript)) {
13
13
  console.error(`Missing ${helperScript}. Run 'npm run build' first or ensure the helper exists.`)
14
14
  process.exit(1)
@@ -0,0 +1,31 @@
1
+ import fs from 'fs'
2
+ import { execSync } from 'child_process'
3
+
4
+ function log(msg: string) { console.log(msg) }
5
+
6
+ if (process.env.SKIP_DEVICE_TESTS === '1') {
7
+ log('SKIP_DEVICE_TESTS=1 detected - skipping android ui tree smoke test')
8
+ process.exit(0)
9
+ }
10
+
11
+ const helperScript = 'test/device/manual/observe/get_ui_tree.manual.ts'
12
+ if (!fs.existsSync(helperScript)) {
13
+ console.error(`Missing ${helperScript}. Run 'npm run build' first or ensure the helper exists.`)
14
+ process.exit(1)
15
+ }
16
+
17
+ try {
18
+ const out = execSync(`tsx ${helperScript} --platform android --id default`, { encoding: 'utf8', maxBuffer: 10 * 1024 * 1024, timeout: 30000 })
19
+ const parsed = JSON.parse(out)
20
+
21
+ if (!parsed?.resolution || parsed.resolution.width <= 0 || parsed.resolution.height <= 0) throw new Error('Invalid UI tree resolution')
22
+ if (typeof parsed.elementCount !== 'number') throw new Error('elementCount missing')
23
+ if (parsed.elementCount > 0 && parsed.hasCenterAndDepth !== true) throw new Error('UI element metadata missing center/depth')
24
+
25
+ log('Android get_ui_tree smoke test: PASS')
26
+ process.exit(0)
27
+ } catch (err: any) {
28
+ console.error('Android get_ui_tree smoke test: FAIL')
29
+ console.error(err && err.message ? err.message : err)
30
+ process.exit(2)
31
+ }
@@ -0,0 +1,31 @@
1
+ import fs from 'fs'
2
+ import { execSync } from 'child_process'
3
+
4
+ function log(msg: string) { console.log(msg) }
5
+
6
+ if (process.env.SKIP_DEVICE_TESTS === '1') {
7
+ log('SKIP_DEVICE_TESTS=1 detected - skipping ios ui tree smoke test')
8
+ process.exit(0)
9
+ }
10
+
11
+ const helperScript = 'test/device/manual/observe/get_ui_tree.manual.ts'
12
+ if (!fs.existsSync(helperScript)) {
13
+ console.error(`Missing ${helperScript}. Run 'npm run build' first or ensure the helper exists.`)
14
+ process.exit(1)
15
+ }
16
+
17
+ try {
18
+ const out = execSync(`tsx ${helperScript} --platform ios --id booted`, { encoding: 'utf8', maxBuffer: 10 * 1024 * 1024, timeout: 30000 })
19
+ const parsed = JSON.parse(out)
20
+
21
+ if (!parsed?.resolution || parsed.resolution.width <= 0 || parsed.resolution.height <= 0) throw new Error('Invalid UI tree resolution')
22
+ if (typeof parsed.elementCount !== 'number') throw new Error('elementCount missing')
23
+ if (parsed.elementCount > 0 && parsed.hasCenterAndDepth !== true) throw new Error('UI element metadata missing center/depth')
24
+
25
+ log('iOS get_ui_tree smoke test: PASS')
26
+ process.exit(0)
27
+ } catch (err: any) {
28
+ console.error('iOS get_ui_tree smoke test: FAIL')
29
+ console.error(err && err.message ? err.message : err)
30
+ process.exit(2)
31
+ }
@@ -0,0 +1,52 @@
1
+ import { readdir } from 'fs/promises'
2
+ import path from 'path'
3
+ import { spawn } from 'child_process'
4
+ import { fileURLToPath } from 'url'
5
+
6
+ const deviceRoot = fileURLToPath(new URL('.', import.meta.url))
7
+ const automatedRoot = fileURLToPath(new URL('./automated', import.meta.url))
8
+ const runnableSuffixes = ['.test.ts', '.smoke.ts', '.integration.ts']
9
+
10
+ async function collectFiles(dir: string): Promise<string[]> {
11
+ const entries = await readdir(dir, { withFileTypes: true })
12
+ const files = await Promise.all(entries.map(async (entry) => {
13
+ const fullPath = path.join(dir, entry.name)
14
+ if (entry.isDirectory()) return collectFiles(fullPath)
15
+ return fullPath
16
+ }))
17
+
18
+ return files.flat()
19
+ }
20
+
21
+ async function runFile(file: string): Promise<void> {
22
+ const relativePath = path.relative(deviceRoot, file).replaceAll(path.sep, '/')
23
+ console.log(`Running device test: ${relativePath}`)
24
+
25
+ await new Promise<void>((resolve, reject) => {
26
+ const child = spawn('tsx', [file], { stdio: 'inherit' })
27
+ child.on('error', reject)
28
+ child.on('close', (code) => {
29
+ if (code === 0) {
30
+ resolve()
31
+ return
32
+ }
33
+
34
+ reject(new Error(`Device test failed: ${relativePath} (exit code ${code ?? 'unknown'})`))
35
+ })
36
+ })
37
+ }
38
+
39
+ (async function () {
40
+ const allFiles = (await collectFiles(automatedRoot))
41
+ .filter((file) => runnableSuffixes.some((suffix) => file.endsWith(suffix)))
42
+ .sort()
43
+
44
+ for (const file of allFiles) {
45
+ await runFile(file)
46
+ }
47
+
48
+ console.log('Device tests loaded.')
49
+ })().catch((error) => {
50
+ console.error(error instanceof Error ? error.message : String(error))
51
+ process.exit(1)
52
+ })
@@ -1,6 +1,6 @@
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";
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";
4
4
  import fs from "fs/promises";
5
5
 
6
6
  const androidObserve = new AndroidObserve();
@@ -14,7 +14,7 @@ async function main() {
14
14
  const appId = args[1];
15
15
 
16
16
  if ((platform !== "android" && platform !== "ios") || !appId) {
17
- console.error("Usage: npx tsx test/smoke-test.ts <android|ios> <appId>");
17
+ console.error("Usage: npx tsx test/device/manual/interact/app_lifecycle.manual.ts <android|ios> <appId>");
18
18
  process.exit(1);
19
19
  }
20
20
 
@@ -104,7 +104,7 @@ async function main() {
104
104
 
105
105
  console.log(`\n✨ Smoke test COMPLETED SUCCESSFULLY! ✨\n`);
106
106
 
107
- } catch {
107
+ } catch (error) {
108
108
  console.error(`\n❌ Smoke test FAILED:`, error);
109
109
  process.exit(1);
110
110
  }
@@ -35,7 +35,7 @@ function spawnStream(cmd: string, args: string[], opts: any = {}): Promise<numbe
35
35
  async function main() {
36
36
  const [, , projectPath, deviceId = 'booted'] = process.argv
37
37
  if (!projectPath) {
38
- console.error('Usage: tsx test/integration/manage/run-build-install-ios.ts <project-dir> [deviceId]')
38
+ console.error('Usage: tsx test/device/manual/manage/build_install_ios.manual.ts <project-dir> [deviceId]')
39
39
  process.exit(1)
40
40
  }
41
41
 
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
- // Integration runner: calls existing run-install-android.ts or run-install-ios.ts
3
- // Usage: npx tsx test/integration/install.integration.ts /path/to/project [deviceId]
2
+ // Integration runner: calls existing manual install helpers.
3
+ // Usage: npx tsx test/device/manual/manage/install.integration.ts /path/to/project [deviceId]
4
4
  import { spawn } from 'child_process'
5
5
  import fs from 'fs'
6
6
  import path from 'path'
7
7
 
8
8
  const args = process.argv.slice(2)
9
9
  if (args.length < 1) {
10
- console.error('Usage: npx tsx test/integration/install.integration.ts /path/to/project [deviceId]')
10
+ console.error('Usage: npx tsx test/device/manual/manage/install.integration.ts /path/to/project [deviceId]')
11
11
  process.exit(2)
12
12
  }
13
13
  const project = args[0]
@@ -29,9 +29,9 @@ function isIosDir(p: string) {
29
29
 
30
30
  let runner: string | undefined
31
31
  if (isAndroidDir(project)) {
32
- runner = path.join(process.cwd(), 'test', 'integration', 'run-install-android.ts')
32
+ runner = path.join(process.cwd(), 'test', 'device', 'manual', 'manage', 'install_android.manual.ts')
33
33
  } else if (isIosDir(project)) {
34
- runner = path.join(process.cwd(), 'test', 'integration', 'run-install-ios.ts')
34
+ runner = path.join(process.cwd(), 'test', 'device', 'manual', 'manage', 'install_ios.manual.ts')
35
35
  } else {
36
36
  console.error('Cannot determine platform for project:', project)
37
37
  process.exit(3)
@@ -39,7 +39,7 @@ if (isAndroidDir(project)) {
39
39
 
40
40
  if (!runner) process.exit(4)
41
41
 
42
- const proc = spawn('node', [runner, project, ...(deviceId ? [deviceId] : [])], { stdio: ['ignore', 'pipe', 'inherit'] })
42
+ const proc = spawn('tsx', [runner, project, ...(deviceId ? [deviceId] : [])], { stdio: ['ignore', 'pipe', 'inherit'] })
43
43
  let stdout = ''
44
44
  proc.stdout?.on('data', (c) => { stdout += c.toString() })
45
45
  proc.on('close', (code) => {
@@ -4,7 +4,7 @@ import { AndroidManage } from '../../../dist/utils/android/manage.js'
4
4
  async function main() {
5
5
  const [, , appPath, deviceId] = process.argv
6
6
  if (!appPath) {
7
- console.error('Usage: node test/integration/run-install-android.ts <apk-or-project-dir> [deviceId]')
7
+ console.error('Usage: node test/device/manual/manage/install_android.manual.ts <apk-or-project-dir> [deviceId]')
8
8
  process.exit(1)
9
9
  }
10
10
 
@@ -4,7 +4,7 @@ import { iOSManage } from '../../../dist/utils/ios/manage.js'
4
4
  async function main() {
5
5
  const [, , appPath, deviceId] = process.argv
6
6
  if (!appPath) {
7
- console.error('Usage: node test/integration/run-install-ios.ts <.app-or-project-dir> [deviceId]')
7
+ console.error('Usage: node test/device/manual/manage/install_ios.manual.ts <.app-or-project-dir> [deviceId]')
8
8
  process.exit(1)
9
9
  }
10
10
 
@@ -0,0 +1,29 @@
1
+ import { ToolsObserve } from '../../../src/observe/index.js'
2
+
3
+ function readArg(flag: string): string | undefined {
4
+ const index = process.argv.indexOf(flag)
5
+ if (index === -1) return undefined
6
+ return process.argv[index + 1]
7
+ }
8
+
9
+ async function main() {
10
+ const platform = (readArg('--platform') || process.argv[2] || 'android') as 'android' | 'ios'
11
+ const deviceId = readArg('--id') || readArg('--deviceId') || process.argv[3]
12
+
13
+ const result = await ToolsObserve.captureScreenshotHandler({ platform, deviceId })
14
+ const screenshot = (result as any).screenshot || ''
15
+ const fallback = (result as any).screenshot_fallback || ''
16
+
17
+ console.log(JSON.stringify({
18
+ device: result.device,
19
+ resolution: result.resolution,
20
+ mimeType: (result as any).screenshot_mime || 'image/png',
21
+ screenshotBytes: screenshot ? Buffer.from(screenshot, 'base64').length : 0,
22
+ fallbackBytes: fallback ? Buffer.from(fallback, 'base64').length : 0
23
+ }))
24
+ }
25
+
26
+ main().catch((error) => {
27
+ console.error(error instanceof Error ? error.message : String(error))
28
+ process.exit(1)
29
+ })
@@ -1,4 +1,4 @@
1
- import { ToolsObserve } from '../../src/observe/index.js'
1
+ import { ToolsObserve } from '../../../src/observe/index.js'
2
2
  import minimist from 'minimist'
3
3
 
4
4
  async function main() {
@@ -0,0 +1,29 @@
1
+ import { ToolsObserve } from '../../../src/observe/index.js'
2
+
3
+ function readArg(flag: string): string | undefined {
4
+ const index = process.argv.indexOf(flag)
5
+ if (index === -1) return undefined
6
+ return process.argv[index + 1]
7
+ }
8
+
9
+ async function main() {
10
+ const platform = (readArg('--platform') || process.argv[2] || 'android') as 'android' | 'ios'
11
+ const deviceId = readArg('--id') || readArg('--deviceId') || process.argv[3]
12
+
13
+ const result = await ToolsObserve.getUITreeHandler({ platform, deviceId })
14
+ if ((result as any).error) throw new Error((result as any).error)
15
+
16
+ const firstElement = Array.isArray((result as any).elements) ? (result as any).elements[0] : null
17
+
18
+ console.log(JSON.stringify({
19
+ device: (result as any).device,
20
+ resolution: (result as any).resolution,
21
+ elementCount: Array.isArray((result as any).elements) ? (result as any).elements.length : 0,
22
+ hasCenterAndDepth: firstElement ? ('center' in firstElement && 'depth' in firstElement) : true
23
+ }))
24
+ }
25
+
26
+ main().catch((error) => {
27
+ console.error(error instanceof Error ? error.message : String(error))
28
+ process.exit(1)
29
+ })
@@ -1,4 +1,4 @@
1
- import { AndroidObserve } from '../../src/observe/index.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
 
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * Device E2E: get_screen_fingerprint
4
- * Usage: RUN_DEVICE_TESTS=true npx tsx run-screen-fingerprint.ts [android|ios] [deviceId]
4
+ * Usage: RUN_DEVICE_TESTS=true npx tsx test/device/manual/observe/screen_fingerprint.manual.ts [android|ios] [deviceId]
5
5
  */
6
6
 
7
7
  import { AndroidObserve } from '../../../src/observe/index.js'
@@ -2,7 +2,7 @@
2
2
  import { AndroidInteract } from '../../../dist/interact/index.js'
3
3
 
4
4
 
5
- // Usage: tsx test/device/observe/run-scroll-test-android.ts <deviceId> <appId> <selectorText>
5
+ // Usage: tsx test/device/manual/observe/scroll_to_element_android.manual.ts <deviceId> <appId> <selectorText>
6
6
  const args = process.argv.slice(2)
7
7
  const DEVICE_ID = args[0] || process.env.DEVICE_ID || 'emulator-5554'
8
8
  const SELECTOR = args[2] || process.env.SELECTOR || 'Generate Session'
@@ -2,15 +2,15 @@
2
2
  * Test script for verify UI Tree functionality.
3
3
  *
4
4
  * Usage:
5
- * npx tsx test-ui-tree.ts [android|ios] [deviceId]
5
+ * npx tsx test/device/manual/observe/ui_tree.manual.ts [android|ios] [deviceId]
6
6
  *
7
7
  * Examples:
8
- * npx tsx test-ui-tree.ts android
9
- * npx tsx test-ui-tree.ts ios booted
8
+ * npx tsx test/device/manual/observe/ui_tree.manual.ts android
9
+ * npx tsx test/device/manual/observe/ui_tree.manual.ts ios booted
10
10
  */
11
11
 
12
- import { AndroidObserve } from '../../src/observe/index.js';
13
- import { iOSObserve } from '../../src/observe/index.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);
@@ -67,7 +67,7 @@ async function main() {
67
67
  console.log(`- Elements with text: ${withText}`);
68
68
  }
69
69
 
70
- } catch {
70
+ } catch (error) {
71
71
  console.error("\n❌ Test Failed:", error);
72
72
  process.exit(1);
73
73
  }
@@ -1,30 +1,50 @@
1
- // Aggregator entrypoint for unit tests (updated to new test layout)
2
- (async function() {
3
- // Core unit tests that do not require real devices
4
- await import('../observe/unit/logparse.test.ts')
5
- await import('../observe/unit/logstream.test.ts')
6
- await import('../observe/unit/get_screen_fingerprint.test.ts')
7
-
8
- await import('../manage/unit/install.test.ts')
9
- await import('../manage/unit/build.test.ts')
10
- await import('../manage/unit/build_and_install.test.ts')
11
- await import('../manage/unit/diagnostics.test.ts')
12
- await import('../manage/unit/detection.test.ts')
13
- await import('../manage/unit/mcp_disable_autodetect.test.ts')
14
- await import('../interact/unit/wait_for_screen_change.test.ts')
15
-
16
- // Conditionally include device-dependent unit tests. Set SKIP_DEVICE_TESTS=1 to exclude.
17
- if (process.env.SKIP_DEVICE_TESTS !== '1') {
18
- try {
19
- await import('../observe/unit/capture_debug_snapshot.test.ts')
20
- await import('../observe/unit/find_element.test.ts')
21
- await import('../interact/unit/wait_for_ui.test.ts')
22
- } catch (e) {
23
- console.warn('Skipping some device-dependent tests due to import error:', e instanceof Error ? e.message : String(e))
24
- }
25
- } else {
26
- console.log('SKIP_DEVICE_TESTS=1 detected - skipping device-dependent unit tests')
1
+ import { readdir } from 'fs/promises'
2
+ import path from 'path'
3
+ import { spawn } from 'child_process'
4
+ import { fileURLToPath } from 'url'
5
+
6
+ const unitRoot = fileURLToPath(new URL('.', import.meta.url))
7
+
8
+ async function collectFiles(dir: string): Promise<string[]> {
9
+ const entries = await readdir(dir, { withFileTypes: true })
10
+ const files = await Promise.all(entries.map(async (entry) => {
11
+ const fullPath = path.join(dir, entry.name)
12
+ if (entry.isDirectory()) return collectFiles(fullPath)
13
+ return fullPath
14
+ }))
15
+
16
+ return files.flat()
17
+ }
18
+
19
+ async function runFile(file: string): Promise<void> {
20
+ const relativePath = path.relative(unitRoot, file).replaceAll(path.sep, '/')
21
+ console.log(`Running unit test: ${relativePath}`)
22
+
23
+ await new Promise<void>((resolve, reject) => {
24
+ const child = spawn('tsx', [file], { stdio: 'inherit' })
25
+ child.on('error', reject)
26
+ child.on('close', (code) => {
27
+ if (code === 0) {
28
+ resolve()
29
+ return
30
+ }
31
+
32
+ reject(new Error(`Unit test failed: ${relativePath} (exit code ${code ?? 'unknown'})`))
33
+ })
34
+ })
35
+ }
36
+
37
+ (async function () {
38
+ const allFiles = (await collectFiles(unitRoot))
39
+ .filter((file) => file.endsWith('.test.ts') && path.basename(file) !== 'index.ts')
40
+ .sort()
41
+
42
+ for (const file of allFiles) {
43
+ await runFile(file)
27
44
  }
28
45
 
29
46
  console.log('Unit tests loaded.')
30
- })().catch(e => { console.error(e); process.exit(1) })
47
+ })().catch((error) => {
48
+ console.error(error instanceof Error ? error.message : String(error))
49
+ process.exit(1)
50
+ })
@@ -0,0 +1,55 @@
1
+ import assert from 'assert'
2
+ import { ToolsInteract, AndroidInteract } from '../../../src/interact/index.js'
3
+
4
+ async function run() {
5
+ const originalMockDevices = process.env.MCP_TEST_MOCK_DEVICES
6
+ const originalTap = AndroidInteract.prototype.tap
7
+ const originalScrollToElement = AndroidInteract.prototype.scrollToElement
8
+
9
+ process.env.MCP_TEST_MOCK_DEVICES = '1'
10
+
11
+ try {
12
+ AndroidInteract.prototype.tap = async function (x: number, y: number, deviceId?: string) {
13
+ return {
14
+ device: { platform: 'android', id: deviceId || 'mock', osVersion: '14', model: 'Pixel', simulator: true },
15
+ success: true,
16
+ x,
17
+ y
18
+ } as any
19
+ }
20
+
21
+ const tapResponse = await ToolsInteract.tapHandler({ platform: 'android', x: 10, y: 20 })
22
+ assert.strictEqual(tapResponse.success, true)
23
+ assert.strictEqual(typeof tapResponse.device.id, 'string')
24
+ assert.strictEqual(tapResponse.x, 10)
25
+ assert.strictEqual(tapResponse.y, 20)
26
+
27
+ AndroidInteract.prototype.scrollToElement = async function () {
28
+ return {
29
+ success: true,
30
+ element: { text: 'Settings', bounds: [0, 0, 100, 50] },
31
+ scrollsPerformed: 2
32
+ } as any
33
+ }
34
+
35
+ const scrollResponse = await ToolsInteract.scrollToElementHandler({
36
+ platform: 'android',
37
+ selector: { text: 'Settings' }
38
+ })
39
+ assert.strictEqual(scrollResponse.success, true)
40
+ assert.strictEqual(scrollResponse.scrollsPerformed, 2)
41
+ assert.strictEqual(scrollResponse.element.text, 'Settings')
42
+
43
+ console.log('interact handler shape tests passed')
44
+ } finally {
45
+ AndroidInteract.prototype.tap = originalTap
46
+ AndroidInteract.prototype.scrollToElement = originalScrollToElement
47
+ if (typeof originalMockDevices === 'undefined') delete process.env.MCP_TEST_MOCK_DEVICES
48
+ else process.env.MCP_TEST_MOCK_DEVICES = originalMockDevices
49
+ }
50
+ }
51
+
52
+ run().catch((error) => {
53
+ console.error(error)
54
+ process.exit(1)
55
+ })