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
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import { handleToolCall } from '../../../src/server-core.js'
|
|
3
|
+
import { ToolsManage } from '../../../src/manage/index.js'
|
|
4
|
+
import { ToolsInteract } from '../../../src/interact/index.js'
|
|
5
|
+
import { ToolsObserve } from '../../../src/observe/index.js'
|
|
6
|
+
|
|
7
|
+
async function run() {
|
|
8
|
+
const originalInstallAppHandler = (ToolsManage as any).installAppHandler
|
|
9
|
+
const originalWaitForUIHandler = (ToolsInteract as any).waitForUIHandler
|
|
10
|
+
const originalTapElementHandler = (ToolsInteract as any).tapElementHandler
|
|
11
|
+
const originalCaptureScreenshotHandler = (ToolsObserve as any).captureScreenshotHandler
|
|
12
|
+
const originalGetUITreeHandler = (ToolsObserve as any).getUITreeHandler
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
;(ToolsManage as any).installAppHandler = async () => ({
|
|
16
|
+
device: { platform: 'android', id: 'emulator-5554', osVersion: '14', model: 'Pixel', simulator: true },
|
|
17
|
+
installed: true,
|
|
18
|
+
output: 'Success'
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const installResponse = await handleToolCall('install_app', { platform: 'android', projectType: 'native', appPath: '/tmp/app.apk' })
|
|
22
|
+
assert.strictEqual((installResponse as any).content.length, 1)
|
|
23
|
+
const installPayload = JSON.parse((installResponse as any).content[0].text)
|
|
24
|
+
assert.strictEqual(installPayload.installed, true)
|
|
25
|
+
assert.strictEqual(installPayload.output, 'Success')
|
|
26
|
+
assert.strictEqual(installPayload.device.id, 'emulator-5554')
|
|
27
|
+
|
|
28
|
+
;(ToolsInteract as any).waitForUIHandler = async () => ({
|
|
29
|
+
status: 'success',
|
|
30
|
+
matched: 1,
|
|
31
|
+
element: { text: 'Ready', bounds: [0, 0, 10, 10], index: 0, elementId: 'el_ready' },
|
|
32
|
+
metrics: { latency_ms: 12, poll_count: 1, attempts: 1 }
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const waitForUIResponse = await handleToolCall('wait_for_ui', { selector: { text: 'Ready' } })
|
|
36
|
+
const waitForUIPayload = JSON.parse((waitForUIResponse as any).content[0].text)
|
|
37
|
+
assert.strictEqual(waitForUIPayload.status, 'success')
|
|
38
|
+
assert.strictEqual(waitForUIPayload.metrics.poll_count, 1)
|
|
39
|
+
assert.strictEqual(waitForUIPayload.element.text, 'Ready')
|
|
40
|
+
assert.strictEqual(waitForUIPayload.element.elementId, 'el_ready')
|
|
41
|
+
|
|
42
|
+
;(ToolsInteract as any).tapElementHandler = async () => ({
|
|
43
|
+
success: true,
|
|
44
|
+
elementId: 'el_ready',
|
|
45
|
+
action: 'tap'
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const tapElementResponse = await handleToolCall('tap_element', { elementId: 'el_ready' })
|
|
49
|
+
const tapElementPayload = JSON.parse((tapElementResponse as any).content[0].text)
|
|
50
|
+
assert.strictEqual(tapElementPayload.success, true)
|
|
51
|
+
assert.strictEqual(tapElementPayload.elementId, 'el_ready')
|
|
52
|
+
assert.strictEqual(tapElementPayload.action, 'tap')
|
|
53
|
+
|
|
54
|
+
;(ToolsObserve as any).captureScreenshotHandler = async () => ({
|
|
55
|
+
device: { platform: 'ios', id: 'booted', osVersion: '18.0', model: 'Simulator', simulator: true },
|
|
56
|
+
screenshot: Buffer.from('png-data').toString('base64'),
|
|
57
|
+
screenshot_mime: 'image/png',
|
|
58
|
+
resolution: { width: 390, height: 844 }
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const screenshotResponse = await handleToolCall('capture_screenshot', { platform: 'ios' })
|
|
62
|
+
assert.strictEqual((screenshotResponse as any).content.length, 2)
|
|
63
|
+
const screenshotMeta = JSON.parse((screenshotResponse as any).content[0].text)
|
|
64
|
+
assert.strictEqual((screenshotResponse as any).content[1].type, 'image')
|
|
65
|
+
assert.strictEqual((screenshotResponse as any).content[1].mimeType, 'image/png')
|
|
66
|
+
assert.strictEqual(screenshotMeta.result.resolution.width, 390)
|
|
67
|
+
|
|
68
|
+
;(ToolsObserve as any).getUITreeHandler = async () => ({
|
|
69
|
+
device: { platform: 'android', id: 'mock', osVersion: '14', model: 'Pixel', simulator: true },
|
|
70
|
+
resolution: { width: 1080, height: 2400 },
|
|
71
|
+
elements: [{ text: 'Login', depth: 0, center: { x: 50, y: 20 } }]
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const uiTreeResponse = await handleToolCall('get_ui_tree', { platform: 'android' })
|
|
75
|
+
const uiTreePayload = JSON.parse((uiTreeResponse as any).content[0].text)
|
|
76
|
+
assert.strictEqual(uiTreePayload.elements.length, 1)
|
|
77
|
+
assert.strictEqual(uiTreePayload.resolution.height, 2400)
|
|
78
|
+
assert.strictEqual(uiTreePayload.elements[0].text, 'Login')
|
|
79
|
+
|
|
80
|
+
console.log('server response-shape tests passed')
|
|
81
|
+
} finally {
|
|
82
|
+
;(ToolsManage as any).installAppHandler = originalInstallAppHandler
|
|
83
|
+
;(ToolsInteract as any).waitForUIHandler = originalWaitForUIHandler
|
|
84
|
+
;(ToolsInteract as any).tapElementHandler = originalTapElementHandler
|
|
85
|
+
;(ToolsObserve as any).captureScreenshotHandler = originalCaptureScreenshotHandler
|
|
86
|
+
;(ToolsObserve as any).getUITreeHandler = originalGetUITreeHandler
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
run().catch((error) => {
|
|
91
|
+
console.error(error)
|
|
92
|
+
process.exit(1)
|
|
93
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import fs from 'fs/promises'
|
|
3
|
+
import os from 'os'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
import * as systemStatus from '../../../src/system/index.js'
|
|
6
|
+
|
|
7
|
+
async function run() {
|
|
8
|
+
const binDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-system-adb-'))
|
|
9
|
+
const adbPath = path.join(binDir, 'adb')
|
|
10
|
+
const originalAdbPath = process.env.ADB_PATH
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
await fs.writeFile(adbPath, `#!/bin/sh
|
|
14
|
+
if [ "$1" = "--version" ]; then
|
|
15
|
+
printf 'Android Debug Bridge version 1.0.41\nRevision 8f3b7\n'
|
|
16
|
+
exit 0
|
|
17
|
+
fi
|
|
18
|
+
if [ "$1" = "devices" ]; then
|
|
19
|
+
printf 'List of devices attached\n'
|
|
20
|
+
exit 0
|
|
21
|
+
fi
|
|
22
|
+
exit 0
|
|
23
|
+
`, { mode: 0o755 })
|
|
24
|
+
process.env.ADB_PATH = adbPath
|
|
25
|
+
const res = await systemStatus.getSystemStatus()
|
|
26
|
+
assert.strictEqual(res.adbVersion, 'Android Debug Bridge version 1.0.41')
|
|
27
|
+
console.log('adb version parsing test passed')
|
|
28
|
+
} finally {
|
|
29
|
+
if (originalAdbPath === undefined) delete process.env.ADB_PATH
|
|
30
|
+
else process.env.ADB_PATH = originalAdbPath
|
|
31
|
+
await fs.rm(binDir, { recursive: true, force: true }).catch(() => {})
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
run().catch((error) => { console.error(error); process.exit(1) })
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import { ensureAdbAvailable } from '../../../src/utils/android/utils.js'
|
|
3
|
+
import { getXcrunCmd } from '../../../src/utils/ios/utils.js'
|
|
4
|
+
import { getSystemStatus } from '../../../src/system/index.js'
|
|
5
|
+
|
|
6
|
+
async function run() {
|
|
7
|
+
const payload = await getSystemStatus()
|
|
8
|
+
assert(typeof payload.success === 'boolean')
|
|
9
|
+
assert(Array.isArray(payload.issues))
|
|
10
|
+
|
|
11
|
+
const adb = ensureAdbAvailable()
|
|
12
|
+
assert(adb && typeof adb.ok === 'boolean')
|
|
13
|
+
|
|
14
|
+
const cmd = getXcrunCmd()
|
|
15
|
+
assert(typeof cmd === 'string')
|
|
16
|
+
|
|
17
|
+
console.log('get_system_status unit tests passed')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
run().catch((error) => { console.error(error); process.exit(1) })
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import fs from 'fs/promises'
|
|
3
|
+
import os from 'os'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
|
|
6
|
+
import * as systemStatus from '../../../src/system/index.js'
|
|
7
|
+
|
|
8
|
+
async function writeFakeCommands(binDir: string) {
|
|
9
|
+
const adbPath = path.join(binDir, 'adb')
|
|
10
|
+
const xcrunPath = path.join(binDir, 'xcrun')
|
|
11
|
+
|
|
12
|
+
await fs.writeFile(adbPath, `#!/bin/sh
|
|
13
|
+
if [ "$1" = "--version" ]; then
|
|
14
|
+
if [ "\${ADB_VERSION_STATUS:-0}" != "0" ]; then
|
|
15
|
+
printf '%s' "\${ADB_VERSION_OUTPUT:-not found}" >&2
|
|
16
|
+
exit "\${ADB_VERSION_STATUS}"
|
|
17
|
+
fi
|
|
18
|
+
printf '%s' "\${ADB_VERSION_OUTPUT:-Android Debug Bridge version 8.1.0}"
|
|
19
|
+
exit 0
|
|
20
|
+
fi
|
|
21
|
+
if [ "$1" = "devices" ]; then
|
|
22
|
+
printf '%s' "\${ADB_DEVICES_OUTPUT:-List of devices attached}"
|
|
23
|
+
exit 0
|
|
24
|
+
fi
|
|
25
|
+
if [ "$1" = "logcat" ]; then
|
|
26
|
+
printf '%s' "\${ADB_LOGCAT_OUTPUT:-I/Tag: ok}"
|
|
27
|
+
exit 0
|
|
28
|
+
fi
|
|
29
|
+
if [ "$1" = "shell" ] && [ "$2" = "pm" ] && [ "$3" = "path" ]; then
|
|
30
|
+
printf '%s' "\${ADB_PM_PATH_OUTPUT:-package:/data/app/com.example/base.apk}"
|
|
31
|
+
exit 0
|
|
32
|
+
fi
|
|
33
|
+
printf '%s' "\${ADB_DEFAULT_OUTPUT:-OK}"
|
|
34
|
+
exit 0
|
|
35
|
+
`, { mode: 0o755 })
|
|
36
|
+
|
|
37
|
+
await fs.writeFile(xcrunPath, `#!/bin/sh
|
|
38
|
+
if [ "$1" = "--version" ]; then
|
|
39
|
+
if [ "\${XCRUN_VERSION_STATUS:-0}" != "0" ]; then
|
|
40
|
+
printf '%s' "\${XCRUN_VERSION_OUTPUT:-not found}" >&2
|
|
41
|
+
exit "\${XCRUN_VERSION_STATUS}"
|
|
42
|
+
fi
|
|
43
|
+
printf '%s' "\${XCRUN_VERSION_OUTPUT:-xcrun version 123}"
|
|
44
|
+
exit 0
|
|
45
|
+
fi
|
|
46
|
+
if [ "$1" = "simctl" ] && [ "$2" = "list" ] && [ "$3" = "devices" ] && [ "$4" = "booted" ] && [ "$5" = "--json" ]; then
|
|
47
|
+
printf '%s' "\${SIMCTL_LIST_OUTPUT:-{\"devices\":{}}}"
|
|
48
|
+
exit 0
|
|
49
|
+
fi
|
|
50
|
+
printf '%s' "\${XCRUN_DEFAULT_OUTPUT:-ok}"
|
|
51
|
+
exit 0
|
|
52
|
+
`, { mode: 0o755 })
|
|
53
|
+
|
|
54
|
+
return { adbPath, xcrunPath }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function setScenario(env: Record<string, string | undefined>) {
|
|
58
|
+
for (const [key, value] of Object.entries(env)) {
|
|
59
|
+
if (value === undefined) delete process.env[key]
|
|
60
|
+
else process.env[key] = value
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function run() {
|
|
65
|
+
const binDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-system-status-'))
|
|
66
|
+
const originalEnv = {
|
|
67
|
+
ADB_PATH: process.env.ADB_PATH,
|
|
68
|
+
XCRUN_PATH: process.env.XCRUN_PATH,
|
|
69
|
+
ADB_VERSION_OUTPUT: process.env.ADB_VERSION_OUTPUT,
|
|
70
|
+
ADB_VERSION_STATUS: process.env.ADB_VERSION_STATUS,
|
|
71
|
+
ADB_DEVICES_OUTPUT: process.env.ADB_DEVICES_OUTPUT,
|
|
72
|
+
ADB_LOGCAT_OUTPUT: process.env.ADB_LOGCAT_OUTPUT,
|
|
73
|
+
ADB_PM_PATH_OUTPUT: process.env.ADB_PM_PATH_OUTPUT,
|
|
74
|
+
XCRUN_VERSION_OUTPUT: process.env.XCRUN_VERSION_OUTPUT,
|
|
75
|
+
XCRUN_VERSION_STATUS: process.env.XCRUN_VERSION_STATUS,
|
|
76
|
+
SIMCTL_LIST_OUTPUT: process.env.SIMCTL_LIST_OUTPUT,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const { adbPath, xcrunPath } = await writeFakeCommands(binDir)
|
|
81
|
+
process.env.ADB_PATH = adbPath
|
|
82
|
+
process.env.XCRUN_PATH = xcrunPath
|
|
83
|
+
|
|
84
|
+
setScenario({
|
|
85
|
+
ADB_VERSION_STATUS: '0',
|
|
86
|
+
ADB_VERSION_OUTPUT: '8.1.0\n',
|
|
87
|
+
ADB_DEVICES_OUTPUT: 'List of devices attached\nemulator-5554\tdevice product:sdk\n',
|
|
88
|
+
ADB_LOGCAT_OUTPUT: 'I/Tag: ok\n',
|
|
89
|
+
XCRUN_VERSION_STATUS: '0',
|
|
90
|
+
XCRUN_VERSION_OUTPUT: 'xcrun version 123\n',
|
|
91
|
+
SIMCTL_LIST_OUTPUT: JSON.stringify({ devices: { runtime: [{ state: 'Booted' }] } }),
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const healthy = await systemStatus.getSystemStatus()
|
|
95
|
+
assert.strictEqual(healthy.success, true)
|
|
96
|
+
assert.strictEqual(healthy.adbAvailable, true)
|
|
97
|
+
assert.strictEqual(typeof healthy.adbVersion, 'string')
|
|
98
|
+
|
|
99
|
+
setScenario({
|
|
100
|
+
ADB_VERSION_STATUS: '1',
|
|
101
|
+
ADB_VERSION_OUTPUT: 'not found',
|
|
102
|
+
ADB_DEVICES_OUTPUT: 'List of devices attached\n',
|
|
103
|
+
XCRUN_VERSION_STATUS: '0',
|
|
104
|
+
XCRUN_VERSION_OUTPUT: 'xcrun version\n',
|
|
105
|
+
})
|
|
106
|
+
const missingAdb = await systemStatus.getSystemStatus()
|
|
107
|
+
assert.strictEqual(missingAdb.success, false)
|
|
108
|
+
assert(missingAdb.issues.some((issue: string) => issue.includes('ADB')))
|
|
109
|
+
|
|
110
|
+
setScenario({
|
|
111
|
+
ADB_VERSION_STATUS: '0',
|
|
112
|
+
ADB_VERSION_OUTPUT: '8.1.0\n',
|
|
113
|
+
ADB_DEVICES_OUTPUT: 'List of devices attached\nserial1\tunauthorized\nserial2\toffline\n',
|
|
114
|
+
XCRUN_VERSION_STATUS: '0',
|
|
115
|
+
XCRUN_VERSION_OUTPUT: 'xcrun version\n',
|
|
116
|
+
})
|
|
117
|
+
const unauthorized = await systemStatus.getSystemStatus()
|
|
118
|
+
assert.strictEqual(unauthorized.success, false)
|
|
119
|
+
assert(unauthorized.issues.some((issue: string) => issue.includes('unauthorized')))
|
|
120
|
+
assert(unauthorized.issues.some((issue: string) => issue.includes('offline')))
|
|
121
|
+
|
|
122
|
+
setScenario({
|
|
123
|
+
ADB_VERSION_STATUS: '0',
|
|
124
|
+
ADB_VERSION_OUTPUT: '8.1.0\n',
|
|
125
|
+
ADB_DEVICES_OUTPUT: 'List of devices attached\nemulator-5554\tdevice product:sdk\n',
|
|
126
|
+
XCRUN_VERSION_STATUS: '1',
|
|
127
|
+
XCRUN_VERSION_OUTPUT: 'not found',
|
|
128
|
+
})
|
|
129
|
+
const missingXcrun = await systemStatus.getSystemStatus()
|
|
130
|
+
assert.strictEqual(missingXcrun.iosAvailable, false)
|
|
131
|
+
assert.strictEqual(missingXcrun.adbAvailable, true)
|
|
132
|
+
assert.strictEqual(Array.isArray(missingXcrun.issues), true)
|
|
133
|
+
|
|
134
|
+
console.log('system_status checks passed')
|
|
135
|
+
} finally {
|
|
136
|
+
setScenario(originalEnv)
|
|
137
|
+
await fs.rm(binDir, { recursive: true, force: true }).catch(() => {})
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
run().catch((error) => { console.error(error); process.exit(1) })
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import fs from 'fs/promises'
|
|
3
|
+
import os from 'os'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
import { execAdbWithDiagnostics } from '../../../src/utils/diagnostics.js'
|
|
6
|
+
import { execCmd } from '../../../src/utils/exec.js'
|
|
7
|
+
|
|
8
|
+
async function run() {
|
|
9
|
+
const envResult = await execCmd(process.execPath, ['-e', 'console.log(process.env.MCP_EXEC_TEST); console.error("warn");'], {
|
|
10
|
+
env: { MCP_EXEC_TEST: 'hello' }
|
|
11
|
+
})
|
|
12
|
+
assert.strictEqual(envResult.exitCode, 0)
|
|
13
|
+
assert.strictEqual(envResult.stdout, 'hello')
|
|
14
|
+
assert.strictEqual(envResult.stderr, 'warn')
|
|
15
|
+
|
|
16
|
+
const timeoutResult = await execCmd(process.execPath, ['-e', 'setTimeout(() => console.log("late"), 200)'], {
|
|
17
|
+
timeout: 50
|
|
18
|
+
})
|
|
19
|
+
assert.strictEqual(timeoutResult.exitCode, null)
|
|
20
|
+
assert.strictEqual(timeoutResult.stdout, '')
|
|
21
|
+
|
|
22
|
+
const binDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-exec-adb-'))
|
|
23
|
+
const adbPath = path.join(binDir, 'adb')
|
|
24
|
+
const script = `#!/bin/sh
|
|
25
|
+
echo "device not found" >&2
|
|
26
|
+
exit 1
|
|
27
|
+
`
|
|
28
|
+
const originalAdbPath = process.env.ADB_PATH
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
await fs.writeFile(adbPath, script, { mode: 0o755 })
|
|
32
|
+
process.env.ADB_PATH = adbPath
|
|
33
|
+
|
|
34
|
+
const adbResult = execAdbWithDiagnostics(['shell', 'getprop'], 'emulator-5554')
|
|
35
|
+
assert.strictEqual(adbResult.runResult.command, adbPath)
|
|
36
|
+
assert.deepStrictEqual(adbResult.runResult.args.slice(0, 2), ['-s', 'emulator-5554'])
|
|
37
|
+
assert.match(adbResult.runResult.stderr, /device not found/)
|
|
38
|
+
assert(adbResult.runResult.suggestedFixes?.some((fix) => fix.includes('adb devices')))
|
|
39
|
+
|
|
40
|
+
console.log('exec utility tests passed')
|
|
41
|
+
} finally {
|
|
42
|
+
if (typeof originalAdbPath === 'undefined') delete process.env.ADB_PATH
|
|
43
|
+
else process.env.ADB_PATH = originalAdbPath
|
|
44
|
+
await fs.rm(binDir, { recursive: true, force: true }).catch(() => {})
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
run().catch((error) => {
|
|
49
|
+
console.error(error)
|
|
50
|
+
process.exit(1)
|
|
51
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import {
|
|
3
|
+
listDevices,
|
|
4
|
+
resolveTargetDevice,
|
|
5
|
+
_setDeviceListersForTests,
|
|
6
|
+
_resetDeviceListersForTests
|
|
7
|
+
} from '../../../src/utils/resolve-device.js'
|
|
8
|
+
|
|
9
|
+
async function run() {
|
|
10
|
+
try {
|
|
11
|
+
_setDeviceListersForTests({
|
|
12
|
+
listAndroidDevices: async () => ([
|
|
13
|
+
{ id: 'android-1', platform: 'android', osVersion: '14', model: 'Pixel 8', simulator: true },
|
|
14
|
+
{ id: 'android-2', platform: 'android', osVersion: '13', model: 'Pixel 7', simulator: false, appInstalled: true } as any
|
|
15
|
+
]),
|
|
16
|
+
listIOSDevices: async () => ([
|
|
17
|
+
{ id: 'ios-1', platform: 'ios', osVersion: '18.0', model: 'iPhone 15', simulator: true, booted: false } as any,
|
|
18
|
+
{ id: 'ios-2', platform: 'ios', osVersion: '17.4', model: 'iPhone 14', simulator: true, booted: true } as any
|
|
19
|
+
])
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const combined = await listDevices()
|
|
23
|
+
assert.strictEqual(combined.length, 4)
|
|
24
|
+
|
|
25
|
+
const explicit = await resolveTargetDevice({ platform: 'android', deviceId: 'android-1' })
|
|
26
|
+
assert.strictEqual(explicit.id, 'android-1')
|
|
27
|
+
|
|
28
|
+
const preferredPhysical = await resolveTargetDevice({ platform: 'android', prefer: 'physical' })
|
|
29
|
+
assert.strictEqual(preferredPhysical.id, 'android-2')
|
|
30
|
+
|
|
31
|
+
const installedForApp = await resolveTargetDevice({ platform: 'android', appId: 'com.example.app' })
|
|
32
|
+
assert.strictEqual(installedForApp.id, 'android-2')
|
|
33
|
+
|
|
34
|
+
const bootedIOS = await resolveTargetDevice({ platform: 'ios' })
|
|
35
|
+
assert.strictEqual(bootedIOS.id, 'ios-2')
|
|
36
|
+
|
|
37
|
+
_setDeviceListersForTests({
|
|
38
|
+
listAndroidDevices: async () => ([
|
|
39
|
+
{ id: 'android-a', platform: 'android', osVersion: '14.0', model: 'Pixel 8', simulator: false },
|
|
40
|
+
{ id: 'android-b', platform: 'android', osVersion: '14.0', model: 'Pixel 8 Pro', simulator: false }
|
|
41
|
+
])
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
await assert.rejects(
|
|
45
|
+
() => resolveTargetDevice({ platform: 'android' }),
|
|
46
|
+
(error: unknown) => {
|
|
47
|
+
assert(error instanceof Error)
|
|
48
|
+
assert.match(error.message, /Multiple matching devices found/)
|
|
49
|
+
assert.strictEqual(Array.isArray((error as any).devices), true)
|
|
50
|
+
return true
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
console.log('resolve-device tests passed')
|
|
55
|
+
} finally {
|
|
56
|
+
_resetDeviceListersForTests()
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
run().catch((error) => {
|
|
61
|
+
console.error(error)
|
|
62
|
+
process.exit(1)
|
|
63
|
+
})
|
package/tsconfig.json
CHANGED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { ToolsInteract } from '../../../src/interact/index.js'
|
|
2
|
-
import * as Observe from '../../../src/observe/index.js'
|
|
3
|
-
|
|
4
|
-
const original = (Observe as any).ToolsObserve.getScreenFingerprintHandler
|
|
5
|
-
|
|
6
|
-
async function runTests() {
|
|
7
|
-
console.log('Starting tests for wait_for_screen_change...')
|
|
8
|
-
|
|
9
|
-
// Test 1: Immediate change
|
|
10
|
-
let seq1: Array<string | null> = ['B','B']
|
|
11
|
-
;(Observe as any).ToolsObserve.getScreenFingerprintHandler = async () => ({ fingerprint: seq1.length ? seq1.shift() : null })
|
|
12
|
-
const start1 = Date.now()
|
|
13
|
-
const res1 = await ToolsInteract.waitForScreenChangeHandler({ platform: 'android', previousFingerprint: 'A', timeoutMs: 2000, pollIntervalMs: 50 })
|
|
14
|
-
const elapsed1 = Date.now() - start1
|
|
15
|
-
console.log('Test 1: Immediate change ->', (res1 && (res1 as any).success === true && (res1 as any).newFingerprint === 'B') ? 'PASS' : 'FAIL', 'Elapsed:', elapsed1, 'ms')
|
|
16
|
-
|
|
17
|
-
// Test 2: Transient nulls then stable change
|
|
18
|
-
let seq2: Array<string | null> = [null, null, 'B', 'B']
|
|
19
|
-
;(Observe as any).ToolsObserve.getScreenFingerprintHandler = async () => ({ fingerprint: seq2.length ? seq2.shift() : 'B' })
|
|
20
|
-
const res2 = await ToolsInteract.waitForScreenChangeHandler({ platform: 'android', previousFingerprint: 'A', timeoutMs: 3000, pollIntervalMs: 50 })
|
|
21
|
-
console.log('Test 2: Transient nulls ->', (res2 && (res2 as any).success === true && (res2 as any).newFingerprint === 'B') ? 'PASS' : 'FAIL')
|
|
22
|
-
|
|
23
|
-
// Test 3: Timeout
|
|
24
|
-
;(Observe as any).ToolsObserve.getScreenFingerprintHandler = async () => ({ fingerprint: 'A' })
|
|
25
|
-
const res3 = await ToolsInteract.waitForScreenChangeHandler({ platform: 'android', previousFingerprint: 'A', timeoutMs: 300, pollIntervalMs: 50 })
|
|
26
|
-
console.log('Test 3: Timeout ->', (res3 && (res3 as any).success === false && (res3 as any).reason === 'timeout') ? 'PASS' : 'FAIL')
|
|
27
|
-
|
|
28
|
-
// Restore original
|
|
29
|
-
;(Observe as any).ToolsObserve.getScreenFingerprintHandler = original
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
runTests().catch(console.error)
|
|
@@ -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) })
|