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.
- package/AGENTS.md +74 -0
- package/README.md +24 -5
- package/dist/interact/index.js +220 -13
- package/dist/observe/ios.js +10 -3
- package/dist/server-core.js +707 -0
- package/dist/server.js +6 -693
- package/dist/utils/resolve-device.js +15 -3
- package/docs/CHANGELOG.md +6 -1
- package/docs/tools/interact.md +69 -30
- package/package.json +3 -3
- package/skills/README.md +35 -0
- package/skills/test-authoring/SKILL.md +57 -0
- package/skills/test-authoring/references/repo-test-layout.md +47 -0
- package/skills/test-authoring/references/test-authoring-workflow.md +73 -0
- package/skills/test-authoring/references/test-quality-checklist.md +39 -0
- package/src/interact/index.ts +250 -13
- package/src/observe/ios.ts +12 -3
- package/src/server-core.ts +762 -0
- package/src/server.ts +8 -754
- package/src/types.ts +10 -1
- package/src/utils/resolve-device.ts +19 -3
- package/test/device/automated/observe/capture_screenshot.android.smoke.ts +30 -0
- package/test/device/automated/observe/capture_screenshot.ios.smoke.ts +30 -0
- package/test/{observe/device → device/automated/observe}/get_logs.android.smoke.ts +1 -1
- package/test/{observe/device → device/automated/observe}/get_logs.ios.smoke.ts +1 -1
- package/test/device/automated/observe/get_ui_tree.android.smoke.ts +31 -0
- package/test/device/automated/observe/get_ui_tree.ios.smoke.ts +31 -0
- package/test/device/index.ts +52 -0
- package/test/{interact/device/smoke-test.ts → device/manual/interact/app_lifecycle.manual.ts} +5 -5
- package/test/{manage/device/run-build-install-ios.ts → device/manual/manage/build_install_ios.manual.ts} +1 -1
- package/test/{manage/device → device/manual/manage}/install.integration.ts +6 -6
- package/test/{manage/device/run-install-android.ts → device/manual/manage/install_android.manual.ts} +1 -1
- package/test/{manage/device/run-install-ios.ts → device/manual/manage/install_ios.manual.ts} +1 -1
- package/test/device/manual/observe/capture_screenshot.manual.ts +29 -0
- package/test/{helpers/run-get-logs.ts → device/manual/observe/get_logs.manual.ts} +1 -1
- package/test/device/manual/observe/get_ui_tree.manual.ts +29 -0
- package/test/{observe/device/logstream-real.ts → device/manual/observe/logstream.manual.ts} +1 -1
- package/test/{observe/device/run-screen-fingerprint.ts → device/manual/observe/screen_fingerprint.manual.ts} +1 -1
- package/test/{observe/device/run-scroll-test-android.ts → device/manual/observe/scroll_to_element_android.manual.ts} +1 -1
- package/test/{observe/device/test-ui-tree.ts → device/manual/observe/ui_tree.manual.ts} +6 -6
- package/test/unit/index.ts +47 -27
- package/test/unit/interact/handler_shapes.test.ts +55 -0
- package/test/unit/interact/tap_element.test.ts +170 -0
- package/test/unit/interact/wait_for_screen_change.test.ts +34 -0
- package/test/{interact/unit → unit/interact}/wait_for_ui_contract.test.ts +11 -10
- package/test/unit/interact/wait_for_ui_selector_matching.test.ts +76 -0
- package/test/unit/manage/handler_shapes.test.ts +43 -0
- package/test/{observe/unit → unit/observe}/capture_debug_snapshot.test.ts +5 -1
- package/test/{observe/unit → unit/observe}/find_element.test.ts +12 -6
- package/test/unit/observe/get_screen_fingerprint.test.ts +71 -0
- package/test/unit/observe/ios-getlogs.test.ts +53 -0
- package/test/unit/observe/scroll_to_element.test.ts +127 -0
- package/test/unit/server/contract.test.ts +45 -0
- package/test/unit/server/response_shapes.test.ts +93 -0
- package/test/unit/system/adb_version.test.ts +35 -0
- package/test/unit/system/get_system_status.test.ts +20 -0
- package/test/unit/system/system_status.test.ts +141 -0
- package/test/{utils → unit/utils}/detect_java.test.ts +1 -1
- package/test/unit/utils/exec.test.ts +51 -0
- package/test/unit/utils/resolve_device.test.ts +63 -0
- package/tsconfig.json +2 -2
- package/test/interact/device/run-real-test.ts +0 -3
- package/test/interact/unit/wait_for_screen_change.test.ts +0 -32
- package/test/interact/unit/wait_for_ui.test.ts +0 -76
- package/test/interact/unit/wait_for_ui_new.test.ts +0 -57
- package/test/observe/device/wait_for_element_real.ts +0 -3
- package/test/observe/unit/get_screen_fingerprint.test.ts +0 -69
- package/test/observe/unit/ios-getlogs.test.ts +0 -67
- package/test/observe/unit/scroll_to_element.test.ts +0 -129
- package/test/observe/unit/wait_for_element_mock.ts +0 -2
- package/test/observe/unit/wait_for_ui_edge_cases.test.ts +0 -41
- package/test/observe/unit/wait_for_ui_stability.test.ts +0 -30
- package/test/system/adb_version.test.ts +0 -25
- package/test/system/get_system_status.test.ts +0 -52
- package/test/system/system_status.test.ts +0 -109
- /package/test/{manage/unit → unit/manage}/build.test.ts +0 -0
- /package/test/{manage/unit → unit/manage}/build_and_install.test.ts +0 -0
- /package/test/{manage/unit → unit/manage}/detection.test.ts +0 -0
- /package/test/{manage/unit → unit/manage}/diagnostics.test.ts +0 -0
- /package/test/{manage/unit → unit/manage}/install.test.ts +0 -0
- /package/test/{manage/unit → unit/manage}/mcp_disable_autodetect.test.ts +0 -0
- /package/test/{observe/unit → unit/observe}/get_logs.test.ts +0 -0
- /package/test/{observe/unit → unit/observe}/logparse.test.ts +0 -0
- /package/test/{observe/unit → unit/observe}/logstream.test.ts +0 -0
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { ToolsInteract } from '../../../src/interact/index.js'
|
|
2
|
-
import * as Observe from '../../../src/observe/index.js'
|
|
3
|
-
|
|
4
|
-
async function runTests() {
|
|
5
|
-
console.log('Starting wait_for_ui unit tests...')
|
|
6
|
-
|
|
7
|
-
const origFind = (ToolsInteract as any).findElementHandler
|
|
8
|
-
const origReadLog = (Observe as any).ToolsObserve.readLogStreamHandler
|
|
9
|
-
const origGetLogs = (Observe as any).ToolsObserve.getLogsHandler
|
|
10
|
-
const origGetFp = (Observe as any).ToolsObserve.getScreenFingerprintHandler
|
|
11
|
-
const origResolveObserve = (Observe as any).ToolsObserve.resolveObserve
|
|
12
|
-
const origGetScreenFp = (Observe as any).ToolsObserve.getScreenFingerprintHandler
|
|
13
|
-
|
|
14
|
-
try {
|
|
15
|
-
// Timeout / snapshot case: ensure snapshot captured when condition not met
|
|
16
|
-
const origCapture = (Observe as any).ToolsObserve.captureDebugSnapshotHandler
|
|
17
|
-
;(Observe as any).ToolsObserve.captureDebugSnapshotHandler = async ({ reason }: any) => ({ reason, fingerprint: 'snap-123', ui_tree: null, logs: [] })
|
|
18
|
-
// make findElement always fail
|
|
19
|
-
(ToolsInteract as any).findElementHandler = async () => ({ found: false })
|
|
20
|
-
const resTimeout = await ToolsInteract.waitForUIHandler({ type: 'ui', query: 'WillNeverExist', timeoutMs: 500, pollIntervalMs: 100, platform: 'android' })
|
|
21
|
-
const okTimeout = resTimeout && !(resTimeout as any).success && (resTimeout as any).snapshot && (resTimeout as any).snapshot.fingerprint === 'snap-123' && (resTimeout as any).telemetry && (resTimeout as any).telemetry.pollCount > 0
|
|
22
|
-
console.log('Timeout Snapshot Test:', okTimeout ? 'PASS' : 'FAIL', JSON.stringify((resTimeout as any).telemetry || {}, null, 2))
|
|
23
|
-
;(Observe as any).ToolsObserve.captureDebugSnapshotHandler = origCapture
|
|
24
|
-
|
|
25
|
-
// UI condition: findElement returns found on 2nd call
|
|
26
|
-
let calls = 0
|
|
27
|
-
;(ToolsInteract as any).findElementHandler = async (args) => {
|
|
28
|
-
calls++
|
|
29
|
-
const query = (args && (args.query || args)) || ''
|
|
30
|
-
if (calls >= 2) return { found: true, element: { text: query } }
|
|
31
|
-
return { found: false }
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const resUi = await ToolsInteract.waitForUIHandler({ type: 'ui', query: 'Generate Session', timeoutMs: 3000, pollIntervalMs: 100, platform: 'android' })
|
|
35
|
-
const okUi = resUi && (resUi as any).success && (resUi as any).telemetry && (resUi as any).telemetry.pollCount > 0 && (resUi as any).telemetry.timeToMatch >= 0
|
|
36
|
-
console.log('UI Test:', okUi ? 'PASS' : 'FAIL', JSON.stringify((resUi as any).telemetry || {}, null, 2))
|
|
37
|
-
|
|
38
|
-
// Log condition: stream empty, snapshot contains matching line
|
|
39
|
-
;(Observe as any).ToolsObserve.readLogStreamHandler = async () => ({ entries: [ { message: 'nothing' } ] })
|
|
40
|
-
let glCalls = 0
|
|
41
|
-
;(Observe as any).ToolsObserve.getLogsHandler = async () => {
|
|
42
|
-
glCalls++
|
|
43
|
-
if (glCalls === 1) return { device: {}, logs: ['INFO start'] }
|
|
44
|
-
return { device: {}, logs: ['INFO start', 'ERROR Exception occurred', 'Server: Boom'] }
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const resLog = await ToolsInteract.waitForUIHandler({ type: 'log', query: 'Server', timeoutMs: 3000, pollIntervalMs: 100, platform: 'android' })
|
|
48
|
-
const okLog = resLog && (resLog as any).success && (resLog as any).telemetry && (resLog as any).telemetry.pollCount > 0 && (resLog as any).telemetry.matchSource === 'log-snapshot'
|
|
49
|
-
console.log('Log Test:', okLog ? 'PASS' : 'FAIL', JSON.stringify((resLog as any).telemetry || {}, null, 2))
|
|
50
|
-
|
|
51
|
-
// Screen condition: fingerprint changes after a few polls
|
|
52
|
-
let seq = ['A', 'A', 'B']
|
|
53
|
-
;(Observe as any).ToolsObserve.getScreenFingerprintHandler = async () => ({ fingerprint: seq.length ? seq.shift() : null })
|
|
54
|
-
const resScreen = await ToolsInteract.waitForUIHandler({ type: 'screen', timeoutMs: 3000, pollIntervalMs: 100, platform: 'android' })
|
|
55
|
-
const okScreen = resScreen && (resScreen as any).success && (resScreen as any).telemetry && (resScreen as any).telemetry.matchSource === 'screen-fingerprint'
|
|
56
|
-
console.log('Screen Test:', okScreen ? 'PASS' : 'FAIL', JSON.stringify((resScreen as any).telemetry || {}, null, 2))
|
|
57
|
-
|
|
58
|
-
// Idle condition: stable fingerprints observed
|
|
59
|
-
let idleSeq = ['X', 'X', 'X']
|
|
60
|
-
;(Observe as any).ToolsObserve.getScreenFingerprintHandler = async () => ({ fingerprint: idleSeq.length ? idleSeq.shift() : 'X' })
|
|
61
|
-
const resIdle = await ToolsInteract.waitForUIHandler({ type: 'idle', timeoutMs: 3000, pollIntervalMs: 100, platform: 'android' })
|
|
62
|
-
const okIdle = resIdle && (resIdle as any).success && (resIdle as any).telemetry && (resIdle as any).telemetry.matchSource === 'idle-stable'
|
|
63
|
-
console.log('Idle Test:', okIdle ? 'PASS' : 'FAIL', JSON.stringify((resIdle as any).telemetry || {}, null, 2))
|
|
64
|
-
|
|
65
|
-
} finally {
|
|
66
|
-
;(ToolsInteract as any).findElementHandler = origFind
|
|
67
|
-
;(Observe as any).ToolsObserve.readLogStreamHandler = origReadLog
|
|
68
|
-
;(Observe as any).ToolsObserve.getLogsHandler = origGetLogs
|
|
69
|
-
;(Observe as any).ToolsObserve.getScreenFingerprintHandler = origGetFp
|
|
70
|
-
;(Observe as any).ToolsObserve.resolveObserve = origResolveObserve
|
|
71
|
-
;(Observe as any).ToolsObserve.getScreenFingerprintHandler = origGetScreenFp
|
|
72
|
-
;(Observe as any).ToolsObserve.getScreenFingerprintHandler = origGetScreenFp
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
runTests().catch(console.error)
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { ToolsInteract } from '../../../src/interact/index.js'
|
|
2
|
-
import * as Observe from '../../../src/observe/index.js'
|
|
3
|
-
|
|
4
|
-
async function run() {
|
|
5
|
-
console.log('Starting new wait_for_ui unit tests...')
|
|
6
|
-
const origGetUITree = (Observe as any).ToolsObserve.getUITreeHandler
|
|
7
|
-
|
|
8
|
-
try {
|
|
9
|
-
// Test 1: exact text match -> exists
|
|
10
|
-
(Observe as any).ToolsObserve.getUITreeHandler = async () => ({ elements: [ { text: 'Hello', resourceId: 'rid1', contentDescription: 'acc1', type: 'Button', bounds: [0,0,10,10], visible: true, clickable: false, enabled: true } ] })
|
|
11
|
-
const r1 = await ToolsInteract.waitForUIHandler({ selector: { text: 'Hello' }, condition: 'exists', timeout_ms: 1000, poll_interval_ms: 50, platform: 'android' })
|
|
12
|
-
const ok1 = r1 && r1.status === 'success' && r1.matched === 1 && r1.element && r1.element.text === 'Hello'
|
|
13
|
-
console.log('Exact match exists:', ok1 ? 'PASS' : 'FAIL', JSON.stringify(r1, null, 2))
|
|
14
|
-
|
|
15
|
-
// Test 2: contains matching
|
|
16
|
-
(Observe as any).ToolsObserve.getUITreeHandler = async () => ({ elements: [ { text: 'Welcome User', resourceId: 'rid2', contentDescription: 'acc2', type: 'TextView', bounds: [0,0,50,10], visible: true } ] })
|
|
17
|
-
const r2 = await ToolsInteract.waitForUIHandler({ selector: { text: 'User', contains: true }, condition: 'exists', timeout_ms: 1000, poll_interval_ms: 50, platform: 'android' })
|
|
18
|
-
const ok2 = r2 && r2.status === 'success' && r2.matched === 1 && r2.element && r2.element.text && r2.element.text.includes('Welcome')
|
|
19
|
-
console.log('Contains match:', ok2 ? 'PASS' : 'FAIL', JSON.stringify(r2, null, 2))
|
|
20
|
-
|
|
21
|
-
// Test 3: visible condition
|
|
22
|
-
(Observe as any).ToolsObserve.getUITreeHandler = async () => ({ elements: [ { text: 'Hidden', resourceId: 'rid3', bounds: [0,0,0,0], visible: false } ] })
|
|
23
|
-
const r3 = await ToolsInteract.waitForUIHandler({ selector: { text: 'Hidden' }, condition: 'visible', timeout_ms: 300, poll_interval_ms: 50, platform: 'android' })
|
|
24
|
-
const ok3 = r3 && r3.status === 'timeout' && r3.error && r3.error.code === 'ELEMENT_NOT_FOUND'
|
|
25
|
-
console.log('Visible negative (hidden element):', ok3 ? 'PASS' : 'FAIL', JSON.stringify(r3, null, 2))
|
|
26
|
-
|
|
27
|
-
// Test 4: clickable condition
|
|
28
|
-
(Observe as any).ToolsObserve.getUITreeHandler = async () => ({ elements: [ { text: 'TapMe', resourceId: 'rid4', bounds: [0,0,20,20], visible: true, clickable: true, enabled: true } ] })
|
|
29
|
-
const r4 = await ToolsInteract.waitForUIHandler({ selector: { text: 'TapMe' }, condition: 'clickable', timeout_ms: 1000, poll_interval_ms: 50, platform: 'android' })
|
|
30
|
-
const ok4 = r4 && r4.status === 'success' && r4.matched === 1 && r4.element && r4.element.index === 0
|
|
31
|
-
console.log('Clickable match:', ok4 ? 'PASS' : 'FAIL', JSON.stringify(r4, null, 2))
|
|
32
|
-
|
|
33
|
-
// Test 5: retry behavior - first attempt times out, second attempt succeeds
|
|
34
|
-
const start = Date.now()
|
|
35
|
-
let seqTree = async () => {
|
|
36
|
-
const now = Date.now()
|
|
37
|
-
// for first ~400ms return no elements, afterwards return match
|
|
38
|
-
if (now - start < 400) return { elements: [] }
|
|
39
|
-
return { elements: [ { text: 'Retried', resourceId: 'rid5', bounds: [0,0,10,10], visible: true } ] }
|
|
40
|
-
}
|
|
41
|
-
;(Observe as any).ToolsObserve.getUITreeHandler = seqTree
|
|
42
|
-
const r5 = await ToolsInteract.waitForUIHandler({ selector: { text: 'Retried' }, condition: 'exists', timeout_ms: 200, poll_interval_ms: 50, match: undefined, retry: { max_attempts: 3, backoff_ms: 150 }, platform: 'android' })
|
|
43
|
-
const ok5 = r5 && r5.status === 'success' && r5.metrics && r5.metrics.attempts >= 2
|
|
44
|
-
console.log('Retry behavior:', ok5 ? 'PASS' : 'FAIL', JSON.stringify(r5, null, 2))
|
|
45
|
-
|
|
46
|
-
// Test 6: timeout with no selector match -> correct error code
|
|
47
|
-
(Observe as any).ToolsObserve.getUITreeHandler = async () => ({ elements: [] })
|
|
48
|
-
const r6 = await ToolsInteract.waitForUIHandler({ selector: { text: 'Nope' }, condition: 'exists', timeout_ms: 300, poll_interval_ms: 50, retry: { max_attempts: 1 }, platform: 'android' })
|
|
49
|
-
const ok6 = r6 && r6.status === 'timeout' && r6.error && r6.error.code === 'ELEMENT_NOT_FOUND'
|
|
50
|
-
console.log('Timeout no match:', ok6 ? 'PASS' : 'FAIL', JSON.stringify(r6, null, 2))
|
|
51
|
-
|
|
52
|
-
} finally {
|
|
53
|
-
(Observe as any).ToolsObserve.getUITreeHandler = origGetUITree
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
run().catch(err => { console.error('wait_for_ui_new tests failed:', err); process.exit(1) })
|
|
@@ -1,69 +0,0 @@
|
|
|
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,67 +0,0 @@
|
|
|
1
|
-
import { iOSObserve } from '../../../src/observe/ios'
|
|
2
|
-
import assert from 'assert'
|
|
3
|
-
|
|
4
|
-
// Lightweight unit tests: verify predicate construction and meta extraction logic using internal functions
|
|
5
|
-
// Since getLogs executes xcrun, run tests in SKIP_DEVICE_TESTS=1 environment by stubbing execCommand where possible.
|
|
6
|
-
|
|
7
|
-
import * as iosUtils from '../../../src/utils/ios/utils'
|
|
8
|
-
|
|
9
|
-
function stubExecCommand(original: any, expectedArgsChecker: (args: string[]) => boolean, output: string) {
|
|
10
|
-
return async function (args: string[], deviceId?: string) {
|
|
11
|
-
if (!expectedArgsChecker(args)) throw new Error('Unexpected args: ' + JSON.stringify(args))
|
|
12
|
-
return { output, device: { platform: 'ios', id: deviceId || 'booted' } }
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
describe('iOS getLogs predicate and meta', () => {
|
|
17
|
-
let obs: iOSObserve
|
|
18
|
-
beforeEach(() => {
|
|
19
|
-
obs = new iOSObserve()
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
it('uses simple process name predicate when appId provided', async () => {
|
|
23
|
-
const bundle = 'com.ideamechanics.modul8'
|
|
24
|
-
// stub execCommand twice: first for pgrep, second for log show
|
|
25
|
-
const pgrepOutput = '12345\n'
|
|
26
|
-
const logOutput = '2026-03-31 09:21:20.085 Module[12345:678] <Info> Modul8: Test message'
|
|
27
|
-
|
|
28
|
-
const orig = (iosUtils as any).execCommand
|
|
29
|
-
try {
|
|
30
|
-
(iosUtils as any).execCommand = stubExecCommand(orig, (args) => args.includes('pgrep'), pgrepOutput)
|
|
31
|
-
// second replacement for the log show call
|
|
32
|
-
let called = false
|
|
33
|
-
(iosUtils as any).execCommand = async function (args: string[]) {
|
|
34
|
-
if (args.includes('pgrep')) return { output: pgrepOutput, device: { platform: 'ios', id: 'booted' } }
|
|
35
|
-
if (args.includes('log') && args.includes('show')) { called = true; return { output: logOutput, device: { platform: 'ios', id: 'booted' } } }
|
|
36
|
-
throw new Error('Unexpected args: ' + JSON.stringify(args))
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const res = await obs.getLogs({ appId: bundle, deviceId: 'booted' })
|
|
40
|
-
assert(res.meta.processNameUsed === 'modul8' || res.meta.processNameUsed === 'Modul8' || !!res.meta.processNameUsed)
|
|
41
|
-
assert(res.meta.detectedPid === 12345)
|
|
42
|
-
assert(res.source === 'pid')
|
|
43
|
-
assert(res.logCount === 1)
|
|
44
|
-
assert(res.logs[0].message.includes('Test message'))
|
|
45
|
-
assert(called, 'log show must have been called')
|
|
46
|
-
} finally {
|
|
47
|
-
(iosUtils as any).execCommand = orig
|
|
48
|
-
}
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
it('falls back to broad when no appId', async () => {
|
|
52
|
-
const logOutput = '2026-03-31 09:21:20.085 SomeOther[222:333] <Info> Other: Hello'
|
|
53
|
-
const orig = (iosUtils as any).execCommand
|
|
54
|
-
try {
|
|
55
|
-
(iosUtils as any).execCommand = async function (args: string[]) {
|
|
56
|
-
if (args.includes('log') && args.includes('show')) return { output: logOutput, device: { platform: 'ios', id: 'booted' } }
|
|
57
|
-
throw new Error('Unexpected args: ' + JSON.stringify(args))
|
|
58
|
-
}
|
|
59
|
-
const obs = new iOSObserve()
|
|
60
|
-
const res = await obs.getLogs({ deviceId: 'booted' })
|
|
61
|
-
assert(res.source === 'broad')
|
|
62
|
-
assert(res.logCount === 1)
|
|
63
|
-
} finally {
|
|
64
|
-
(iosUtils as any).execCommand = orig
|
|
65
|
-
}
|
|
66
|
-
})
|
|
67
|
-
})
|
|
@@ -1,129 +0,0 @@
|
|
|
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,41 +0,0 @@
|
|
|
1
|
-
import { ToolsInteract } from '../../../../src/interact/index.js'
|
|
2
|
-
import * as Observe from '../../../../src/observe/index.js'
|
|
3
|
-
|
|
4
|
-
async function run() {
|
|
5
|
-
console.log('Unit: wait_for_ui edge cases')
|
|
6
|
-
|
|
7
|
-
const origFind = (ToolsInteract as any).findElementHandler
|
|
8
|
-
const origFp = (Observe as any).ToolsObserve.getScreenFingerprintHandler
|
|
9
|
-
|
|
10
|
-
try {
|
|
11
|
-
// 1) Immediate absence should pass for match='absent'
|
|
12
|
-
(ToolsInteract as any).findElementHandler = async () => ({ found: false })
|
|
13
|
-
const r1 = await (ToolsInteract as any).waitForUIHandler({ type: 'ui', query: 'Nothing', timeoutMs: 2000, pollIntervalMs: 100, stability_ms: 200, match: 'absent', platform: 'android' })
|
|
14
|
-
console.log('Immediate absent test:', r1 && (r1 as any).success ? 'PASS' : 'FAIL', JSON.stringify({ poll_count: (r1 as any).poll_count, duration_ms: (r1 as any).duration_ms, stable_duration_ms: (r1 as any).stable_duration_ms, matchSource: (r1 as any).matchSource }, null, 2))
|
|
15
|
-
|
|
16
|
-
// 2) Boundary stability: condition becomes true and stays exactly long enough
|
|
17
|
-
// Use pollInterval 100ms and stability 300ms -> need ~3 consecutive trues
|
|
18
|
-
let seq2 = [false, true, true, true]
|
|
19
|
-
(ToolsInteract as any).findElementHandler = async () => ({ found: seq2.shift() ?? true })
|
|
20
|
-
const r2 = await (ToolsInteract as any).waitForUIHandler({ type: 'ui', query: 'Boundary', timeoutMs: 2000, pollIntervalMs: 100, stability_ms: 300, match: 'present', platform: 'android' })
|
|
21
|
-
console.log('Boundary stability test:', r2 && (r2 as any).success ? 'PASS' : 'FAIL', JSON.stringify({ poll_count: (r2 as any).poll_count, duration_ms: (r2 as any).duration_ms, stable_duration_ms: (r2 as any).stable_duration_ms, matchSource: (r2 as any).matchSource }, null, 2))
|
|
22
|
-
|
|
23
|
-
// 3) Long flicker that never stabilizes should timeout/fail
|
|
24
|
-
// Sequence toggles true/false repeatedly
|
|
25
|
-
let seq3 = [false, true, false, true, false, true, false]
|
|
26
|
-
(ToolsInteract as any).findElementHandler = async () => ({ found: seq3.shift() ?? false })
|
|
27
|
-
const r3 = await (ToolsInteract as any).waitForUIHandler({ type: 'ui', query: 'Flicker', timeoutMs: 1200, pollIntervalMs: 150, stability_ms: 400, match: 'present', platform: 'android' })
|
|
28
|
-
console.log('Long flicker timeout test:', !(r3 && (r3 as any).success) ? 'PASS' : 'FAIL', JSON.stringify({ poll_count: (r3 as any).poll_count, duration_ms: (r3 as any).duration_ms, stable_duration_ms: (r3 as any).stable_duration_ms, matchSource: (r3 as any).matchSource }, null, 2))
|
|
29
|
-
|
|
30
|
-
// 4) Very short stability requirement should pass quickly
|
|
31
|
-
(ToolsInteract as any).findElementHandler = async () => ({ found: true })
|
|
32
|
-
const r4 = await (ToolsInteract as any).waitForUIHandler({ type: 'ui', query: 'ShortStable', timeoutMs: 2000, pollIntervalMs: 200, stability_ms: 50, match: 'present', platform: 'android' })
|
|
33
|
-
console.log('Short stability test:', r4 && (r4 as any).success ? 'PASS' : 'FAIL', JSON.stringify({ poll_count: (r4 as any).poll_count, duration_ms: (r4 as any).duration_ms, stable_duration_ms: (r4 as any).stable_duration_ms, matchSource: (r4 as any).matchSource }, null, 2))
|
|
34
|
-
|
|
35
|
-
} finally {
|
|
36
|
-
(ToolsInteract as any).findElementHandler = origFind
|
|
37
|
-
(Observe as any).ToolsObserve.getScreenFingerprintHandler = origFp
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
run().catch(e=>{ console.error(e); process.exit(1) })
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { ToolsInteract } from '../../../../src/interact/index.js'
|
|
2
|
-
import * as Observe from '../../../../src/observe/index.js'
|
|
3
|
-
|
|
4
|
-
async function run() {
|
|
5
|
-
console.log('Unit: wait_for_ui stability behavior')
|
|
6
|
-
|
|
7
|
-
const origFind = (ToolsInteract as any).findElementHandler
|
|
8
|
-
const origFp = (Observe as any).ToolsObserve.getScreenFingerprintHandler
|
|
9
|
-
|
|
10
|
-
try {
|
|
11
|
-
// Simulate UI flicker: present, absent, present, then stable
|
|
12
|
-
const seq = [false, true, false, true, true, true]
|
|
13
|
-
(ToolsInteract as any).findElementHandler = async () => ({ found: seq.shift() ?? true })
|
|
14
|
-
|
|
15
|
-
const res = await (ToolsInteract as any).waitForUIHandler({ type: 'ui', query: 'X', timeoutMs: 5000, pollIntervalMs: 100, stability_ms: 500, platform: 'android' })
|
|
16
|
-
const ok = res && (res as any).success
|
|
17
|
-
console.log('Flicker stability test:', ok ? 'PASS' : 'FAIL', JSON.stringify((res as any).telemetry || {}, null, 2))
|
|
18
|
-
|
|
19
|
-
// Simulate immediate stable presence
|
|
20
|
-
(ToolsInteract as any).findElementHandler = async () => ({ found: true })
|
|
21
|
-
const res2 = await (ToolsInteract as any).waitForUIHandler({ type: 'ui', query: 'Y', timeoutMs: 2000, pollIntervalMs: 100, stability_ms: 300, platform: 'android' })
|
|
22
|
-
console.log('Immediate stable test:', res2 && (res2 as any).success ? 'PASS' : 'FAIL', JSON.stringify((res2 as any).telemetry || {}, null, 2))
|
|
23
|
-
|
|
24
|
-
} finally {
|
|
25
|
-
(ToolsInteract as any).findElementHandler = origFind
|
|
26
|
-
(Observe as any).ToolsObserve.getScreenFingerprintHandler = origFp
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
run().catch(e=>{ console.error(e); process.exit(1) })
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import assert from 'assert'
|
|
2
|
-
import * as androidUtils from '../../src/utils/android/utils.js'
|
|
3
|
-
import * as systemStatus from '../../src/system/index.js'
|
|
4
|
-
|
|
5
|
-
const origEnsure = (androidUtils as any).ensureAdbAvailable
|
|
6
|
-
|
|
7
|
-
function mockEnsure(returnVal: any) {
|
|
8
|
-
(androidUtils as any).ensureAdbAvailable = () => returnVal
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function restoreEnsure() {
|
|
12
|
-
(androidUtils as any).ensureAdbAvailable = origEnsure
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
describe('adb version parsing', () => {
|
|
16
|
-
afterEach(() => {
|
|
17
|
-
restoreEnsure()
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
it('uses only the first line of multi-line adb --version output', async () => {
|
|
21
|
-
mockEnsure({ adbCmd: 'adb', ok: true, version: 'Android Debug Bridge version 1.0.41\nRevision 8f3b7' })
|
|
22
|
-
const res = await systemStatus.getSystemStatus()
|
|
23
|
-
assert.strictEqual(res.adbVersion, 'Android Debug Bridge version 1.0.41')
|
|
24
|
-
})
|
|
25
|
-
})
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import assert from 'assert'
|
|
2
|
-
import { ensureAdbAvailable } from '../../src/utils/android/utils.js'
|
|
3
|
-
import { getXcrunCmd } from '../../src/utils/ios/utils.js'
|
|
4
|
-
|
|
5
|
-
// We import the server handler module to access the internal get_system_status implementation.
|
|
6
|
-
import * as server from '../../src/server.js'
|
|
7
|
-
|
|
8
|
-
// Small helper to call the tool handler similarly to how the MCP transport would.
|
|
9
|
-
async function callGetSystemStatus() {
|
|
10
|
-
const req = { params: { name: 'get_system_status', arguments: {} } }
|
|
11
|
-
// @ts-ignore - use the handler exported from server
|
|
12
|
-
const handler = (server as any).defaultRequestHandler || (server as any).callToolHandler || (server as any).__callTool
|
|
13
|
-
if (!handler) {
|
|
14
|
-
// fallback: require the module and call the exported server instance's request handler
|
|
15
|
-
// The server code registers the handler directly; we will emulate by requiring compiled code in dist if available.
|
|
16
|
-
try {
|
|
17
|
-
const dist = await import('../../dist/server.js')
|
|
18
|
-
// Try to execute by sending a call via the server instance if exported
|
|
19
|
-
if (dist && dist.server && typeof dist.server._handleCall === 'function') {
|
|
20
|
-
return dist.server._handleCall(req)
|
|
21
|
-
}
|
|
22
|
-
} catch {
|
|
23
|
-
// best effort only
|
|
24
|
-
}
|
|
25
|
-
throw new Error('Cannot locate server call handler for tests')
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return handler(req)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
describe('get_system_status tool (unit)', () => {
|
|
32
|
-
it('returns structured result without throwing', async () => {
|
|
33
|
-
const res = await callGetSystemStatus()
|
|
34
|
-
// Handler returns { content: [{ type: 'text', text: JSON.stringify(...) }] }
|
|
35
|
-
assert(res && res.content && Array.isArray(res.content))
|
|
36
|
-
const textBlock = res.content.find((c: any) => c.type === 'text')
|
|
37
|
-
assert(textBlock && textBlock.text)
|
|
38
|
-
const payload = JSON.parse(textBlock.text)
|
|
39
|
-
assert(typeof payload.success === 'boolean')
|
|
40
|
-
assert(Array.isArray(payload.issues))
|
|
41
|
-
}).timeout(5000)
|
|
42
|
-
|
|
43
|
-
it('detects adb availability helper works', () => {
|
|
44
|
-
const adb = ensureAdbAvailable()
|
|
45
|
-
assert(adb && typeof adb.ok === 'boolean')
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
it('detects xcrun command helper exists', () => {
|
|
49
|
-
const cmd = getXcrunCmd()
|
|
50
|
-
assert(typeof cmd === 'string')
|
|
51
|
-
})
|
|
52
|
-
})
|