mobile-debug-mcp 0.21.4 → 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 +263 -41
  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 +9 -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 +286 -38
  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
@@ -1,76 +0,0 @@
1
- import { ToolsInteract } from '../../../src/interact/index.js'
2
- import * as Observe from '../../../src/observe/index.js'
3
-
4
- async function runTests() {
5
- console.log('Starting wait_for_ui unit tests...')
6
-
7
- const origFind = (ToolsInteract as any).findElementHandler
8
- const origReadLog = (Observe as any).ToolsObserve.readLogStreamHandler
9
- const origGetLogs = (Observe as any).ToolsObserve.getLogsHandler
10
- const origGetFp = (Observe as any).ToolsObserve.getScreenFingerprintHandler
11
- const origResolveObserve = (Observe as any).ToolsObserve.resolveObserve
12
- const origGetScreenFp = (Observe as any).ToolsObserve.getScreenFingerprintHandler
13
-
14
- try {
15
- // Timeout / snapshot case: ensure snapshot captured when condition not met
16
- const origCapture = (Observe as any).ToolsObserve.captureDebugSnapshotHandler
17
- ;(Observe as any).ToolsObserve.captureDebugSnapshotHandler = async ({ reason }: any) => ({ reason, fingerprint: 'snap-123', ui_tree: null, logs: [] })
18
- // make findElement always fail
19
- (ToolsInteract as any).findElementHandler = async () => ({ found: false })
20
- const resTimeout = await ToolsInteract.waitForUIHandler({ type: 'ui', query: 'WillNeverExist', timeoutMs: 500, pollIntervalMs: 100, platform: 'android' })
21
- const okTimeout = resTimeout && !(resTimeout as any).success && (resTimeout as any).snapshot && (resTimeout as any).snapshot.fingerprint === 'snap-123' && (resTimeout as any).telemetry && (resTimeout as any).telemetry.pollCount > 0
22
- console.log('Timeout Snapshot Test:', okTimeout ? 'PASS' : 'FAIL', JSON.stringify((resTimeout as any).telemetry || {}, null, 2))
23
- ;(Observe as any).ToolsObserve.captureDebugSnapshotHandler = origCapture
24
-
25
- // UI condition: findElement returns found on 2nd call
26
- let calls = 0
27
- ;(ToolsInteract as any).findElementHandler = async (args) => {
28
- calls++
29
- const query = (args && (args.query || args)) || ''
30
- if (calls >= 2) return { found: true, element: { text: query } }
31
- return { found: false }
32
- }
33
-
34
- const resUi = await ToolsInteract.waitForUIHandler({ type: 'ui', query: 'Generate Session', timeoutMs: 3000, pollIntervalMs: 100, platform: 'android' })
35
- const okUi = resUi && (resUi as any).success && (resUi as any).telemetry && (resUi as any).telemetry.pollCount > 0 && (resUi as any).telemetry.timeToMatch >= 0
36
- console.log('UI Test:', okUi ? 'PASS' : 'FAIL', JSON.stringify((resUi as any).telemetry || {}, null, 2))
37
-
38
- // Log condition: stream empty, snapshot contains matching line
39
- ;(Observe as any).ToolsObserve.readLogStreamHandler = async () => ({ entries: [ { message: 'nothing' } ] })
40
- let glCalls = 0
41
- ;(Observe as any).ToolsObserve.getLogsHandler = async () => {
42
- glCalls++
43
- if (glCalls === 1) return { device: {}, logs: ['INFO start'] }
44
- return { device: {}, logs: ['INFO start', 'ERROR Exception occurred', 'Server: Boom'] }
45
- }
46
-
47
- const resLog = await ToolsInteract.waitForUIHandler({ type: 'log', query: 'Server', timeoutMs: 3000, pollIntervalMs: 100, platform: 'android' })
48
- const okLog = resLog && (resLog as any).success && (resLog as any).telemetry && (resLog as any).telemetry.pollCount > 0 && (resLog as any).telemetry.matchSource === 'log-snapshot'
49
- console.log('Log Test:', okLog ? 'PASS' : 'FAIL', JSON.stringify((resLog as any).telemetry || {}, null, 2))
50
-
51
- // Screen condition: fingerprint changes after a few polls
52
- let seq = ['A', 'A', 'B']
53
- ;(Observe as any).ToolsObserve.getScreenFingerprintHandler = async () => ({ fingerprint: seq.length ? seq.shift() : null })
54
- const resScreen = await ToolsInteract.waitForUIHandler({ type: 'screen', timeoutMs: 3000, pollIntervalMs: 100, platform: 'android' })
55
- const okScreen = resScreen && (resScreen as any).success && (resScreen as any).telemetry && (resScreen as any).telemetry.matchSource === 'screen-fingerprint'
56
- console.log('Screen Test:', okScreen ? 'PASS' : 'FAIL', JSON.stringify((resScreen as any).telemetry || {}, null, 2))
57
-
58
- // Idle condition: stable fingerprints observed
59
- let idleSeq = ['X', 'X', 'X']
60
- ;(Observe as any).ToolsObserve.getScreenFingerprintHandler = async () => ({ fingerprint: idleSeq.length ? idleSeq.shift() : 'X' })
61
- const resIdle = await ToolsInteract.waitForUIHandler({ type: 'idle', timeoutMs: 3000, pollIntervalMs: 100, platform: 'android' })
62
- const okIdle = resIdle && (resIdle as any).success && (resIdle as any).telemetry && (resIdle as any).telemetry.matchSource === 'idle-stable'
63
- console.log('Idle Test:', okIdle ? 'PASS' : 'FAIL', JSON.stringify((resIdle as any).telemetry || {}, null, 2))
64
-
65
- } finally {
66
- ;(ToolsInteract as any).findElementHandler = origFind
67
- ;(Observe as any).ToolsObserve.readLogStreamHandler = origReadLog
68
- ;(Observe as any).ToolsObserve.getLogsHandler = origGetLogs
69
- ;(Observe as any).ToolsObserve.getScreenFingerprintHandler = origGetFp
70
- ;(Observe as any).ToolsObserve.resolveObserve = origResolveObserve
71
- ;(Observe as any).ToolsObserve.getScreenFingerprintHandler = origGetScreenFp
72
- ;(Observe as any).ToolsObserve.getScreenFingerprintHandler = origGetScreenFp
73
- }
74
- }
75
-
76
- runTests().catch(console.error)
@@ -1,57 +0,0 @@
1
- import { ToolsInteract } from '../../../src/interact/index.js'
2
- import * as Observe from '../../../src/observe/index.js'
3
-
4
- async function run() {
5
- console.log('Starting new wait_for_ui unit tests...')
6
- const origGetUITree = (Observe as any).ToolsObserve.getUITreeHandler
7
-
8
- try {
9
- // Test 1: exact text match -> exists
10
- (Observe as any).ToolsObserve.getUITreeHandler = async () => ({ elements: [ { text: 'Hello', resourceId: 'rid1', contentDescription: 'acc1', type: 'Button', bounds: [0,0,10,10], visible: true, clickable: false, enabled: true } ] })
11
- const r1 = await ToolsInteract.waitForUIHandler({ selector: { text: 'Hello' }, condition: 'exists', timeout_ms: 1000, poll_interval_ms: 50, platform: 'android' })
12
- const ok1 = r1 && r1.status === 'success' && r1.matched === 1 && r1.element && r1.element.text === 'Hello'
13
- console.log('Exact match exists:', ok1 ? 'PASS' : 'FAIL', JSON.stringify(r1, null, 2))
14
-
15
- // Test 2: contains matching
16
- (Observe as any).ToolsObserve.getUITreeHandler = async () => ({ elements: [ { text: 'Welcome User', resourceId: 'rid2', contentDescription: 'acc2', type: 'TextView', bounds: [0,0,50,10], visible: true } ] })
17
- const r2 = await ToolsInteract.waitForUIHandler({ selector: { text: 'User', contains: true }, condition: 'exists', timeout_ms: 1000, poll_interval_ms: 50, platform: 'android' })
18
- const ok2 = r2 && r2.status === 'success' && r2.matched === 1 && r2.element && r2.element.text && r2.element.text.includes('Welcome')
19
- console.log('Contains match:', ok2 ? 'PASS' : 'FAIL', JSON.stringify(r2, null, 2))
20
-
21
- // Test 3: visible condition
22
- (Observe as any).ToolsObserve.getUITreeHandler = async () => ({ elements: [ { text: 'Hidden', resourceId: 'rid3', bounds: [0,0,0,0], visible: false } ] })
23
- const r3 = await ToolsInteract.waitForUIHandler({ selector: { text: 'Hidden' }, condition: 'visible', timeout_ms: 300, poll_interval_ms: 50, platform: 'android' })
24
- const ok3 = r3 && r3.status === 'timeout' && r3.error && r3.error.code === 'ELEMENT_NOT_FOUND'
25
- console.log('Visible negative (hidden element):', ok3 ? 'PASS' : 'FAIL', JSON.stringify(r3, null, 2))
26
-
27
- // Test 4: clickable condition
28
- (Observe as any).ToolsObserve.getUITreeHandler = async () => ({ elements: [ { text: 'TapMe', resourceId: 'rid4', bounds: [0,0,20,20], visible: true, clickable: true, enabled: true } ] })
29
- const r4 = await ToolsInteract.waitForUIHandler({ selector: { text: 'TapMe' }, condition: 'clickable', timeout_ms: 1000, poll_interval_ms: 50, platform: 'android' })
30
- const ok4 = r4 && r4.status === 'success' && r4.matched === 1 && r4.element && r4.element.index === 0
31
- console.log('Clickable match:', ok4 ? 'PASS' : 'FAIL', JSON.stringify(r4, null, 2))
32
-
33
- // Test 5: retry behavior - first attempt times out, second attempt succeeds
34
- const start = Date.now()
35
- let seqTree = async () => {
36
- const now = Date.now()
37
- // for first ~400ms return no elements, afterwards return match
38
- if (now - start < 400) return { elements: [] }
39
- return { elements: [ { text: 'Retried', resourceId: 'rid5', bounds: [0,0,10,10], visible: true } ] }
40
- }
41
- ;(Observe as any).ToolsObserve.getUITreeHandler = seqTree
42
- const r5 = await ToolsInteract.waitForUIHandler({ selector: { text: 'Retried' }, condition: 'exists', timeout_ms: 200, poll_interval_ms: 50, match: undefined, retry: { max_attempts: 3, backoff_ms: 150 }, platform: 'android' })
43
- const ok5 = r5 && r5.status === 'success' && r5.metrics && r5.metrics.attempts >= 2
44
- console.log('Retry behavior:', ok5 ? 'PASS' : 'FAIL', JSON.stringify(r5, null, 2))
45
-
46
- // Test 6: timeout with no selector match -> correct error code
47
- (Observe as any).ToolsObserve.getUITreeHandler = async () => ({ elements: [] })
48
- const r6 = await ToolsInteract.waitForUIHandler({ selector: { text: 'Nope' }, condition: 'exists', timeout_ms: 300, poll_interval_ms: 50, retry: { max_attempts: 1 }, platform: 'android' })
49
- const ok6 = r6 && r6.status === 'timeout' && r6.error && r6.error.code === 'ELEMENT_NOT_FOUND'
50
- console.log('Timeout no match:', ok6 ? 'PASS' : 'FAIL', JSON.stringify(r6, null, 2))
51
-
52
- } finally {
53
- (Observe as any).ToolsObserve.getUITreeHandler = origGetUITree
54
- }
55
- }
56
-
57
- run().catch(err => { console.error('wait_for_ui_new tests failed:', err); process.exit(1) })
@@ -1,3 +0,0 @@
1
- // wait_for_element device runner removed
2
- console.log('wait_for_element device test removed');
3
- process.exit(0);
@@ -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,2 +0,0 @@
1
- // wait_for_element tests removed — tool deprecated
2
- console.log('wait_for_element unit tests removed');
@@ -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
- })