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
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import { ToolsInteract } from '../../../src/interact/index.js'
|
|
3
|
+
import * as Observe from '../../../src/observe/index.js'
|
|
4
|
+
|
|
5
|
+
async function run() {
|
|
6
|
+
console.log('Starting tap_element unit tests...')
|
|
7
|
+
const originalGetUITreeHandler = (Observe as any).ToolsObserve.getUITreeHandler
|
|
8
|
+
const originalTapHandler = (ToolsInteract as any).tapHandler
|
|
9
|
+
const originalComputeElementId = (ToolsInteract as any)._computeElementId
|
|
10
|
+
;(ToolsInteract as any)._resetResolvedUiElementsForTests()
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
;(Observe as any).ToolsObserve.getUITreeHandler = async () => ({
|
|
14
|
+
device: { platform: 'android', id: 'mock-device' },
|
|
15
|
+
elements: [
|
|
16
|
+
{ text: 'Submit', resourceId: 'btn_submit', bounds: [0, 0, 20, 20], visible: true, enabled: true, clickable: true }
|
|
17
|
+
]
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const waitSuccess = await ToolsInteract.waitForUIHandler({
|
|
21
|
+
selector: { text: 'Submit' },
|
|
22
|
+
condition: 'exists',
|
|
23
|
+
timeout_ms: 200,
|
|
24
|
+
poll_interval_ms: 50,
|
|
25
|
+
platform: 'android'
|
|
26
|
+
})
|
|
27
|
+
assert.strictEqual(waitSuccess.status, 'success')
|
|
28
|
+
const successElementId = waitSuccess.element.elementId
|
|
29
|
+
|
|
30
|
+
let tapped: { x: number, y: number, platform?: string, deviceId?: string } | null = null
|
|
31
|
+
;(ToolsInteract as any).tapHandler = async ({ platform, x, y, deviceId }: any) => {
|
|
32
|
+
tapped = { platform, x, y, deviceId }
|
|
33
|
+
return { success: true, device: { platform: platform || 'android', id: deviceId || 'mock-device' }, x, y }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const tapSuccess = await ToolsInteract.tapElementHandler({ elementId: successElementId })
|
|
37
|
+
assert.deepStrictEqual(tapSuccess, { success: true, elementId: successElementId, action: 'tap' })
|
|
38
|
+
assert.deepStrictEqual(tapped, { platform: 'android', x: 10, y: 10, deviceId: 'mock-device' })
|
|
39
|
+
|
|
40
|
+
;(Observe as any).ToolsObserve.getUITreeHandler = async () => ({
|
|
41
|
+
device: { platform: 'android', id: 'mock-device' },
|
|
42
|
+
elements: [
|
|
43
|
+
{ text: 'Hidden', resourceId: 'btn_hidden', bounds: [0, 0, 20, 20], visible: false, enabled: true, clickable: true }
|
|
44
|
+
]
|
|
45
|
+
})
|
|
46
|
+
const waitHidden = await ToolsInteract.waitForUIHandler({
|
|
47
|
+
selector: { text: 'Hidden' },
|
|
48
|
+
condition: 'exists',
|
|
49
|
+
timeout_ms: 200,
|
|
50
|
+
poll_interval_ms: 50,
|
|
51
|
+
platform: 'android'
|
|
52
|
+
})
|
|
53
|
+
const hiddenResult = await ToolsInteract.tapElementHandler({ elementId: waitHidden.element.elementId })
|
|
54
|
+
assert.strictEqual(hiddenResult.success, false)
|
|
55
|
+
assert.strictEqual(hiddenResult.error?.code, 'element_not_visible')
|
|
56
|
+
|
|
57
|
+
;(Observe as any).ToolsObserve.getUITreeHandler = async () => ({
|
|
58
|
+
device: { platform: 'android', id: 'mock-device' },
|
|
59
|
+
elements: [
|
|
60
|
+
{ text: 'Disabled', resourceId: 'btn_disabled', bounds: [0, 0, 20, 20], visible: true, enabled: false, clickable: true }
|
|
61
|
+
]
|
|
62
|
+
})
|
|
63
|
+
const waitDisabled = await ToolsInteract.waitForUIHandler({
|
|
64
|
+
selector: { text: 'Disabled' },
|
|
65
|
+
condition: 'exists',
|
|
66
|
+
timeout_ms: 200,
|
|
67
|
+
poll_interval_ms: 50,
|
|
68
|
+
platform: 'android'
|
|
69
|
+
})
|
|
70
|
+
const disabledResult = await ToolsInteract.tapElementHandler({ elementId: waitDisabled.element.elementId })
|
|
71
|
+
assert.strictEqual(disabledResult.success, false)
|
|
72
|
+
assert.strictEqual(disabledResult.error?.code, 'element_not_enabled')
|
|
73
|
+
|
|
74
|
+
;(Observe as any).ToolsObserve.getUITreeHandler = async () => ({
|
|
75
|
+
device: { platform: 'android', id: 'mock-device' },
|
|
76
|
+
elements: []
|
|
77
|
+
})
|
|
78
|
+
const notFoundResult = await ToolsInteract.tapElementHandler({ elementId: successElementId })
|
|
79
|
+
assert.strictEqual(notFoundResult.success, false)
|
|
80
|
+
assert.strictEqual(notFoundResult.error?.code, 'element_not_found')
|
|
81
|
+
|
|
82
|
+
;(ToolsInteract as any)._resetResolvedUiElementsForTests()
|
|
83
|
+
const targetIndex = 25
|
|
84
|
+
;(Observe as any).ToolsObserve.getUITreeHandler = async () => ({
|
|
85
|
+
device: { platform: 'android', id: 'mock-device' },
|
|
86
|
+
elements: Array.from({ length: 50 }, (_, index) => ({
|
|
87
|
+
text: index === targetIndex ? 'Indexed target' : `Filler ${index}`,
|
|
88
|
+
resourceId: index === targetIndex ? 'btn_indexed_target' : `btn_filler_${index}`,
|
|
89
|
+
bounds: [index, index, index + 20, index + 20],
|
|
90
|
+
visible: true,
|
|
91
|
+
enabled: true,
|
|
92
|
+
clickable: true
|
|
93
|
+
}))
|
|
94
|
+
})
|
|
95
|
+
const indexedWait = await ToolsInteract.waitForUIHandler({
|
|
96
|
+
selector: { text: 'Indexed target' },
|
|
97
|
+
condition: 'exists',
|
|
98
|
+
timeout_ms: 200,
|
|
99
|
+
poll_interval_ms: 50,
|
|
100
|
+
platform: 'android'
|
|
101
|
+
})
|
|
102
|
+
assert.strictEqual(indexedWait.status, 'success')
|
|
103
|
+
|
|
104
|
+
let computeCalls = 0
|
|
105
|
+
;(ToolsInteract as any)._computeElementId = (...args: any[]) => {
|
|
106
|
+
computeCalls++
|
|
107
|
+
return originalComputeElementId.apply(ToolsInteract, args)
|
|
108
|
+
}
|
|
109
|
+
const indexedTap = await ToolsInteract.tapElementHandler({ elementId: indexedWait.element.elementId })
|
|
110
|
+
assert.strictEqual(indexedTap.success, true)
|
|
111
|
+
assert.strictEqual(computeCalls, 1, 'Stored index should allow a single fast-path element ID check')
|
|
112
|
+
;(ToolsInteract as any)._computeElementId = originalComputeElementId
|
|
113
|
+
|
|
114
|
+
;(Observe as any).ToolsObserve.getUITreeHandler = async () => ({
|
|
115
|
+
device: { platform: 'android', id: 'mock-device' },
|
|
116
|
+
elements: Array.from({ length: 51 }, (_, index) => ({
|
|
117
|
+
text: index === 0 ? 'Inserted first' : index === targetIndex + 1 ? 'Indexed target' : `Shifted filler ${index}`,
|
|
118
|
+
resourceId: index === 0 ? 'btn_inserted_first' : index === targetIndex + 1 ? 'btn_indexed_target' : `btn_shifted_filler_${index}`,
|
|
119
|
+
bounds: [index, index, index + 20, index + 20],
|
|
120
|
+
visible: true,
|
|
121
|
+
enabled: true,
|
|
122
|
+
clickable: true
|
|
123
|
+
}))
|
|
124
|
+
})
|
|
125
|
+
const shiftedIndexResult = await ToolsInteract.tapElementHandler({ elementId: indexedWait.element.elementId })
|
|
126
|
+
assert.strictEqual(shiftedIndexResult.success, false)
|
|
127
|
+
assert.strictEqual(shiftedIndexResult.error?.code, 'element_not_found')
|
|
128
|
+
|
|
129
|
+
;(ToolsInteract as any)._resetResolvedUiElementsForTests()
|
|
130
|
+
const cacheLimit = (ToolsInteract as any)._maxResolvedUiElements as number
|
|
131
|
+
let oldestElementId: string | null = null
|
|
132
|
+
let cacheFixtureIndex = 0
|
|
133
|
+
;(Observe as any).ToolsObserve.getUITreeHandler = async () => ({
|
|
134
|
+
device: { platform: 'android', id: 'mock-device' },
|
|
135
|
+
elements: [
|
|
136
|
+
{ text: `Button ${cacheFixtureIndex}`, resourceId: `btn_${cacheFixtureIndex}`, bounds: [0, 0, 20, 20], visible: true, enabled: true, clickable: true }
|
|
137
|
+
]
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
for (let i = 0; i < cacheLimit + 1; i++) {
|
|
141
|
+
cacheFixtureIndex = i
|
|
142
|
+
const result = await ToolsInteract.waitForUIHandler({
|
|
143
|
+
selector: { text: `Button ${i}` },
|
|
144
|
+
condition: 'exists',
|
|
145
|
+
timeout_ms: 200,
|
|
146
|
+
poll_interval_ms: 50,
|
|
147
|
+
platform: 'android'
|
|
148
|
+
})
|
|
149
|
+
assert.strictEqual(result.status, 'success')
|
|
150
|
+
if (i === 0) oldestElementId = result.element.elementId
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
assert.ok(oldestElementId, 'Oldest element ID should be captured')
|
|
154
|
+
const evictedResult = await ToolsInteract.tapElementHandler({ elementId: oldestElementId as string })
|
|
155
|
+
assert.strictEqual(evictedResult.success, false)
|
|
156
|
+
assert.strictEqual(evictedResult.error?.code, 'element_not_found')
|
|
157
|
+
|
|
158
|
+
console.log('tap_element unit tests passed')
|
|
159
|
+
} finally {
|
|
160
|
+
;(ToolsInteract as any)._resetResolvedUiElementsForTests()
|
|
161
|
+
;(ToolsInteract as any)._computeElementId = originalComputeElementId
|
|
162
|
+
;(Observe as any).ToolsObserve.getUITreeHandler = originalGetUITreeHandler
|
|
163
|
+
;(ToolsInteract as any).tapHandler = originalTapHandler
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
run().catch((error) => {
|
|
168
|
+
console.error(error)
|
|
169
|
+
process.exit(1)
|
|
170
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { ToolsInteract } from '../../../src/interact/index.js'
|
|
2
|
+
import * as Observe from '../../../src/observe/index.js'
|
|
3
|
+
import assert from 'assert'
|
|
4
|
+
|
|
5
|
+
const original = (Observe as any).ToolsObserve.getScreenFingerprintHandler
|
|
6
|
+
|
|
7
|
+
async function runTests() {
|
|
8
|
+
console.log('Starting tests for wait_for_screen_change...')
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
let seq1: Array<string | null> = ['B', 'B']
|
|
12
|
+
;(Observe as any).ToolsObserve.getScreenFingerprintHandler = async () => ({ fingerprint: seq1.length ? seq1.shift() : null })
|
|
13
|
+
const start1 = Date.now()
|
|
14
|
+
const res1 = await ToolsInteract.waitForScreenChangeHandler({ platform: 'android', previousFingerprint: 'A', timeoutMs: 2000, pollIntervalMs: 50 })
|
|
15
|
+
const elapsed1 = Date.now() - start1
|
|
16
|
+
assert.ok(res1 && (res1 as any).success === true && (res1 as any).newFingerprint === 'B', 'Immediate fingerprint change should succeed')
|
|
17
|
+
console.log('Test 1: Immediate change -> PASS', 'Elapsed:', elapsed1, 'ms')
|
|
18
|
+
|
|
19
|
+
let seq2: Array<string | null> = [null, null, 'B', 'B']
|
|
20
|
+
;(Observe as any).ToolsObserve.getScreenFingerprintHandler = async () => ({ fingerprint: seq2.length ? seq2.shift() : 'B' })
|
|
21
|
+
const res2 = await ToolsInteract.waitForScreenChangeHandler({ platform: 'android', previousFingerprint: 'A', timeoutMs: 3000, pollIntervalMs: 50 })
|
|
22
|
+
assert.ok(res2 && (res2 as any).success === true && (res2 as any).newFingerprint === 'B', 'Transient nulls should not prevent success')
|
|
23
|
+
console.log('Test 2: Transient nulls -> PASS')
|
|
24
|
+
|
|
25
|
+
;(Observe as any).ToolsObserve.getScreenFingerprintHandler = async () => ({ fingerprint: 'A' })
|
|
26
|
+
const res3 = await ToolsInteract.waitForScreenChangeHandler({ platform: 'android', previousFingerprint: 'A', timeoutMs: 300, pollIntervalMs: 50 })
|
|
27
|
+
assert.ok(res3 && (res3 as any).success === false && (res3 as any).reason === 'timeout', 'Unchanged fingerprint should time out')
|
|
28
|
+
console.log('Test 3: Timeout -> PASS')
|
|
29
|
+
} finally {
|
|
30
|
+
;(Observe as any).ToolsObserve.getScreenFingerprintHandler = original
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
runTests().catch((error) => { console.error(error); process.exit(1) })
|
|
@@ -8,21 +8,22 @@ async function run() {
|
|
|
8
8
|
|
|
9
9
|
try {
|
|
10
10
|
// success shape
|
|
11
|
-
(Observe as any).ToolsObserve.getUITreeHandler = async () => ({ elements: [ { text: 'OK', resourceId: 'rid', contentDescription: 'acc', type: 'TextView', bounds: [0,0,10,10], visible: true, clickable: false, enabled: true } ] })
|
|
11
|
+
;(Observe as any).ToolsObserve.getUITreeHandler = async () => ({ elements: [ { text: 'OK', resourceId: 'rid', contentDescription: 'acc', type: 'TextView', bounds: [0,0,10,10], visible: true, clickable: false, enabled: true } ] })
|
|
12
12
|
const s = await ToolsInteract.waitForUIHandler({ selector: { text: 'OK' }, condition: 'exists', timeout_ms: 500, poll_interval_ms: 50, platform: 'android' })
|
|
13
13
|
// Assert contract fields for success
|
|
14
|
-
assert.strictEqual(s.status, 'success', 'status must be success')
|
|
15
|
-
assert.strictEqual(typeof s.matched, 'number', 'matched must be number')
|
|
16
|
-
assert.ok(s.element, 'element must be present')
|
|
17
|
-
assert.
|
|
18
|
-
assert.ok(
|
|
14
|
+
assert.strictEqual(s.status, 'success', 'status must be success');
|
|
15
|
+
assert.strictEqual(typeof s.matched, 'number', 'matched must be number');
|
|
16
|
+
assert.ok(s.element, 'element must be present');
|
|
17
|
+
assert.strictEqual(typeof s.element.elementId, 'string', 'elementId must be present');
|
|
18
|
+
assert.ok(s.metrics && typeof s.metrics.latency_ms === 'number' && typeof s.metrics.poll_count === 'number' && typeof s.metrics.attempts === 'number', 'metrics must include latency_ms, poll_count, attempts');
|
|
19
|
+
assert.ok(typeof s.element.bounds !== 'undefined' && s.element.bounds !== null, 'element.bounds must be present');
|
|
19
20
|
|
|
20
21
|
// timeout shape
|
|
21
|
-
(Observe as any).ToolsObserve.getUITreeHandler = async () => ({ elements: [] })
|
|
22
|
+
;(Observe as any).ToolsObserve.getUITreeHandler = async () => ({ elements: [] })
|
|
22
23
|
const t = await ToolsInteract.waitForUIHandler({ selector: { text: 'Nope' }, condition: 'exists', timeout_ms: 200, poll_interval_ms: 50, platform: 'android' })
|
|
23
|
-
assert.strictEqual(t.status, 'timeout', 'status must be timeout on no match')
|
|
24
|
-
assert.ok(t.error && t.error.code && t.error.message, 'timeout must include error with code and message')
|
|
25
|
-
assert.ok(t.metrics && typeof t.metrics.latency_ms === 'number', 'timeout metrics must include latency_ms')
|
|
24
|
+
assert.strictEqual(t.status, 'timeout', 'status must be timeout on no match');
|
|
25
|
+
assert.ok(t.error && t.error.code && t.error.message, 'timeout must include error with code and message');
|
|
26
|
+
assert.ok(t.metrics && typeof t.metrics.latency_ms === 'number', 'timeout metrics must include latency_ms');
|
|
26
27
|
|
|
27
28
|
console.log('wait_for_ui contract tests: PASS')
|
|
28
29
|
} finally {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { ToolsInteract } from '../../../src/interact/index.js'
|
|
2
|
+
import * as Observe from '../../../src/observe/index.js'
|
|
3
|
+
import assert from 'assert'
|
|
4
|
+
|
|
5
|
+
async function run() {
|
|
6
|
+
console.log('Starting new wait_for_ui unit tests...')
|
|
7
|
+
const origGetUITree = (Observe as any).ToolsObserve.getUITreeHandler
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
// Test 1: exact text match -> exists
|
|
11
|
+
;(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 } ] })
|
|
12
|
+
const r1 = await ToolsInteract.waitForUIHandler({ selector: { text: 'Hello' }, condition: 'exists', timeout_ms: 1000, poll_interval_ms: 50, platform: 'android' })
|
|
13
|
+
const ok1 = r1 && r1.status === 'success' && r1.matched === 1 && r1.element && r1.element.text === 'Hello' && typeof r1.element.elementId === 'string'
|
|
14
|
+
assert.ok(ok1, 'Exact match should satisfy exists condition')
|
|
15
|
+
console.log('Exact match exists:', ok1 ? 'PASS' : 'FAIL', JSON.stringify(r1, null, 2))
|
|
16
|
+
|
|
17
|
+
// Test 2: contains matching
|
|
18
|
+
;(Observe as any).ToolsObserve.getUITreeHandler = async () => ({ elements: [ { text: 'Welcome User', resourceId: 'rid2', contentDescription: 'acc2', type: 'TextView', bounds: [0,0,50,10], visible: true } ] })
|
|
19
|
+
const r2 = await ToolsInteract.waitForUIHandler({ selector: { text: 'User', contains: true }, condition: 'exists', timeout_ms: 1000, poll_interval_ms: 50, platform: 'android' })
|
|
20
|
+
const ok2 = r2 && r2.status === 'success' && r2.matched === 1 && r2.element && r2.element.text && r2.element.text.includes('Welcome') && typeof r2.element.elementId === 'string'
|
|
21
|
+
assert.ok(ok2, 'Contains matching should succeed')
|
|
22
|
+
console.log('Contains match:', ok2 ? 'PASS' : 'FAIL', JSON.stringify(r2, null, 2))
|
|
23
|
+
|
|
24
|
+
// Test 3: visible condition
|
|
25
|
+
;(Observe as any).ToolsObserve.getUITreeHandler = async () => ({ elements: [ { text: 'Hidden', resourceId: 'rid3', bounds: [0,0,0,0], visible: false } ] })
|
|
26
|
+
const r3 = await ToolsInteract.waitForUIHandler({ selector: { text: 'Hidden' }, condition: 'visible', timeout_ms: 300, poll_interval_ms: 50, platform: 'android' })
|
|
27
|
+
const ok3 = r3 && r3.status === 'timeout' && r3.error && r3.error.code === 'ELEMENT_NOT_FOUND'
|
|
28
|
+
assert.ok(ok3, 'Hidden element should fail visible condition')
|
|
29
|
+
console.log('Visible negative (hidden element):', ok3 ? 'PASS' : 'FAIL', JSON.stringify(r3, null, 2))
|
|
30
|
+
|
|
31
|
+
// Test 4: clickable condition
|
|
32
|
+
;(Observe as any).ToolsObserve.getUITreeHandler = async () => ({ elements: [ { text: 'TapMe', resourceId: 'rid4', bounds: [0,0,20,20], visible: true, clickable: true, enabled: true } ] })
|
|
33
|
+
const r4 = await ToolsInteract.waitForUIHandler({ selector: { text: 'TapMe' }, condition: 'clickable', timeout_ms: 1000, poll_interval_ms: 50, platform: 'android' })
|
|
34
|
+
const ok4 = r4 && r4.status === 'success' && r4.matched === 1 && r4.element && r4.element.index === 0 && typeof r4.element.elementId === 'string'
|
|
35
|
+
assert.ok(ok4, 'Clickable element should satisfy clickable condition')
|
|
36
|
+
console.log('Clickable match:', ok4 ? 'PASS' : 'FAIL', JSON.stringify(r4, null, 2))
|
|
37
|
+
|
|
38
|
+
// Test 5: clickable condition should resolve a clickable parent for matching text
|
|
39
|
+
;(Observe as any).ToolsObserve.getUITreeHandler = async () => ({
|
|
40
|
+
elements: [
|
|
41
|
+
{ bounds: [100, 100, 300, 220], visible: true, clickable: true, enabled: true, children: [1] },
|
|
42
|
+
{ text: 'Play session', bounds: [140, 130, 260, 180], visible: true, clickable: false, enabled: true, parentId: 0 }
|
|
43
|
+
]
|
|
44
|
+
})
|
|
45
|
+
const r5 = await ToolsInteract.waitForUIHandler({ selector: { text: 'Play Session', contains: true }, condition: 'clickable', timeout_ms: 1000, poll_interval_ms: 50, platform: 'android' })
|
|
46
|
+
const ok5 = r5 && r5.status === 'success' && r5.element && r5.element.index === 0
|
|
47
|
+
assert.ok(ok5, 'Clickable parent should satisfy clickable condition for child label text')
|
|
48
|
+
console.log('Clickable parent resolution:', ok5 ? 'PASS' : 'FAIL', JSON.stringify(r5, null, 2))
|
|
49
|
+
|
|
50
|
+
// Test 6: retry behavior - first attempt times out, second attempt succeeds
|
|
51
|
+
const start = Date.now()
|
|
52
|
+
let seqTree = async () => {
|
|
53
|
+
const now = Date.now()
|
|
54
|
+
// for first ~400ms return no elements, afterwards return match
|
|
55
|
+
if (now - start < 400) return { elements: [] }
|
|
56
|
+
return { elements: [ { text: 'Retried', resourceId: 'rid5', bounds: [0,0,10,10], visible: true } ] }
|
|
57
|
+
}
|
|
58
|
+
;(Observe as any).ToolsObserve.getUITreeHandler = seqTree
|
|
59
|
+
const r6 = 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' })
|
|
60
|
+
const ok6 = r6 && r6.status === 'success' && r6.metrics && r6.metrics.attempts >= 2
|
|
61
|
+
assert.ok(ok6, 'Retry path should eventually succeed')
|
|
62
|
+
console.log('Retry behavior:', ok6 ? 'PASS' : 'FAIL', JSON.stringify(r6, null, 2))
|
|
63
|
+
|
|
64
|
+
// Test 7: timeout with no selector match -> correct error code
|
|
65
|
+
;(Observe as any).ToolsObserve.getUITreeHandler = async () => ({ elements: [] })
|
|
66
|
+
const r7 = await ToolsInteract.waitForUIHandler({ selector: { text: 'Nope' }, condition: 'exists', timeout_ms: 300, poll_interval_ms: 50, retry: { max_attempts: 1 }, platform: 'android' })
|
|
67
|
+
const ok7 = r7 && r7.status === 'timeout' && r7.error && r7.error.code === 'ELEMENT_NOT_FOUND'
|
|
68
|
+
assert.ok(ok7, 'Missing selector should time out with ELEMENT_NOT_FOUND')
|
|
69
|
+
console.log('Timeout no match:', ok7 ? 'PASS' : 'FAIL', JSON.stringify(r7, null, 2))
|
|
70
|
+
|
|
71
|
+
} finally {
|
|
72
|
+
;(Observe as any).ToolsObserve.getUITreeHandler = origGetUITree
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
run().catch(err => { console.error('wait_for_ui_selector_matching tests failed:', err); process.exit(1) })
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import { ToolsManage } from '../../../src/manage/index.js'
|
|
3
|
+
|
|
4
|
+
async function run() {
|
|
5
|
+
const originalBuildAppHandler = (ToolsManage as any).buildAppHandler
|
|
6
|
+
const originalInstallAppHandler = (ToolsManage as any).installAppHandler
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
;(ToolsManage as any).buildAppHandler = async () => ({ artifactPath: '/tmp/fake.apk' })
|
|
10
|
+
;(ToolsManage as any).installAppHandler = async () => ({
|
|
11
|
+
device: { platform: 'android', id: 'emulator-5554', osVersion: '14', model: 'Pixel', simulator: true },
|
|
12
|
+
installed: true,
|
|
13
|
+
output: 'Installed'
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const response = await ToolsManage.buildAndInstallHandler({
|
|
17
|
+
platform: 'android',
|
|
18
|
+
projectPath: '/tmp/project',
|
|
19
|
+
projectType: 'native'
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const lines = response.ndjson.trim().split('\n').map((line) => JSON.parse(line))
|
|
23
|
+
assert.deepStrictEqual(lines.map((line) => `${line.type}:${line.status}`), [
|
|
24
|
+
'build:started',
|
|
25
|
+
'build:finished',
|
|
26
|
+
'install:started',
|
|
27
|
+
'install:finished'
|
|
28
|
+
])
|
|
29
|
+
assert.strictEqual(response.result.success, true)
|
|
30
|
+
assert.strictEqual((response.result as any).artifactPath, '/tmp/fake.apk')
|
|
31
|
+
assert.strictEqual((response.result as any).device.id, 'emulator-5554')
|
|
32
|
+
|
|
33
|
+
console.log('manage handler shape tests passed')
|
|
34
|
+
} finally {
|
|
35
|
+
;(ToolsManage as any).buildAppHandler = originalBuildAppHandler
|
|
36
|
+
;(ToolsManage as any).installAppHandler = originalInstallAppHandler
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
run().catch((error) => {
|
|
41
|
+
console.error(error)
|
|
42
|
+
process.exit(1)
|
|
43
|
+
})
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ToolsObserve } from '../../../src/observe/index.js'
|
|
2
|
+
import assert from 'assert'
|
|
2
3
|
|
|
3
4
|
async function run() {
|
|
4
5
|
console.log('Starting capture_debug_snapshot unit tests...')
|
|
@@ -35,6 +36,7 @@ async function run() {
|
|
|
35
36
|
const res1: any = await ToolsObserve.captureDebugSnapshotHandler({ platform: 'android', includeLogs: true, logLines: 50, sessionId: 's1' })
|
|
36
37
|
console.log('res1:', JSON.stringify(res1, null, 2))
|
|
37
38
|
const pass1 = res1 && res1.screenshot === 'BASE64PNG' && res1.activity && res1.fingerprint === 'abc123' && Array.isArray(res1.logs) && res1.logs.length === 1
|
|
39
|
+
assert.ok(pass1, 'captureDebugSnapshot should aggregate successful handler results')
|
|
38
40
|
console.log('Test 1:', pass1 ? 'PASS' : 'FAIL')
|
|
39
41
|
|
|
40
42
|
// Restore handlers before next test
|
|
@@ -54,6 +56,7 @@ async function run() {
|
|
|
54
56
|
const res2: any = await ToolsObserve.captureDebugSnapshotHandler({ platform: 'android', includeLogs: true, logLines: 10, appId: 'com.example' })
|
|
55
57
|
console.log('res2:', JSON.stringify(res2, null, 2))
|
|
56
58
|
const pass2 = res2 && res2.screenshot_error && res2.ui_tree_error && Array.isArray(res2.logs) && res2.logs.length === 2
|
|
59
|
+
assert.ok(pass2, 'captureDebugSnapshot should surface partial failures and fallback logs')
|
|
57
60
|
console.log('Test 2:', pass2 ? 'PASS' : 'FAIL')
|
|
58
61
|
|
|
59
62
|
// Restore handlers before next test
|
|
@@ -74,6 +77,7 @@ async function run() {
|
|
|
74
77
|
const res3: any = await ToolsObserve.captureDebugSnapshotHandler({ platform: 'android', includeLogs: false })
|
|
75
78
|
console.log('res3:', JSON.stringify(res3, null, 2))
|
|
76
79
|
const pass3 = res3 && typeof res3.logs !== 'undefined' && res3.logs.length === 0
|
|
80
|
+
assert.ok(pass3, 'captureDebugSnapshot should return an empty logs array when includeLogs is false')
|
|
77
81
|
console.log('Test 3:', pass3 ? 'PASS' : 'FAIL')
|
|
78
82
|
|
|
79
83
|
} finally {
|
|
@@ -86,4 +90,4 @@ async function run() {
|
|
|
86
90
|
}
|
|
87
91
|
}
|
|
88
92
|
|
|
89
|
-
run().catch(console.error)
|
|
93
|
+
run().catch((error) => { console.error(error); process.exit(1) })
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ToolsInteract } from '../../../src/interact/index.js'
|
|
2
2
|
import { ToolsObserve } from '../../../src/observe/index.js'
|
|
3
|
+
import assert from 'assert'
|
|
3
4
|
|
|
4
5
|
async function run() {
|
|
5
6
|
process.stdout.write('Starting find_element unit tests...\n')
|
|
@@ -8,7 +9,7 @@ async function run() {
|
|
|
8
9
|
|
|
9
10
|
try {
|
|
10
11
|
// Test 1: exact text match
|
|
11
|
-
(ToolsObserve as any).getUITreeHandler = async () => ({
|
|
12
|
+
;(ToolsObserve as any).getUITreeHandler = async () => ({
|
|
12
13
|
device: { platform: 'android', id: 'mock' },
|
|
13
14
|
screen: '',
|
|
14
15
|
resolution: { width: 1080, height: 1920 },
|
|
@@ -21,10 +22,11 @@ async function run() {
|
|
|
21
22
|
const res1: any = await ToolsInteract.findElementHandler({ query: 'login', exact: true, platform: 'android' })
|
|
22
23
|
process.stdout.write('res1 ' + JSON.stringify(res1, null, 2) + '\n');
|
|
23
24
|
const pass1 = res1.found === true && res1.element && res1.element.resourceId === 'btn_login' && res1.element.tapCoordinates && typeof res1.element.tapCoordinates.x === 'number' && typeof res1.element.tapCoordinates.y === 'number' && typeof res1.confidence === 'number'
|
|
25
|
+
assert.ok(pass1, 'Exact text match should find the actionable login button')
|
|
24
26
|
process.stdout.write('Test 1: ' + (pass1 ? 'PASS' : 'FAIL') + '\n');
|
|
25
27
|
|
|
26
28
|
// Test 2: partial match & scoring
|
|
27
|
-
(ToolsObserve as any).getUITreeHandler = async () => ({
|
|
29
|
+
;(ToolsObserve as any).getUITreeHandler = async () => ({
|
|
28
30
|
device: { platform: 'android', id: 'mock' },
|
|
29
31
|
screen: '',
|
|
30
32
|
resolution: { width: 1080, height: 1920 },
|
|
@@ -37,10 +39,11 @@ async function run() {
|
|
|
37
39
|
const res2: any = await ToolsInteract.findElementHandler({ query: 'login', exact: false, platform: 'android' })
|
|
38
40
|
process.stdout.write('res2 ' + JSON.stringify(res2, null, 2) + '\n');
|
|
39
41
|
const pass2 = res2.found === true && res2.element && res2.element.resourceId === 'btn_login_email' && res2.element.tapCoordinates && typeof res2.element.tapCoordinates.x === 'number' && typeof res2.element.tapCoordinates.y === 'number' && typeof res2.confidence === 'number'
|
|
42
|
+
assert.ok(pass2, 'Partial text matching should pick the best scoring element')
|
|
40
43
|
process.stdout.write('Test 2: ' + (pass2 ? 'PASS' : 'FAIL') + '\n');
|
|
41
44
|
|
|
42
45
|
// Test 3: resourceId match
|
|
43
|
-
(ToolsObserve as any).getUITreeHandler = async () => ({
|
|
46
|
+
;(ToolsObserve as any).getUITreeHandler = async () => ({
|
|
44
47
|
device: { platform: 'android', id: 'mock' },
|
|
45
48
|
screen: '',
|
|
46
49
|
resolution: { width: 1080, height: 1920 },
|
|
@@ -52,10 +55,11 @@ async function run() {
|
|
|
52
55
|
const res3: any = await ToolsInteract.findElementHandler({ query: 'icon_login', exact: false, platform: 'android' })
|
|
53
56
|
process.stdout.write('res3 ' + JSON.stringify(res3, null, 2) + '\n');
|
|
54
57
|
const pass3 = res3.found === true && res3.element && res3.element.resourceId === 'icon_login' && res3.element.tapCoordinates && typeof res3.element.tapCoordinates.x === 'number' && typeof res3.element.tapCoordinates.y === 'number' && typeof res3.confidence === 'number'
|
|
58
|
+
assert.ok(pass3, 'Resource-id matching should find icon_login')
|
|
55
59
|
process.stdout.write('Test 3: ' + (pass3 ? 'PASS' : 'FAIL') + '\n');
|
|
56
60
|
|
|
57
61
|
// Test 4: parent-clickable child-text scenario
|
|
58
|
-
(ToolsObserve as any).getUITreeHandler = async () => ({
|
|
62
|
+
;(ToolsObserve as any).getUITreeHandler = async () => ({
|
|
59
63
|
device: { platform: 'android', id: 'mock' },
|
|
60
64
|
screen: '',
|
|
61
65
|
resolution: { width: 1080, height: 1920 },
|
|
@@ -68,13 +72,15 @@ async function run() {
|
|
|
68
72
|
const res4: any = await ToolsInteract.findElementHandler({ query: 'generate', exact: false, platform: 'android', timeoutMs: 300 })
|
|
69
73
|
process.stdout.write('res4 ' + JSON.stringify(res4, null, 2) + '\n');
|
|
70
74
|
const pass4 = res4.found === true && res4.element && res4.element.clickable === true && res4.element.resourceId === 'btn_generate' && res4.element.tapCoordinates && typeof res4.element.tapCoordinates.x === 'number' && typeof res4.element.tapCoordinates.y === 'number' && typeof res4.confidence === 'number'
|
|
75
|
+
assert.ok(pass4, 'Child text should resolve to a clickable parent ancestor')
|
|
71
76
|
process.stdout.write('Test 4: ' + (pass4 ? 'PASS' : 'FAIL') + '\n');
|
|
72
77
|
|
|
73
78
|
// Test 5: not found
|
|
74
|
-
(ToolsObserve as any).getUITreeHandler = async () => ({ device: { platform: 'android', id: 'mock' }, screen: '', resolution: { width: 1080, height: 1920 }, elements: [] })
|
|
79
|
+
;(ToolsObserve as any).getUITreeHandler = async () => ({ device: { platform: 'android', id: 'mock' }, screen: '', resolution: { width: 1080, height: 1920 }, elements: [] })
|
|
75
80
|
const res5: any = await ToolsInteract.findElementHandler({ query: 'nope', exact: false, platform: 'android', timeoutMs: 300 })
|
|
76
81
|
process.stdout.write('res5 ' + JSON.stringify(res5, null, 2) + '\n');
|
|
77
82
|
const pass5 = res5.found === false
|
|
83
|
+
assert.ok(pass5, 'Missing elements should return found=false')
|
|
78
84
|
process.stdout.write('Test 5: ' + (pass5 ? 'PASS' : 'FAIL') + '\n');
|
|
79
85
|
|
|
80
86
|
} finally {
|
|
@@ -82,4 +88,4 @@ async function run() {
|
|
|
82
88
|
}
|
|
83
89
|
}
|
|
84
90
|
|
|
85
|
-
run().catch(console.error)
|
|
91
|
+
run().catch((error) => { console.error(error); process.exit(1) })
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { AndroidObserve } from '../../../src/observe/index.js'
|
|
2
|
+
import assert from 'assert'
|
|
3
|
+
|
|
4
|
+
async function run() {
|
|
5
|
+
console.log('Starting get_screen_fingerprint unit tests...')
|
|
6
|
+
|
|
7
|
+
const origGet = (AndroidObserve as any).prototype.getUITree
|
|
8
|
+
const origCurrent = (AndroidObserve as any).prototype.getCurrentScreen
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
;(AndroidObserve as any).prototype.getUITree = async function() {
|
|
12
|
+
return {
|
|
13
|
+
device: { platform: 'android', id: 'mock', osVersion: '12', model: 'Pixel', simulator: true },
|
|
14
|
+
screen: '',
|
|
15
|
+
resolution: { width: 1080, height: 1920 },
|
|
16
|
+
elements: [
|
|
17
|
+
{ text: 'Title', type: 'TextView', contentDescription: null, clickable: false, enabled: true, visible: true, bounds: [0,0,1080,100], resourceId: 'id/title' },
|
|
18
|
+
{ text: 'Sign in', type: 'Button', contentDescription: null, clickable: true, enabled: true, visible: true, bounds: [0,200,200,260], resourceId: 'id/signin' }
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
;(AndroidObserve as any).prototype.getCurrentScreen = async function() {
|
|
24
|
+
return { device: { platform: 'android', id: 'mock', osVersion: '12', model: 'Pixel', simulator: true }, package: 'com.example', activity: 'com.example.MainActivity', shortActivity: 'MainActivity' }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const ai = new AndroidObserve()
|
|
28
|
+
const a = await ai.getScreenFingerprint('mock')
|
|
29
|
+
const b = await ai.getScreenFingerprint('mock')
|
|
30
|
+
assert.strictEqual(a.fingerprint, b.fingerprint, 'Identical screens should produce the same fingerprint')
|
|
31
|
+
console.log('Test 1: PASS')
|
|
32
|
+
|
|
33
|
+
;(AndroidObserve as any).prototype.getUITree = async function() {
|
|
34
|
+
return {
|
|
35
|
+
device: { platform: 'android', id: 'mock', osVersion: '12', model: 'Pixel', simulator: true },
|
|
36
|
+
screen: '',
|
|
37
|
+
resolution: { width: 1080, height: 1920 },
|
|
38
|
+
elements: [
|
|
39
|
+
{ text: 'Title', type: 'TextView', contentDescription: null, clickable: false, enabled: true, visible: true, bounds: [0,0,1080,100], resourceId: 'id/title' },
|
|
40
|
+
{ text: 'Profile', type: 'Button', contentDescription: null, clickable: true, enabled: true, visible: true, bounds: [0,200,200,260], resourceId: 'id/signin' }
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const c = await ai.getScreenFingerprint('mock')
|
|
46
|
+
assert.notStrictEqual(a.fingerprint, c.fingerprint, 'Meaningful UI text changes should change the fingerprint')
|
|
47
|
+
console.log('Test 2: PASS')
|
|
48
|
+
|
|
49
|
+
;(AndroidObserve as any).prototype.getUITree = async function() {
|
|
50
|
+
return {
|
|
51
|
+
device: { platform: 'android', id: 'mock', osVersion: '12', model: 'Pixel', simulator: true },
|
|
52
|
+
screen: '',
|
|
53
|
+
resolution: { width: 1080, height: 1920 },
|
|
54
|
+
elements: [
|
|
55
|
+
{ text: 'Title', type: 'TextView', contentDescription: null, clickable: false, enabled: true, visible: true, bounds: [0,0,1080,100], resourceId: 'id/title' },
|
|
56
|
+
{ text: 'Sign in', type: 'Button', contentDescription: null, clickable: true, enabled: true, visible: true, bounds: [0,200,200,260], resourceId: 'id/signin' },
|
|
57
|
+
{ text: '12:34', type: 'TextView', contentDescription: null, clickable: false, enabled: true, visible: true, bounds: [900,10,1080,40], resourceId: null }
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const d = await ai.getScreenFingerprint('mock')
|
|
63
|
+
assert.strictEqual(a.fingerprint, d.fingerprint, 'Dynamic timestamp-like text should be ignored')
|
|
64
|
+
console.log('Test 3: PASS')
|
|
65
|
+
} finally {
|
|
66
|
+
;(AndroidObserve as any).prototype.getUITree = origGet
|
|
67
|
+
;(AndroidObserve as any).prototype.getCurrentScreen = origCurrent
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
run().catch((error) => { console.error(error); process.exit(1) })
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { iOSObserve, _resetIOSExecCommandForTests, _setIOSExecCommandForTests } from '../../../src/observe/ios'
|
|
2
|
+
import assert from 'assert'
|
|
3
|
+
|
|
4
|
+
function stubExecCommand(expectedArgsChecker: (args: string[]) => boolean, output: string) {
|
|
5
|
+
return async function (args: string[], deviceId?: string) {
|
|
6
|
+
if (!expectedArgsChecker(args)) throw new Error('Unexpected args: ' + JSON.stringify(args))
|
|
7
|
+
return { output, device: { platform: 'ios', id: deviceId || 'booted' } }
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function run() {
|
|
12
|
+
const bundle = 'com.ideamechanics.modul8'
|
|
13
|
+
const pgrepOutput = '12345\n'
|
|
14
|
+
const logOutput = '2026-03-31 09:21:20.085 Module[12345:678] <Info> Modul8: Test message'
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const obs = new iOSObserve()
|
|
18
|
+
_setIOSExecCommandForTests(stubExecCommand((args) => args.includes('pgrep'), pgrepOutput))
|
|
19
|
+
|
|
20
|
+
let called = false
|
|
21
|
+
_setIOSExecCommandForTests(async function (args: string[]) {
|
|
22
|
+
if (args.includes('pgrep')) return { output: pgrepOutput, device: { platform: 'ios', id: 'booted' } }
|
|
23
|
+
if (args.includes('log') && args.includes('show')) {
|
|
24
|
+
called = true
|
|
25
|
+
return { output: logOutput, device: { platform: 'ios', id: 'booted' } }
|
|
26
|
+
}
|
|
27
|
+
throw new Error('Unexpected args: ' + JSON.stringify(args))
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const pidResult = await obs.getLogs({ appId: bundle, deviceId: 'booted' })
|
|
31
|
+
assert(pidResult.meta.processNameUsed === 'modul8' || pidResult.meta.processNameUsed === 'Modul8' || !!pidResult.meta.processNameUsed)
|
|
32
|
+
assert(pidResult.meta.detectedPid === 12345)
|
|
33
|
+
assert(pidResult.source === 'pid')
|
|
34
|
+
assert(pidResult.logCount === 1)
|
|
35
|
+
assert(pidResult.logs[0].message.includes('Test message'))
|
|
36
|
+
assert(called, 'log show must have been called')
|
|
37
|
+
|
|
38
|
+
_setIOSExecCommandForTests(async function (args: string[]) {
|
|
39
|
+
if (args.includes('log') && args.includes('show')) return { output: '2026-03-31 09:21:20.085 SomeOther[222:333] <Info> Other: Hello', device: { platform: 'ios', id: 'booted' } }
|
|
40
|
+
throw new Error('Unexpected args: ' + JSON.stringify(args))
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const broadResult = await new iOSObserve().getLogs({ deviceId: 'booted' })
|
|
44
|
+
assert(broadResult.source === 'broad')
|
|
45
|
+
assert(broadResult.logCount === 1)
|
|
46
|
+
|
|
47
|
+
console.log('iOS getLogs predicate and meta tests passed')
|
|
48
|
+
} finally {
|
|
49
|
+
_resetIOSExecCommandForTests()
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
run().catch((error) => { console.error(error); process.exit(1) })
|