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.
- package/README.md +2 -2
- package/dist/android/interact.js +13 -1
- package/dist/android/observe.js +13 -0
- package/dist/cli/ios/run-ios-smoke.js +2 -2
- package/dist/cli/ios/run-ios-ui-tree-tap.js +2 -2
- package/dist/interact/android.js +91 -0
- package/dist/interact/index.js +37 -0
- package/dist/interact/ios.js +120 -0
- package/dist/interact/shared/fingerprint.js +72 -0
- package/dist/interact/shared/scroll_to_element.js +98 -0
- package/dist/ios/interact.js +52 -1
- package/dist/ios/observe.js +12 -0
- package/dist/manage/android.js +162 -0
- package/dist/manage/index.js +364 -0
- package/dist/manage/ios.js +353 -0
- package/dist/observe/android.js +351 -0
- package/dist/observe/fingerprint.js +1 -0
- package/dist/observe/index.js +85 -0
- package/dist/observe/ios.js +320 -0
- package/dist/observe/test/device/logstream-real.js +34 -0
- package/dist/observe/test/device/run-screen-fingerprint.js +29 -0
- package/dist/observe/test/device/run-scroll-test-android.js +22 -0
- package/dist/observe/test/device/test-ui-tree.js +67 -0
- package/dist/observe/test/device/wait_for_element_real.js +69 -0
- package/dist/observe/test/unit/get_screen_fingerprint.test.js +54 -0
- package/dist/observe/test/unit/logparse.test.js +39 -0
- package/dist/observe/test/unit/logstream.test.js +41 -0
- package/dist/observe/test/unit/scroll_to_element.test.js +113 -0
- package/dist/observe/test/unit/wait_for_element_mock.js +92 -0
- package/dist/server.js +54 -9
- package/dist/shared/fingerprint.js +72 -0
- package/dist/shared/scroll_to_element.js +98 -0
- package/dist/tools/interact.js +19 -22
- package/dist/tools/manage.js +2 -2
- package/dist/tools/observe.js +45 -43
- package/dist/tools/scroll_to_element.js +98 -0
- package/dist/utils/android/utils.js +429 -0
- package/dist/utils/cli/idb/check-idb.js +84 -0
- package/dist/utils/cli/idb/idb-helper.js +91 -0
- package/dist/utils/cli/idb/install-idb.js +82 -0
- package/dist/utils/cli/ios/preflight-ios.js +155 -0
- package/dist/utils/cli/ios/run-ios-smoke.js +28 -0
- package/dist/utils/cli/ios/run-ios-ui-tree-tap.js +29 -0
- package/dist/utils/diagnostics.js +1 -1
- package/dist/utils/ios/utils.js +301 -0
- package/dist/utils/resolve-device.js +2 -2
- package/docs/CHANGELOG.md +11 -0
- package/docs/tools/TOOLS.md +3 -3
- package/docs/tools/interact.md +31 -0
- package/docs/tools/observe.md +24 -0
- package/package.json +1 -1
- package/src/{android/interact.ts → interact/android.ts} +15 -2
- package/src/interact/index.ts +47 -0
- package/src/{ios/interact.ts → interact/ios.ts} +58 -3
- package/src/interact/shared/fingerprint.ts +73 -0
- package/src/interact/shared/scroll_to_element.ts +110 -0
- package/src/{android/manage.ts → manage/android.ts} +2 -2
- package/src/{tools/manage.ts → manage/index.ts} +7 -4
- package/src/{ios/manage.ts → manage/ios.ts} +1 -1
- package/src/{android/observe.ts → observe/android.ts} +14 -26
- package/src/observe/index.ts +92 -0
- package/src/{ios/observe.ts → observe/ios.ts} +17 -35
- package/src/server.ts +57 -10
- package/src/{android → utils/android}/utils.ts +2 -2
- package/src/{cli → utils/cli}/ios/run-ios-smoke.ts +2 -2
- package/src/{cli → utils/cli}/ios/run-ios-ui-tree-tap.ts +3 -3
- package/src/utils/diagnostics.ts +1 -1
- package/src/{ios → utils/ios}/utils.ts +2 -2
- package/src/utils/resolve-device.ts +2 -2
- package/test/{device/interact → interact/device}/smoke-test.ts +3 -4
- package/test/{device/manage → manage/device}/run-install-android.ts +1 -1
- package/test/{device/manage → manage/device}/run-install-ios.ts +1 -1
- package/test/{device/manage → manage/device}/run-install-kmp.ts +1 -1
- package/test/{unit/manage → manage/unit}/build.test.ts +1 -1
- package/test/{unit/manage → manage/unit}/build_and_install.test.ts +1 -1
- package/test/{unit/manage → manage/unit}/detection.test.ts +1 -1
- package/test/{unit/manage → manage/unit}/diagnostics.test.ts +2 -2
- package/test/{unit/manage → manage/unit}/install.test.ts +1 -1
- package/test/{unit/manage → manage/unit}/mcp_disable_autodetect.test.ts +1 -1
- package/test/{device/observe → observe/device}/logstream-real.ts +1 -1
- package/test/observe/device/run-screen-fingerprint.ts +36 -0
- package/test/observe/device/run-scroll-test-android.ts +24 -0
- package/test/{device/observe → observe/device}/test-ui-tree.ts +2 -2
- package/test/{device/observe → observe/device}/wait_for_element_real.ts +2 -2
- package/test/observe/unit/get_screen_fingerprint.test.ts +69 -0
- package/test/{unit/observe → observe/unit}/logparse.test.ts +1 -1
- package/test/{unit/observe → observe/unit}/logstream.test.ts +1 -1
- package/test/observe/unit/scroll_to_element.test.ts +129 -0
- package/test/{unit/observe → observe/unit}/wait_for_element_mock.ts +3 -3
- package/test/unit/index.ts +12 -11
- package/src/tools/interact.ts +0 -45
- package/src/tools/observe.ts +0 -82
- package/test/device/README.md +0 -49
- package/test/device/index.ts +0 -27
- package/test/device/utils/test-dist.ts +0 -41
- package/test/unit/utils/detect-java.test.ts +0 -22
- /package/src/{cli → utils/cli}/idb/check-idb.ts +0 -0
- /package/src/{cli → utils/cli}/idb/idb-helper.ts +0 -0
- /package/src/{cli → utils/cli}/idb/install-idb.ts +0 -0
- /package/src/{cli → utils/cli}/ios/preflight-ios.ts +0 -0
- /package/test/{device/interact → interact/device}/run-real-test.ts +0 -0
- /package/test/{device/manage → manage/device}/install.integration.ts +0 -0
- /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 "
|
|
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 '../
|
|
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 '
|
|
2
|
-
import { iOSManage } from '
|
|
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 '
|
|
2
|
-
import { iOSInteract } from '
|
|
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
|
|
package/src/utils/diagnostics.ts
CHANGED
|
@@ -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 '
|
|
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 "
|
|
2
|
+
import { DeviceInfo } from "../../types.js"
|
|
3
3
|
import { promises as fsPromises } from 'fs'
|
|
4
4
|
import path from 'path'
|
|
5
|
-
import { makeEnvSnapshot } from '../
|
|
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 "
|
|
3
|
-
import { listIOSDevices } from "
|
|
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/
|
|
2
|
-
import { AndroidInteract } from "../../src/
|
|
3
|
-
import {
|
|
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();
|
|
@@ -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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
|
@@ -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/
|
|
13
|
-
import { iOSObserve } from '../../src/
|
|
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/
|
|
2
|
-
import { AndroidObserve } from "../../src/
|
|
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,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/
|
|
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/
|
|
2
|
-
import { AndroidObserve } from '../../../src/
|
|
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 <
|
|
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;
|
package/test/unit/index.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
// Aggregator entrypoint for unit tests
|
|
2
|
-
import '
|
|
3
|
-
import '
|
|
4
|
-
import '
|
|
5
|
-
import '
|
|
6
|
-
|
|
7
|
-
import '
|
|
8
|
-
import '
|
|
9
|
-
import '
|
|
10
|
-
import '
|
|
11
|
-
import '
|
|
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.')
|
package/src/tools/interact.ts
DELETED
|
@@ -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
|
-
|