mobile-debug-mcp 0.9.0 → 0.11.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/.eslintignore +5 -0
- package/.eslintrc.cjs +18 -0
- package/.github/workflows/.gitkeep +0 -0
- package/.github/workflows/ci.yml +63 -0
- package/README.md +5 -16
- package/dist/android/interact.js +1 -123
- package/dist/android/manage.js +137 -0
- package/dist/android/observe.js +133 -88
- package/dist/android/run.js +187 -0
- package/dist/android/utils.js +181 -236
- package/dist/ios/interact.js +1 -168
- package/dist/ios/manage.js +145 -0
- package/dist/ios/observe.js +112 -5
- package/dist/ios/run.js +200 -0
- package/dist/ios/utils.js +19 -118
- package/dist/server.js +44 -42
- package/dist/tools/install.js +1 -1
- package/dist/tools/interact.js +39 -0
- package/dist/tools/logs.js +2 -2
- package/dist/tools/manage.js +180 -0
- package/dist/tools/observe.js +80 -0
- package/dist/tools/run.js +180 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/java.js +76 -0
- package/docs/CHANGELOG.md +25 -6
- package/eslint.config.cjs +36 -0
- package/eslint.config.js +60 -0
- package/package.json +8 -2
- package/src/android/interact.ts +2 -136
- package/src/android/manage.ts +135 -0
- package/src/android/observe.ts +129 -97
- package/src/android/utils.ts +199 -229
- package/src/ios/interact.ts +2 -175
- package/src/ios/manage.ts +143 -0
- package/src/ios/observe.ts +113 -5
- package/src/ios/utils.ts +20 -122
- package/src/server.ts +48 -58
- package/src/tools/interact.ts +45 -0
- package/src/tools/manage.ts +171 -0
- package/src/tools/observe.ts +82 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/java.ts +69 -0
- package/test/integration/install.integration.ts +3 -3
- package/test/integration/logstream-real.ts +5 -4
- package/test/integration/run-install-android.ts +1 -1
- package/test/integration/run-install-ios.ts +1 -1
- package/test/integration/smoke-test.ts +1 -1
- package/test/integration/test-dist.ts +1 -1
- package/test/integration/test-ui-tree.ts +1 -1
- package/test/integration/wait_for_element_real.ts +1 -1
- package/test/unit/build.test.ts +84 -0
- package/test/unit/build_and_install.test.ts +132 -0
- package/test/unit/detect-java.test.ts +22 -0
- package/test/unit/install.test.ts +2 -8
- package/test/unit/logstream.test.ts +8 -9
- package/src/tools/app.ts +0 -46
- package/src/tools/devices.ts +0 -6
- package/src/tools/install.ts +0 -43
- package/src/tools/logs.ts +0 -62
- package/src/tools/screenshot.ts +0 -18
- package/src/tools/ui.ts +0 -62
|
@@ -12,7 +12,7 @@ async function main() {
|
|
|
12
12
|
try {
|
|
13
13
|
const res = await inter.installApp(appPath, deviceId || 'booted')
|
|
14
14
|
console.log(JSON.stringify(res, null, 2))
|
|
15
|
-
} catch
|
|
15
|
+
} catch {
|
|
16
16
|
console.error('Install failed:', err instanceof Error ? err.message : String(err))
|
|
17
17
|
process.exit(2)
|
|
18
18
|
}
|
|
@@ -72,7 +72,7 @@ async function runRealTest() {
|
|
|
72
72
|
console.log(`Calls: ${calls} ${calls === 3 ? "PASS" : "FAIL"}`);
|
|
73
73
|
console.log(`Elapsed time (should be >= 1000ms): ${elapsed3} ${elapsed3 >= 1000 ? "PASS" : "FAIL"}`);
|
|
74
74
|
|
|
75
|
-
} catch
|
|
75
|
+
} catch {
|
|
76
76
|
console.error("Test failed with error:", error);
|
|
77
77
|
}
|
|
78
78
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import fs from 'fs/promises'
|
|
3
|
+
import os from 'os'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
|
|
6
|
+
// Test build_app handler by creating fake gradlew and xcodebuild behaviours
|
|
7
|
+
|
|
8
|
+
async function makeTempProject(platform: 'android' | 'ios') {
|
|
9
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), `mcp-build-${platform}-`))
|
|
10
|
+
if (platform === 'android') {
|
|
11
|
+
const gradlew = path.join(dir, 'gradlew')
|
|
12
|
+
const script = `#!/usr/bin/env node
|
|
13
|
+
const fs = require('fs')
|
|
14
|
+
const path = require('path')
|
|
15
|
+
const apk = path.join(process.cwd(),'app','build','outputs','apk','debug','app-debug.apk')
|
|
16
|
+
fs.mkdirSync(path.dirname(apk), { recursive: true })
|
|
17
|
+
fs.writeFileSync(apk, 'fake-apk')
|
|
18
|
+
console.log('BUILD SUCCESS')
|
|
19
|
+
process.exit(0)
|
|
20
|
+
`
|
|
21
|
+
await fs.writeFile(gradlew, script, { mode: 0o755 })
|
|
22
|
+
} else {
|
|
23
|
+
// create minimal Xcode workspace structure
|
|
24
|
+
const ws = path.join(dir, 'Example.xcworkspace')
|
|
25
|
+
await fs.writeFile(ws, '')
|
|
26
|
+
const script = `#!/usr/bin/env node
|
|
27
|
+
const fs = require('fs')
|
|
28
|
+
const path = require('path')
|
|
29
|
+
const app = path.join(process.cwd(),'Build','Products','Debug-iphonesimulator','Example.app')
|
|
30
|
+
fs.mkdirSync(app, { recursive: true })
|
|
31
|
+
fs.writeFileSync(path.join(app,'Info.plist'), '<plist/>')
|
|
32
|
+
console.log('BUILD SUCCESS')
|
|
33
|
+
process.exit(0)
|
|
34
|
+
`
|
|
35
|
+
const xbuild = path.join(dir, 'xcodebuild')
|
|
36
|
+
await fs.writeFile(xbuild, script, { mode: 0o755 })
|
|
37
|
+
}
|
|
38
|
+
return dir
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function run() {
|
|
42
|
+
const androidProject = await makeTempProject('android')
|
|
43
|
+
const iosProject = await makeTempProject('ios')
|
|
44
|
+
|
|
45
|
+
// Create a fake xcodebuild in PATH for the iOS build step
|
|
46
|
+
const binDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-xcode-bin-'))
|
|
47
|
+
const xcodeScript = `#!/usr/bin/env node
|
|
48
|
+
const fs = require('fs')
|
|
49
|
+
const path = require('path')
|
|
50
|
+
// emulate xcodebuild by creating an Example.app inside Build/Products/Debug-iphonesimulator
|
|
51
|
+
const app = path.join(process.cwd(),'Build','Products','Debug-iphonesimulator','Example.app')
|
|
52
|
+
fs.mkdirSync(app, { recursive: true })
|
|
53
|
+
fs.writeFileSync(path.join(app,'Info.plist'), '<plist/>')
|
|
54
|
+
console.log('xcodebuild simulated')
|
|
55
|
+
process.exit(0)
|
|
56
|
+
`
|
|
57
|
+
const xcodePath = path.join(binDir, 'xcodebuild')
|
|
58
|
+
await fs.writeFile(xcodePath, xcodeScript, { mode: 0o755 })
|
|
59
|
+
|
|
60
|
+
const origPath = process.env.PATH || ''
|
|
61
|
+
process.env.PATH = `${binDir}:${origPath}`
|
|
62
|
+
|
|
63
|
+
const { ToolsManage } = await import('../../src/tools/manage.js')
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const ares = await ToolsManage.buildAppHandler({ platform: 'android', projectPath: androidProject })
|
|
67
|
+
console.log('android build', ares)
|
|
68
|
+
assert.ok((ares as any).artifactPath && (ares as any).artifactPath.endsWith('.apk'))
|
|
69
|
+
|
|
70
|
+
const ires = await ToolsManage.buildAppHandler({ platform: 'ios', projectPath: iosProject })
|
|
71
|
+
console.log('ios build', ires)
|
|
72
|
+
assert.ok((ires as any).artifactPath && (ires as any).artifactPath.endsWith('.app'))
|
|
73
|
+
|
|
74
|
+
console.log('build tests passed')
|
|
75
|
+
} finally {
|
|
76
|
+
// cleanup
|
|
77
|
+
await fs.rm(androidProject, { recursive: true, force: true }).catch(() => {})
|
|
78
|
+
await fs.rm(iosProject, { recursive: true, force: true }).catch(() => {})
|
|
79
|
+
await fs.rm(binDir, { recursive: true, force: true }).catch(() => {})
|
|
80
|
+
process.env.PATH = origPath
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
run().catch(e => { console.error(e); process.exit(1) })
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import fs from 'fs/promises'
|
|
3
|
+
import os from 'os'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
|
|
6
|
+
async function makeAndroidProject() {
|
|
7
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-build-ai-android-'))
|
|
8
|
+
const gradlew = path.join(dir, 'gradlew')
|
|
9
|
+
const script = `#!/usr/bin/env node
|
|
10
|
+
const fs = require('fs')
|
|
11
|
+
const path = require('path')
|
|
12
|
+
const apk = path.join(process.cwd(),'app','build','outputs','apk','debug','app-debug.apk')
|
|
13
|
+
fs.mkdirSync(path.dirname(apk), { recursive: true })
|
|
14
|
+
fs.writeFileSync(apk, 'fake-apk')
|
|
15
|
+
console.log('BUILD SUCCESS')
|
|
16
|
+
process.exit(0)
|
|
17
|
+
`
|
|
18
|
+
await fs.writeFile(gradlew, script, { mode: 0o755 })
|
|
19
|
+
return dir
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function makeIOSProject() {
|
|
23
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-build-ai-ios-'))
|
|
24
|
+
const ws = path.join(dir, 'Example.xcworkspace')
|
|
25
|
+
await fs.writeFile(ws, '')
|
|
26
|
+
return dir
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function run() {
|
|
30
|
+
const androidProject = await makeAndroidProject()
|
|
31
|
+
const iosProject = await makeIOSProject()
|
|
32
|
+
|
|
33
|
+
// Create fake bin dir with adb and simctl
|
|
34
|
+
const binDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-bin-'))
|
|
35
|
+
const adbPath = path.join(binDir, 'adb')
|
|
36
|
+
const adbScript = `#!/usr/bin/env node
|
|
37
|
+
const args = process.argv.slice(2)
|
|
38
|
+
if (args.includes('devices')) {
|
|
39
|
+
console.log('List of devices attached')
|
|
40
|
+
console.log('emulator-5554\tdevice product:sdk_gphone64_arm64')
|
|
41
|
+
process.exit(0)
|
|
42
|
+
}
|
|
43
|
+
if (args.includes('install')) {
|
|
44
|
+
console.log('Performing Streamed Install')
|
|
45
|
+
console.log('Success')
|
|
46
|
+
process.exit(0)
|
|
47
|
+
}
|
|
48
|
+
if (args.includes('shell')) {
|
|
49
|
+
const idx = args.indexOf('shell')
|
|
50
|
+
const shellArgs = args.slice(idx+1)
|
|
51
|
+
if (shellArgs[0] === 'getprop') {
|
|
52
|
+
console.log('')
|
|
53
|
+
process.exit(0)
|
|
54
|
+
}
|
|
55
|
+
console.log('Success')
|
|
56
|
+
process.exit(0)
|
|
57
|
+
}
|
|
58
|
+
console.log('OK')
|
|
59
|
+
process.exit(0)
|
|
60
|
+
`
|
|
61
|
+
await fs.writeFile(adbPath, adbScript, { mode: 0o755 })
|
|
62
|
+
|
|
63
|
+
const simctlPath = path.join(binDir, 'simctl')
|
|
64
|
+
const simctlScript = `#!/usr/bin/env node
|
|
65
|
+
const args = process.argv.slice(2)
|
|
66
|
+
if (args.includes('install')) {
|
|
67
|
+
console.log('simctl install simulated')
|
|
68
|
+
process.exit(0)
|
|
69
|
+
}
|
|
70
|
+
// Respond to 'list devices --json'
|
|
71
|
+
if (args.includes('list') && args.includes('devices') && args.includes('--json')) {
|
|
72
|
+
const out = { devices: { 'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [ { udid: 'booted', name: 'iPhone 14', state: 'Booted' } ] } }
|
|
73
|
+
console.log(JSON.stringify(out))
|
|
74
|
+
process.exit(0)
|
|
75
|
+
}
|
|
76
|
+
console.log('simctl ok')
|
|
77
|
+
process.exit(0)
|
|
78
|
+
`
|
|
79
|
+
await fs.writeFile(simctlPath, simctlScript, { mode: 0o755 })
|
|
80
|
+
|
|
81
|
+
// fake xcodebuild for iOS build step
|
|
82
|
+
const xcodeScript = `#!/usr/bin/env node
|
|
83
|
+
const fs = require('fs')
|
|
84
|
+
const path = require('path')
|
|
85
|
+
const app = path.join(process.cwd(),'Build','Products','Debug-iphonesimulator','Example.app')
|
|
86
|
+
fs.mkdirSync(app, { recursive: true })
|
|
87
|
+
fs.writeFileSync(path.join(app,'Info.plist'), '<plist/>')
|
|
88
|
+
console.log('xcodebuild simulated')
|
|
89
|
+
process.exit(0)
|
|
90
|
+
`
|
|
91
|
+
const xcodePath = path.join(binDir, 'xcodebuild')
|
|
92
|
+
await fs.writeFile(xcodePath, xcodeScript, { mode: 0o755 })
|
|
93
|
+
|
|
94
|
+
const origPath = process.env.PATH || ''
|
|
95
|
+
const origXcrun = process.env.XCRUN_PATH
|
|
96
|
+
process.env.PATH = `${binDir}:${origPath}`
|
|
97
|
+
process.env.XCRUN_PATH = simctlPath
|
|
98
|
+
|
|
99
|
+
const { ToolsManage } = await import('../../src/tools/manage.js')
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
// Android build_and_install
|
|
103
|
+
const ares = await ToolsManage.buildAndInstallHandler({ platform: 'android', projectPath: androidProject, deviceId: 'emulator-5554' })
|
|
104
|
+
console.log('android ndjson:\n', ares.ndjson)
|
|
105
|
+
console.log('android result:', ares.result)
|
|
106
|
+
assert.ok(ares.result.success === true, 'android build_and_install should succeed')
|
|
107
|
+
assert.ok(ares.result.artifactPath && ares.result.artifactPath.endsWith('.apk'))
|
|
108
|
+
assert.ok(ares.ndjson.includes('"type":"build"'))
|
|
109
|
+
assert.ok(ares.ndjson.includes('"type":"install"'))
|
|
110
|
+
|
|
111
|
+
// iOS build_and_install
|
|
112
|
+
const ires = await ToolsManage.buildAndInstallHandler({ platform: 'ios', projectPath: iosProject, deviceId: 'booted' })
|
|
113
|
+
console.log('ios ndjson:\n', ires.ndjson)
|
|
114
|
+
console.log('ios result:', ires.result)
|
|
115
|
+
assert.ok(ires.result.success === true, 'ios build_and_install should succeed')
|
|
116
|
+
assert.ok(ires.result.artifactPath && ires.result.artifactPath.endsWith('.app'))
|
|
117
|
+
assert.ok(ires.ndjson.includes('"type":"build"'))
|
|
118
|
+
assert.ok(ires.ndjson.includes('"type":"install"'))
|
|
119
|
+
|
|
120
|
+
console.log('build_and_install tests passed')
|
|
121
|
+
} finally {
|
|
122
|
+
process.env.PATH = origPath
|
|
123
|
+
if (origXcrun === undefined) delete process.env.XCRUN_PATH
|
|
124
|
+
else process.env.XCRUN_PATH = origXcrun
|
|
125
|
+
await fs.rm(androidProject, { recursive: true, force: true }).catch(() => {})
|
|
126
|
+
await fs.rm(iosProject, { recursive: true, force: true }).catch(() => {})
|
|
127
|
+
await fs.rm(binDir, { recursive: true, force: true }).catch(() => {})
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
run().catch(e => { console.error(e); process.exit(1) })
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import { detectJavaHome } from '../../src/utils/java.js'
|
|
3
|
+
|
|
4
|
+
// These tests are lightweight smoke tests; they don't rely on actual JDK17 installs,
|
|
5
|
+
// but exercise the failure modes and ensure the function returns undefined or a string.
|
|
6
|
+
|
|
7
|
+
export async function run() {
|
|
8
|
+
const res = await detectJavaHome()
|
|
9
|
+
// It's acceptable for local dev env to not have JDK17; just ensure call returns (string|undefined)
|
|
10
|
+
assert.ok(typeof res === 'string' || typeof res === 'undefined')
|
|
11
|
+
|
|
12
|
+
// Basic mocking: if JAVA_HOME points to a fake path, detection should return undefined
|
|
13
|
+
const orig = process.env.JAVA_HOME
|
|
14
|
+
process.env.JAVA_HOME = '/non/existent/java/home'
|
|
15
|
+
const res2 = await detectJavaHome()
|
|
16
|
+
assert.ok(typeof res2 === 'undefined')
|
|
17
|
+
process.env.JAVA_HOME = orig
|
|
18
|
+
|
|
19
|
+
console.log('detectJavaHome tests passed')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
run().catch((e) => { console.error(e); process.exit(1) })
|
|
@@ -2,7 +2,6 @@ import assert from 'assert'
|
|
|
2
2
|
import fs from 'fs/promises'
|
|
3
3
|
import os from 'os'
|
|
4
4
|
import path from 'path'
|
|
5
|
-
import { createRequire } from 'module'
|
|
6
5
|
|
|
7
6
|
// This test mocks child_process.spawn and simulates a Gradle build producing an APK
|
|
8
7
|
// and an adb install. It does not patch AndroidInteract.installApp itself so the
|
|
@@ -15,11 +14,6 @@ async function makeTempFile(ext: string) {
|
|
|
15
14
|
return { dir, file }
|
|
16
15
|
}
|
|
17
16
|
|
|
18
|
-
async function makeTempDirWith(name: string) {
|
|
19
|
-
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-test-'))
|
|
20
|
-
await fs.writeFile(path.join(dir, name), '')
|
|
21
|
-
return dir
|
|
22
|
-
}
|
|
23
17
|
|
|
24
18
|
export async function run() {
|
|
25
19
|
// Create a fake adb executable in a temporary bin dir and prepend to PATH so
|
|
@@ -37,12 +31,12 @@ process.exit(0)
|
|
|
37
31
|
process.env.PATH = `${binDir}:${origPath}`
|
|
38
32
|
|
|
39
33
|
// Import the module under test after PATH is adjusted
|
|
40
|
-
const {
|
|
34
|
+
const { AndroidManage } = await import('../../src/android/manage.js')
|
|
41
35
|
|
|
42
36
|
try {
|
|
43
37
|
// Test: install with .apk file should call adb install
|
|
44
38
|
const { dir: d1, file: apk } = await makeTempFile('.apk')
|
|
45
|
-
const ai = new
|
|
39
|
+
const ai = new AndroidManage()
|
|
46
40
|
const res1 = await ai.installApp(apk)
|
|
47
41
|
console.log('res1', res1)
|
|
48
42
|
assert.ok(res1.installed === true, 'APK install should succeed')
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { promises as fs } from 'fs'
|
|
2
2
|
import os from 'os'
|
|
3
3
|
import path from 'path'
|
|
4
|
-
import {
|
|
4
|
+
import { AndroidObserve } from '../../src/android/observe.js'
|
|
5
5
|
|
|
6
6
|
async function run() {
|
|
7
7
|
const tmp = os.tmpdir()
|
|
8
|
-
const
|
|
8
|
+
const sessionId = 'unit-test-logstream'
|
|
9
|
+
const file = path.join(tmp, `mobile-debug-log-${sessionId}.ndjson`)
|
|
9
10
|
|
|
10
11
|
// Prepare NDJSON with one crash entry and one info entry
|
|
11
12
|
const crashEntry = { timestamp: '2026-03-13T14:00:00.000Z', level: 'E', tag: 'AndroidRuntime', message: 'FATAL EXCEPTION: main\njava.lang.NullPointerException' }
|
|
@@ -13,12 +14,11 @@ async function run() {
|
|
|
13
14
|
|
|
14
15
|
await fs.writeFile(file, JSON.stringify(crashEntry) + '\n' + JSON.stringify(infoEntry) + '\n')
|
|
15
16
|
|
|
16
|
-
const sessionId = 'unit-test-logstream'
|
|
17
|
-
_setActiveLogStream(sessionId, file)
|
|
18
17
|
|
|
19
18
|
try {
|
|
20
|
-
// Read all
|
|
21
|
-
const
|
|
19
|
+
// Read all via AndroidObserve (falls back to session NDJSON file)
|
|
20
|
+
const obs = new AndroidObserve()
|
|
21
|
+
const { entries, crash_summary } = await obs.readLogStream(sessionId, 10)
|
|
22
22
|
if (!Array.isArray(entries) || entries.length !== 2) throw new Error('Expected 2 entries')
|
|
23
23
|
if (!crash_summary || crash_summary.crash_detected !== true) throw new Error('Expected crash_detected true')
|
|
24
24
|
if (!crash_summary.exception || !/NullPointerException/.test(crash_summary.exception)) throw new Error('Expected NullPointerException detected')
|
|
@@ -27,18 +27,17 @@ async function run() {
|
|
|
27
27
|
|
|
28
28
|
// Test since filter (after first entry)
|
|
29
29
|
const since = new Date('2026-03-13T14:00:30.000Z').toISOString()
|
|
30
|
-
const r2 = await
|
|
30
|
+
const r2 = await obs.readLogStream(sessionId, 10, since)
|
|
31
31
|
if (r2.entries.length !== 1) throw new Error('Expected 1 entry after since filter')
|
|
32
32
|
console.log('Test 2 PASS: since filter')
|
|
33
33
|
|
|
34
34
|
// Test limit
|
|
35
|
-
const r3 = await
|
|
35
|
+
const r3 = await obs.readLogStream(sessionId, 1)
|
|
36
36
|
if (r3.entries.length !== 1) throw new Error('Expected 1 entry with limit=1')
|
|
37
37
|
console.log('Test 3 PASS: limit works')
|
|
38
38
|
|
|
39
39
|
console.log('ALL logstream tests passed')
|
|
40
40
|
} finally {
|
|
41
|
-
_clearActiveLogStream(sessionId)
|
|
42
41
|
await fs.unlink(file).catch(()=>{})
|
|
43
42
|
}
|
|
44
43
|
}
|
package/src/tools/app.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { resolveTargetDevice } from '../resolve-device.js'
|
|
2
|
-
import { AndroidInteract } from '../android/interact.js'
|
|
3
|
-
import { iOSInteract } from '../ios/interact.js'
|
|
4
|
-
|
|
5
|
-
const androidInteract = new AndroidInteract()
|
|
6
|
-
const iosInteract = new iOSInteract()
|
|
7
|
-
|
|
8
|
-
export async function startAppHandler({ platform, appId, deviceId }: { platform: 'android' | 'ios', appId: string, deviceId?: string }) {
|
|
9
|
-
if (platform === 'android') {
|
|
10
|
-
const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId })
|
|
11
|
-
return await androidInteract.startApp(appId, resolved.id)
|
|
12
|
-
} else {
|
|
13
|
-
const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId })
|
|
14
|
-
return await iosInteract.startApp(appId, resolved.id)
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export async function terminateAppHandler({ platform, appId, deviceId }: { platform: 'android' | 'ios', appId: string, deviceId?: string }) {
|
|
19
|
-
if (platform === 'android') {
|
|
20
|
-
const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId })
|
|
21
|
-
return await androidInteract.terminateApp(appId, resolved.id)
|
|
22
|
-
} else {
|
|
23
|
-
const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId })
|
|
24
|
-
return await iosInteract.terminateApp(appId, resolved.id)
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export async function restartAppHandler({ platform, appId, deviceId }: { platform: 'android' | 'ios', appId: string, deviceId?: string }) {
|
|
29
|
-
if (platform === 'android') {
|
|
30
|
-
const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId })
|
|
31
|
-
return await androidInteract.restartApp(appId, resolved.id)
|
|
32
|
-
} else {
|
|
33
|
-
const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId })
|
|
34
|
-
return await iosInteract.restartApp(appId, resolved.id)
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export async function resetAppDataHandler({ platform, appId, deviceId }: { platform: 'android' | 'ios', appId: string, deviceId?: string }) {
|
|
39
|
-
if (platform === 'android') {
|
|
40
|
-
const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId })
|
|
41
|
-
return await androidInteract.resetAppData(appId, resolved.id)
|
|
42
|
-
} else {
|
|
43
|
-
const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId })
|
|
44
|
-
return await iosInteract.resetAppData(appId, resolved.id)
|
|
45
|
-
}
|
|
46
|
-
}
|
package/src/tools/devices.ts
DELETED
package/src/tools/install.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from 'fs'
|
|
2
|
-
import path from 'path'
|
|
3
|
-
import { resolveTargetDevice } from '../resolve-device.js'
|
|
4
|
-
import { AndroidInteract } from '../android/interact.js'
|
|
5
|
-
import { iOSInteract } from '../ios/interact.js'
|
|
6
|
-
|
|
7
|
-
const androidInteract = new AndroidInteract()
|
|
8
|
-
const iosInteract = new iOSInteract()
|
|
9
|
-
|
|
10
|
-
export async function installAppHandler({ platform, appPath, deviceId }: { platform?: 'android' | 'ios', appPath: string, deviceId?: string }) {
|
|
11
|
-
let chosenPlatform: 'android' | 'ios' | undefined = platform
|
|
12
|
-
|
|
13
|
-
try {
|
|
14
|
-
const stat = await fs.stat(appPath).catch(() => null)
|
|
15
|
-
if (stat && stat.isDirectory()) {
|
|
16
|
-
const files = (await fs.readdir(appPath).catch(() => [])) as string[]
|
|
17
|
-
if (files.some(f => f.endsWith('.xcodeproj') || f.endsWith('.xcworkspace'))) {
|
|
18
|
-
chosenPlatform = 'ios'
|
|
19
|
-
} else if (files.includes('gradlew') || files.includes('build.gradle') || files.includes('settings.gradle') || (files.includes('app') && (await fs.stat(path.join(appPath, 'app')).catch(() => null)))) {
|
|
20
|
-
chosenPlatform = 'android'
|
|
21
|
-
} else {
|
|
22
|
-
chosenPlatform = 'android'
|
|
23
|
-
}
|
|
24
|
-
} else if (typeof appPath === 'string') {
|
|
25
|
-
const ext = path.extname(appPath).toLowerCase()
|
|
26
|
-
if (ext === '.apk') chosenPlatform = 'android'
|
|
27
|
-
else if (ext === '.ipa' || ext === '.app') chosenPlatform = 'ios'
|
|
28
|
-
else chosenPlatform = 'android'
|
|
29
|
-
}
|
|
30
|
-
} catch (e) {
|
|
31
|
-
chosenPlatform = 'android'
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (chosenPlatform === 'android') {
|
|
35
|
-
const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
|
|
36
|
-
const result = await androidInteract.installApp(appPath, resolved.id)
|
|
37
|
-
return result
|
|
38
|
-
} else {
|
|
39
|
-
const resolved = await resolveTargetDevice({ platform: 'ios', deviceId })
|
|
40
|
-
const result = await iosInteract.installApp(appPath, resolved.id)
|
|
41
|
-
return result
|
|
42
|
-
}
|
|
43
|
-
}
|
package/src/tools/logs.ts
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { resolveTargetDevice } from '../resolve-device.js'
|
|
2
|
-
import { AndroidObserve } from '../android/observe.js'
|
|
3
|
-
import { iOSObserve } from '../ios/observe.js'
|
|
4
|
-
import { startAndroidLogStream, readLogStreamLines, stopAndroidLogStream } from '../android/utils.js'
|
|
5
|
-
import { startIOSLogStream, readIOSLogStreamLines, stopIOSLogStream } from '../ios/utils.js'
|
|
6
|
-
|
|
7
|
-
const androidObserve = new AndroidObserve()
|
|
8
|
-
|
|
9
|
-
export async function getLogsHandler({ platform, appId, deviceId, lines }: { platform: 'android' | 'ios', appId?: string, deviceId?: string, lines?: number }) {
|
|
10
|
-
if (platform === 'android') {
|
|
11
|
-
const resolved = await resolveTargetDevice({ platform: 'android', appId, deviceId })
|
|
12
|
-
const deviceInfo = resolved
|
|
13
|
-
const response = await androidObserve.getLogs(appId, lines ?? 200, resolved.id)
|
|
14
|
-
const logs = Array.isArray(response.logs) ? response.logs : []
|
|
15
|
-
const crashLines = logs.filter(line => line.includes('FATAL EXCEPTION'))
|
|
16
|
-
return { device: deviceInfo, logs, crashLines }
|
|
17
|
-
} else {
|
|
18
|
-
const resolved = await resolveTargetDevice({ platform: 'ios', appId, deviceId })
|
|
19
|
-
const deviceInfo = resolved
|
|
20
|
-
try {
|
|
21
|
-
const iosObs = new iOSObserve()
|
|
22
|
-
const resp = await iosObs.getLogs(appId, resolved.id)
|
|
23
|
-
const logs = Array.isArray(resp.logs) ? resp.logs : []
|
|
24
|
-
const crashLines = logs.filter(l => l.includes('FATAL EXCEPTION'))
|
|
25
|
-
return { device: deviceInfo, logs, crashLines }
|
|
26
|
-
} catch (e) {
|
|
27
|
-
return { device: deviceInfo, logs: [], crashLines: [] }
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export async function startLogStreamHandler({ platform, packageName, level, sessionId, deviceId }: { platform?: 'android' | 'ios', packageName: string, level?: 'error' | 'warn' | 'info' | 'debug', sessionId?: string, deviceId?: string }) {
|
|
33
|
-
const effectivePlatform = platform || 'android'
|
|
34
|
-
const sid = sessionId || 'default'
|
|
35
|
-
if (effectivePlatform === 'android') {
|
|
36
|
-
const resolved = await resolveTargetDevice({ platform: 'android', appId: packageName, deviceId })
|
|
37
|
-
return await startAndroidLogStream(packageName, level || 'error', resolved.id, sid)
|
|
38
|
-
} else {
|
|
39
|
-
const resolved = await resolveTargetDevice({ platform: 'ios', appId: packageName, deviceId })
|
|
40
|
-
return await startIOSLogStream(packageName, level || 'error', resolved.id, sid)
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export async function readLogStreamHandler({ platform, sessionId, limit, since }: { platform?: 'android' | 'ios', sessionId?: string, limit?: number, since?: string }) {
|
|
45
|
-
const effectivePlatform = platform || 'android'
|
|
46
|
-
const sid = sessionId || 'default'
|
|
47
|
-
if (effectivePlatform === 'android') {
|
|
48
|
-
return await readLogStreamLines(sid, limit ?? 100, since)
|
|
49
|
-
} else {
|
|
50
|
-
return await readIOSLogStreamLines(sid, limit ?? 100, since)
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export async function stopLogStreamHandler({ platform, sessionId }: { platform?: 'android' | 'ios', sessionId?: string }) {
|
|
55
|
-
const effectivePlatform = platform || 'android'
|
|
56
|
-
const sid = sessionId || 'default'
|
|
57
|
-
if (effectivePlatform === 'android') {
|
|
58
|
-
return await stopAndroidLogStream(sid)
|
|
59
|
-
} else {
|
|
60
|
-
return await stopIOSLogStream(sid)
|
|
61
|
-
}
|
|
62
|
-
}
|
package/src/tools/screenshot.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { resolveTargetDevice } from '../resolve-device.js'
|
|
2
|
-
import { AndroidObserve } from '../android/observe.js'
|
|
3
|
-
import { iOSObserve } from '../ios/observe.js'
|
|
4
|
-
|
|
5
|
-
const androidObserve = new AndroidObserve()
|
|
6
|
-
const iosObserve = new iOSObserve()
|
|
7
|
-
|
|
8
|
-
export async function captureScreenshotHandler({ platform, deviceId }: { platform: 'android' | 'ios', deviceId?: string }) {
|
|
9
|
-
if (platform === 'android') {
|
|
10
|
-
const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
|
|
11
|
-
const result = await androidObserve.captureScreen(resolved.id)
|
|
12
|
-
return { device: resolved, resolution: result.resolution, screenshot: result.screenshot }
|
|
13
|
-
} else {
|
|
14
|
-
const resolved = await resolveTargetDevice({ platform: 'ios', deviceId })
|
|
15
|
-
const result = await iosObserve.captureScreenshot(resolved.id)
|
|
16
|
-
return { device: resolved, resolution: result.resolution, screenshot: result.screenshot }
|
|
17
|
-
}
|
|
18
|
-
}
|
package/src/tools/ui.ts
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { resolveTargetDevice } from '../resolve-device.js'
|
|
2
|
-
import { AndroidObserve } from '../android/observe.js'
|
|
3
|
-
import { iOSObserve } from '../ios/observe.js'
|
|
4
|
-
import { AndroidInteract } from '../android/interact.js'
|
|
5
|
-
import { iOSInteract } from '../ios/interact.js'
|
|
6
|
-
|
|
7
|
-
const androidObserve = new AndroidObserve()
|
|
8
|
-
const iosObserve = new iOSObserve()
|
|
9
|
-
const androidInteract = new AndroidInteract()
|
|
10
|
-
const iosInteract = new iOSInteract()
|
|
11
|
-
|
|
12
|
-
export async function getUITreeHandler({ platform, deviceId }: { platform: 'android' | 'ios', deviceId?: string }) {
|
|
13
|
-
if (platform === 'android') {
|
|
14
|
-
const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
|
|
15
|
-
return await androidObserve.getUITree(resolved.id)
|
|
16
|
-
} else {
|
|
17
|
-
const resolved = await resolveTargetDevice({ platform: 'ios', deviceId })
|
|
18
|
-
return await iosObserve.getUITree(resolved.id)
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export async function getCurrentScreenHandler({ deviceId }: { deviceId?: string }) {
|
|
23
|
-
const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
|
|
24
|
-
return await androidObserve.getCurrentScreen(resolved.id)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export async function waitForElementHandler({ platform, text, timeout, deviceId }: { platform: 'android' | 'ios', text: string, timeout?: number, deviceId?: string }) {
|
|
28
|
-
const effectiveTimeout = timeout ?? 10000
|
|
29
|
-
if (platform === 'android') {
|
|
30
|
-
const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
|
|
31
|
-
return await androidInteract.waitForElement(text, effectiveTimeout, resolved.id)
|
|
32
|
-
} else {
|
|
33
|
-
const resolved = await resolveTargetDevice({ platform: 'ios', deviceId })
|
|
34
|
-
return await iosInteract.waitForElement(text, effectiveTimeout, resolved.id)
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export async function tapHandler({ platform, x, y, deviceId }: { platform?: 'android' | 'ios', x: number, y: number, deviceId?: string }) {
|
|
39
|
-
const effectivePlatform = platform || 'android'
|
|
40
|
-
if (effectivePlatform === 'android') {
|
|
41
|
-
const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
|
|
42
|
-
return await androidInteract.tap(x, y, resolved.id)
|
|
43
|
-
} else {
|
|
44
|
-
const resolved = await resolveTargetDevice({ platform: 'ios', deviceId })
|
|
45
|
-
return await iosInteract.tap(x, y, resolved.id)
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export async function swipeHandler({ x1, y1, x2, y2, duration, deviceId }: { x1: number, y1: number, x2: number, y2: number, duration: number, deviceId?: string }) {
|
|
50
|
-
const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
|
|
51
|
-
return await androidInteract.swipe(x1, y1, x2, y2, duration, resolved.id)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export async function typeTextHandler({ text, deviceId }: { text: string, deviceId?: string }) {
|
|
55
|
-
const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
|
|
56
|
-
return await androidInteract.typeText(text, resolved.id)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export async function pressBackHandler({ deviceId }: { deviceId?: string }) {
|
|
60
|
-
const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
|
|
61
|
-
return await androidInteract.pressBack(resolved.id)
|
|
62
|
-
}
|