mobile-debug-mcp 0.21.5 → 0.23.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/classify.js +35 -0
- package/dist/interact/index.js +220 -13
- package/dist/network/index.js +232 -0
- package/dist/observe/ios.js +10 -3
- package/dist/server-core.js +822 -0
- package/dist/server.js +6 -693
- package/dist/utils/resolve-device.js +15 -3
- package/docs/CHANGELOG.md +10 -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/classify.ts +64 -0
- package/src/interact/index.ts +250 -13
- package/src/network/index.ts +268 -0
- package/src/observe/ios.ts +12 -3
- package/src/server-core.ts +879 -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/classify_action_outcome.test.ts +110 -0
- 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/unit/network/get_network_activity.test.ts +181 -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,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
|
-
})
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import assert from 'assert'
|
|
2
|
-
import child_process from 'child_process'
|
|
3
|
-
|
|
4
|
-
import * as androidUtils from '../../src/utils/android/utils.js'
|
|
5
|
-
import * as iosUtils from '../../src/utils/ios/utils.js'
|
|
6
|
-
import * as systemStatus from '../../src/system/index.js'
|
|
7
|
-
|
|
8
|
-
const origExecSync = child_process.execSync
|
|
9
|
-
const origEnsure = (androidUtils as any).ensureAdbAvailable
|
|
10
|
-
const origGetXcrun = (iosUtils as any).getXcrunCmd
|
|
11
|
-
|
|
12
|
-
function mockExec(behaviour: (cmd: string) => string) {
|
|
13
|
-
(child_process as any).execSync = (cmd: string) => {
|
|
14
|
-
const s = typeof cmd === 'string' ? cmd : (Array.isArray(cmd) ? cmd.join(' ') : String(cmd))
|
|
15
|
-
const out = behaviour(s)
|
|
16
|
-
if (out instanceof Error) throw out
|
|
17
|
-
return out
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function restoreExec() {
|
|
22
|
-
(child_process as any).execSync = origExecSync
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function mockEnsure(returnVal: any) {
|
|
26
|
-
(androidUtils as any).ensureAdbAvailable = () => returnVal
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function restoreEnsure() {
|
|
30
|
-
(androidUtils as any).ensureAdbAvailable = origEnsure
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function mockGetXcrun(val: string) {
|
|
34
|
-
(iosUtils as any).getXcrunCmd = () => val
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function restoreGetXcrun() {
|
|
38
|
-
(iosUtils as any).getXcrunCmd = origGetXcrun
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
describe('system_status checks', () => {
|
|
42
|
-
afterEach(() => {
|
|
43
|
-
restoreExec(); restoreEnsure(); restoreGetXcrun()
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
it('reports healthy system when adb and xcrun present', async () => {
|
|
47
|
-
mockEnsure({ adbCmd: 'adb', ok: true, version: '8.1.0' })
|
|
48
|
-
mockGetXcrun('xcrun')
|
|
49
|
-
|
|
50
|
-
mockExec((cmd) => {
|
|
51
|
-
if (cmd.startsWith('adb devices')) return 'List of devices attached\nemulator-5554\tdevice'
|
|
52
|
-
if (cmd.includes('adb logcat')) return 'I/Tag: ok'
|
|
53
|
-
if (cmd.includes('adb shell pm path')) return 'package:/data/app/com.example-1/base.apk'
|
|
54
|
-
if (cmd.startsWith('xcrun --version')) return 'xcrun version 123'
|
|
55
|
-
if (cmd.includes('simctl list devices booted --json')) return JSON.stringify({ devices: {} })
|
|
56
|
-
return ''
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
const res = await systemStatus.getSystemStatus()
|
|
60
|
-
assert.strictEqual(res.success, true)
|
|
61
|
-
assert.strictEqual(res.adbAvailable, true)
|
|
62
|
-
assert.strictEqual(typeof res.adbVersion, 'string')
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
it('reports adb missing', async () => {
|
|
66
|
-
mockEnsure({ adbCmd: 'adb', ok: false, error: 'not found' })
|
|
67
|
-
mockGetXcrun('xcrun')
|
|
68
|
-
mockExec((cmd) => {
|
|
69
|
-
if (cmd.startsWith('xcrun --version')) return 'xcrun version'
|
|
70
|
-
return ''
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
const res = await systemStatus.getSystemStatus()
|
|
74
|
-
assert.strictEqual(res.success, false)
|
|
75
|
-
assert(res.issues.some((i: string) => i.includes('ADB')))
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
it('detects unauthorized/offline devices', async () => {
|
|
79
|
-
mockEnsure({ adbCmd: 'adb', ok: true, version: '8.1.0' })
|
|
80
|
-
mockGetXcrun('xcrun')
|
|
81
|
-
mockExec((cmd) => {
|
|
82
|
-
if (cmd.startsWith('adb devices')) return 'List of devices attached\nserial1\tunauthorized\nserial2\toffline\n'
|
|
83
|
-
if (cmd.startsWith('xcrun --version')) return 'xcrun version'
|
|
84
|
-
return ''
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
const res = await systemStatus.getSystemStatus()
|
|
88
|
-
assert.strictEqual(res.success, false)
|
|
89
|
-
assert(res.issues.some((i: string) => i.includes('unauthorized')))
|
|
90
|
-
assert(res.issues.some((i: string) => i.includes('offline')))
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
it('handles missing xcrun gracefully', async () => {
|
|
94
|
-
mockEnsure({ adbCmd: 'adb', ok: true, version: '8.1.0' })
|
|
95
|
-
mockGetXcrun('xcrun')
|
|
96
|
-
mockExec((cmd) => {
|
|
97
|
-
if (cmd.startsWith('adb devices')) return 'List of devices attached\nemulator-5554\tdevice'
|
|
98
|
-
if (cmd.startsWith('xcrun --version')) throw new Error('not found')
|
|
99
|
-
return ''
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
const res = await systemStatus.getSystemStatus()
|
|
103
|
-
// Expect iOS check to be false and Android to be healthy
|
|
104
|
-
assert.strictEqual(res.iosAvailable, false)
|
|
105
|
-
assert.strictEqual(res.adbAvailable, true)
|
|
106
|
-
// overall success may still be true (Android ok) but issues should include an xcrun-related message
|
|
107
|
-
assert(res.issues.some((i: string) => i.toLowerCase().includes('xcrun') || i.toLowerCase().includes('ios')))
|
|
108
|
-
})
|
|
109
|
-
})
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|