mobile-debug-mcp 0.7.0 → 0.9.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/README.md +18 -443
- package/dist/android/interact.js +96 -1
- package/dist/android/utils.js +404 -12
- package/dist/ios/interact.js +105 -0
- package/dist/ios/utils.js +154 -0
- package/dist/resolve-device.js +70 -0
- package/dist/server.js +126 -194
- package/dist/tools/app.js +45 -0
- package/dist/tools/devices.js +5 -0
- package/dist/tools/install.js +47 -0
- package/dist/tools/logs.js +62 -0
- package/dist/tools/screenshot.js +17 -0
- package/dist/tools/ui.js +57 -0
- package/docs/CHANGELOG.md +19 -0
- package/docs/TOOLS.md +272 -0
- package/package.json +6 -2
- package/src/android/interact.ts +100 -1
- package/src/android/utils.ts +395 -10
- package/src/ios/interact.ts +102 -0
- package/src/ios/utils.ts +157 -0
- package/src/resolve-device.ts +80 -0
- package/src/server.ts +149 -276
- package/src/tools/app.ts +46 -0
- package/src/tools/devices.ts +6 -0
- package/src/tools/install.ts +43 -0
- package/src/tools/logs.ts +62 -0
- package/src/tools/screenshot.ts +18 -0
- package/src/tools/ui.ts +62 -0
- package/src/types.ts +7 -0
- package/test/integration/index.ts +8 -0
- package/test/integration/install.integration.ts +64 -0
- package/test/integration/logstream-real.ts +35 -0
- package/test/integration/run-install-android.ts +21 -0
- package/test/integration/run-install-ios.ts +21 -0
- package/test/integration/run-real-test.ts +19 -0
- package/{smoke-test.ts → test/integration/smoke-test.ts} +17 -25
- package/test/integration/test-dist.mjs +41 -0
- package/test/integration/test-dist.ts +41 -0
- package/{test-ui-tree.ts → test/integration/test-ui-tree.ts} +2 -2
- package/test/integration/wait_for_element_real.ts +80 -0
- package/test/unit/index.ts +7 -0
- package/test/unit/install.test.ts +82 -0
- package/test/unit/logparse.test.ts +41 -0
- package/test/unit/logstream.test.ts +46 -0
- package/test/unit/wait_for_element_mock.ts +104 -0
- package/tsconfig.json +1 -1
- package/smoke-test.js +0 -102
- package/test/run-real-test.js +0 -24
- package/test/wait_for_element_mock.js +0 -113
- package/test/wait_for_element_real.js +0 -67
- package/test-ui-tree.js +0 -68
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import fs from 'fs/promises'
|
|
3
|
+
import os from 'os'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
import { createRequire } from 'module'
|
|
6
|
+
|
|
7
|
+
// This test mocks child_process.spawn and simulates a Gradle build producing an APK
|
|
8
|
+
// and an adb install. It does not patch AndroidInteract.installApp itself so the
|
|
9
|
+
// internal build-and-install logic is exercised.
|
|
10
|
+
|
|
11
|
+
async function makeTempFile(ext: string) {
|
|
12
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-test-'))
|
|
13
|
+
const file = path.join(dir, `fake${ext}`)
|
|
14
|
+
await fs.writeFile(file, 'binary')
|
|
15
|
+
return { dir, file }
|
|
16
|
+
}
|
|
17
|
+
|
|
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
|
+
|
|
24
|
+
export async function run() {
|
|
25
|
+
// Create a fake adb executable in a temporary bin dir and prepend to PATH so
|
|
26
|
+
// execAdb's spawn('adb', ...) will find it. This avoids requiring a real adb
|
|
27
|
+
// binary during unit tests and exercises the installApp logic.
|
|
28
|
+
const binDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-adb-bin-'))
|
|
29
|
+
const adbPath = path.join(binDir, 'adb')
|
|
30
|
+
const adbScript = `#!/usr/bin/env node
|
|
31
|
+
console.log('Performing Streamed Install')
|
|
32
|
+
console.log('Success')
|
|
33
|
+
process.exit(0)
|
|
34
|
+
`
|
|
35
|
+
await fs.writeFile(adbPath, adbScript, { mode: 0o755 })
|
|
36
|
+
const origPath = process.env.PATH || ''
|
|
37
|
+
process.env.PATH = `${binDir}:${origPath}`
|
|
38
|
+
|
|
39
|
+
// Import the module under test after PATH is adjusted
|
|
40
|
+
const { AndroidInteract } = await import('../../src/android/interact.js')
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
// Test: install with .apk file should call adb install
|
|
44
|
+
const { dir: d1, file: apk } = await makeTempFile('.apk')
|
|
45
|
+
const ai = new AndroidInteract()
|
|
46
|
+
const res1 = await ai.installApp(apk)
|
|
47
|
+
console.log('res1', res1)
|
|
48
|
+
assert.ok(res1.installed === true, 'APK install should succeed')
|
|
49
|
+
|
|
50
|
+
// Test: project directory detection for Android (gradlew present as a simple wrapper script)
|
|
51
|
+
const dirGradle = await fs.mkdtemp(path.join(os.tmpdir(), 'mcp-test-'))
|
|
52
|
+
const gradlewPath = path.join(dirGradle, 'gradlew')
|
|
53
|
+
const gradlewScript = `#!/usr/bin/env node
|
|
54
|
+
const fs = require('fs')
|
|
55
|
+
const path = require('path')
|
|
56
|
+
const apkPath = path.join(process.cwd(), 'app', 'build', 'outputs', 'apk', 'debug', 'app-debug.apk')
|
|
57
|
+
fs.mkdirSync(path.dirname(apkPath), { recursive: true })
|
|
58
|
+
fs.writeFileSync(apkPath, 'fake-apk-binary')
|
|
59
|
+
console.log('BUILD SUCCESS')
|
|
60
|
+
process.exit(0)
|
|
61
|
+
`
|
|
62
|
+
await fs.writeFile(gradlewPath, gradlewScript, { mode: 0o755 })
|
|
63
|
+
|
|
64
|
+
const res2 = await ai.installApp(dirGradle)
|
|
65
|
+
console.log('res2', res2)
|
|
66
|
+
assert.ok(res2.installed === true, 'Project dir (gradle) install should succeed')
|
|
67
|
+
|
|
68
|
+
// cleanup
|
|
69
|
+
await fs.rm(d1, { recursive: true, force: true }).catch(() => {})
|
|
70
|
+
await fs.rm(dirGradle, { recursive: true, force: true }).catch(() => {})
|
|
71
|
+
|
|
72
|
+
// restore PATH
|
|
73
|
+
process.env.PATH = origPath
|
|
74
|
+
|
|
75
|
+
console.log('install tests passed')
|
|
76
|
+
} finally {
|
|
77
|
+
// ensure PATH restored even on failure
|
|
78
|
+
process.env.PATH = origPath
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
run().catch((e) => { console.error(e); process.exit(1) })
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { parseLogLine } from '../../src/android/utils.js'
|
|
2
|
+
|
|
3
|
+
function assert(cond: boolean, msg?: string) { if (!cond) throw new Error(msg || 'Assertion failed') }
|
|
4
|
+
|
|
5
|
+
function run() {
|
|
6
|
+
const samples = [
|
|
7
|
+
// Standard format
|
|
8
|
+
{
|
|
9
|
+
line: '03-13 15:08:25.257 2468 2578 E FromGoneTransitionInteractor: Ignoring startTransition: ...',
|
|
10
|
+
expect: { level: 'E', tag: 'FromGoneTransitionInteractor', crash: false }
|
|
11
|
+
},
|
|
12
|
+
// Full date format
|
|
13
|
+
{
|
|
14
|
+
line: '2026-03-13 15:08:25.257 2468 2578 E Something: Boom happened',
|
|
15
|
+
expect: { level: 'E', tag: 'Something', crash: false }
|
|
16
|
+
},
|
|
17
|
+
// Simple priority/tag
|
|
18
|
+
{
|
|
19
|
+
line: 'W/MyTag: Some warning here',
|
|
20
|
+
expect: { level: 'W', tag: 'MyTag', crash: false }
|
|
21
|
+
},
|
|
22
|
+
// Crash message
|
|
23
|
+
{
|
|
24
|
+
line: '03-13 15:09:01.123 9999 9999 E AndroidRuntime: FATAL EXCEPTION: main\njava.lang.NullPointerException: at ...',
|
|
25
|
+
expect: { level: 'E', tag: 'AndroidRuntime', crash: true, exceptionContains: 'NullPointerException' }
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
for (const s of samples) {
|
|
30
|
+
const res = parseLogLine(s.line)
|
|
31
|
+
console.log('Parsed:', res)
|
|
32
|
+
assert(res.level === s.expect.level, `Expected level ${s.expect.level} got ${res.level}`)
|
|
33
|
+
assert(res.tag === s.expect.tag, `Expected tag ${s.expect.tag} got ${res.tag}`)
|
|
34
|
+
if (s.expect.crash) assert(res.crash === true, 'Expected crash true')
|
|
35
|
+
if (s.expect.exceptionContains) assert(res.exception && res.exception.indexOf(s.expect.exceptionContains) !== -1, 'Expected exception to contain ' + s.expect.exceptionContains)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.log('Log parse tests passed')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
run()
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { promises as fs } from 'fs'
|
|
2
|
+
import os from 'os'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { readLogStreamLines, _setActiveLogStream, _clearActiveLogStream } from '../../src/android/utils.js'
|
|
5
|
+
|
|
6
|
+
async function run() {
|
|
7
|
+
const tmp = os.tmpdir()
|
|
8
|
+
const file = path.join(tmp, `test-mobile-debug-log-${Date.now()}.ndjson`)
|
|
9
|
+
|
|
10
|
+
// Prepare NDJSON with one crash entry and one info entry
|
|
11
|
+
const crashEntry = { timestamp: '2026-03-13T14:00:00.000Z', level: 'E', tag: 'AndroidRuntime', message: 'FATAL EXCEPTION: main\njava.lang.NullPointerException' }
|
|
12
|
+
const infoEntry = { timestamp: '2026-03-13T14:01:00.000Z', level: 'I', tag: 'MyTag', message: 'Info message' }
|
|
13
|
+
|
|
14
|
+
await fs.writeFile(file, JSON.stringify(crashEntry) + '\n' + JSON.stringify(infoEntry) + '\n')
|
|
15
|
+
|
|
16
|
+
const sessionId = 'unit-test-logstream'
|
|
17
|
+
_setActiveLogStream(sessionId, file)
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
// Read all
|
|
21
|
+
const { entries, crash_summary } = await readLogStreamLines(sessionId, 10)
|
|
22
|
+
if (!Array.isArray(entries) || entries.length !== 2) throw new Error('Expected 2 entries')
|
|
23
|
+
if (!crash_summary || crash_summary.crash_detected !== true) throw new Error('Expected crash_detected true')
|
|
24
|
+
if (!crash_summary.exception || !/NullPointerException/.test(crash_summary.exception)) throw new Error('Expected NullPointerException detected')
|
|
25
|
+
|
|
26
|
+
console.log('Test 1 PASS: basic parsing & crash detection')
|
|
27
|
+
|
|
28
|
+
// Test since filter (after first entry)
|
|
29
|
+
const since = new Date('2026-03-13T14:00:30.000Z').toISOString()
|
|
30
|
+
const r2 = await readLogStreamLines(sessionId, 10, since)
|
|
31
|
+
if (r2.entries.length !== 1) throw new Error('Expected 1 entry after since filter')
|
|
32
|
+
console.log('Test 2 PASS: since filter')
|
|
33
|
+
|
|
34
|
+
// Test limit
|
|
35
|
+
const r3 = await readLogStreamLines(sessionId, 1)
|
|
36
|
+
if (r3.entries.length !== 1) throw new Error('Expected 1 entry with limit=1')
|
|
37
|
+
console.log('Test 3 PASS: limit works')
|
|
38
|
+
|
|
39
|
+
console.log('ALL logstream tests passed')
|
|
40
|
+
} finally {
|
|
41
|
+
_clearActiveLogStream(sessionId)
|
|
42
|
+
await fs.unlink(file).catch(()=>{})
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
run().catch(err => { console.error('Logstream tests failed:', err); process.exit(1) })
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { AndroidInteract } from "../../src/android/interact.js";
|
|
2
|
+
import { AndroidObserve } from "../../src/android/observe.js";
|
|
3
|
+
|
|
4
|
+
const originalGetUITree = (AndroidObserve as any).prototype.getUITree;
|
|
5
|
+
|
|
6
|
+
async function runTests() {
|
|
7
|
+
console.log("Starting tests for wait_for_element...");
|
|
8
|
+
const interact = new AndroidInteract();
|
|
9
|
+
|
|
10
|
+
console.log("\nTest 1: Element found immediately");
|
|
11
|
+
(AndroidObserve as any).prototype.getUITree = async () => ({
|
|
12
|
+
device: { platform: "android", id: "mock", osVersion: "12", model: "Pixel", simulator: true },
|
|
13
|
+
screen: "",
|
|
14
|
+
resolution: { width: 1080, height: 1920 },
|
|
15
|
+
elements: [{
|
|
16
|
+
text: "Target",
|
|
17
|
+
type: "Button",
|
|
18
|
+
contentDescription: null,
|
|
19
|
+
clickable: true,
|
|
20
|
+
enabled: true,
|
|
21
|
+
visible: true,
|
|
22
|
+
bounds: [0, 0, 100, 100],
|
|
23
|
+
resourceId: null
|
|
24
|
+
}]
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const start1 = Date.now();
|
|
28
|
+
const result1 = await interact.waitForElement("Target", 1000);
|
|
29
|
+
const elapsed1 = Date.now() - start1;
|
|
30
|
+
console.log("Result:", result1.found === true ? "PASS" : "FAIL");
|
|
31
|
+
console.log("Element:", result1.element ? "FOUND" : "MISSING");
|
|
32
|
+
console.log("Elapsed:", elapsed1, "ms");
|
|
33
|
+
|
|
34
|
+
console.log("\nTest 2: Element not found (timeout)");
|
|
35
|
+
(AndroidObserve as any).prototype.getUITree = async () => ({
|
|
36
|
+
device: { platform: "android", id: "mock", osVersion: "12", model: "Pixel", simulator: true },
|
|
37
|
+
screen: "",
|
|
38
|
+
resolution: { width: 1080, height: 1920 },
|
|
39
|
+
elements: []
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const start2 = Date.now();
|
|
43
|
+
const result2 = await interact.waitForElement("Target", 1200);
|
|
44
|
+
const elapsed2 = Date.now() - start2;
|
|
45
|
+
console.log("Result:", result2.found === false ? "PASS" : "FAIL");
|
|
46
|
+
console.log("Elapsed time (should be >= 1200ms):", elapsed2, elapsed2 >= 1200 ? "PASS" : "FAIL");
|
|
47
|
+
|
|
48
|
+
console.log("\nTest 3: Element found after polling");
|
|
49
|
+
let calls = 0;
|
|
50
|
+
(AndroidObserve as any).prototype.getUITree = async () => {
|
|
51
|
+
calls++;
|
|
52
|
+
if (calls < 3) {
|
|
53
|
+
return {
|
|
54
|
+
device: { platform: "android", id: "mock", osVersion: "12", model: "Pixel", simulator: true },
|
|
55
|
+
screen: "",
|
|
56
|
+
resolution: { width: 1080, height: 1920 },
|
|
57
|
+
elements: []
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
device: { platform: "android", id: "mock", osVersion: "12", model: "Pixel", simulator: true },
|
|
62
|
+
screen: "",
|
|
63
|
+
resolution: { width: 1080, height: 1920 },
|
|
64
|
+
elements: [{
|
|
65
|
+
text: "Target",
|
|
66
|
+
type: "Button",
|
|
67
|
+
contentDescription: null,
|
|
68
|
+
clickable: true,
|
|
69
|
+
enabled: true,
|
|
70
|
+
visible: true,
|
|
71
|
+
bounds: [0, 0, 100, 100],
|
|
72
|
+
resourceId: null
|
|
73
|
+
}]
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const start3 = Date.now();
|
|
78
|
+
const result3 = await interact.waitForElement("Target", 2000);
|
|
79
|
+
const elapsed3 = Date.now() - start3;
|
|
80
|
+
console.log("Result:", result3.found === true ? "PASS" : "FAIL");
|
|
81
|
+
console.log("Calls:", calls, calls === 3 ? "PASS" : "FAIL");
|
|
82
|
+
console.log("Elapsed time (should be >= 1000ms):", elapsed3, elapsed3 >= 1000 ? "PASS" : "FAIL");
|
|
83
|
+
|
|
84
|
+
console.log("\nTest 4: Error handling (fast failure)");
|
|
85
|
+
(AndroidObserve as any).prototype.getUITree = async () => ({
|
|
86
|
+
device: { platform: "android", id: "mock", osVersion: "12", model: "Pixel", simulator: true },
|
|
87
|
+
screen: "",
|
|
88
|
+
resolution: { width: 0, height: 0 },
|
|
89
|
+
elements: [],
|
|
90
|
+
error: "ADB Connection Failed"
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const start4 = Date.now();
|
|
94
|
+
const result4 = await interact.waitForElement("Target", 5000);
|
|
95
|
+
const elapsed4 = Date.now() - start4;
|
|
96
|
+
console.log("Result:", result4.found === false && result4.error === "ADB Connection Failed" ? "PASS" : "FAIL");
|
|
97
|
+
console.log("Error Message:", result4.error);
|
|
98
|
+
console.log("Elapsed time (should be < 500ms):", elapsed4, elapsed4 < 500 ? "PASS" : "FAIL");
|
|
99
|
+
|
|
100
|
+
// Restore
|
|
101
|
+
(AndroidObserve as any).prototype.getUITree = originalGetUITree;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
runTests().catch(console.error);
|
package/tsconfig.json
CHANGED
package/smoke-test.js
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import { startAndroidApp, terminateAndroidApp, captureAndroidScreen, getAndroidLogs } from "./src/android.js";
|
|
2
|
-
import { startIOSApp, terminateIOSApp, captureIOSScreenshot, getIOSLogs } from "./src/ios.js";
|
|
3
|
-
import fs from "fs/promises";
|
|
4
|
-
async function main() {
|
|
5
|
-
const args = process.argv.slice(2);
|
|
6
|
-
const platform = args[0]; // Cast to string first
|
|
7
|
-
const appId = args[1];
|
|
8
|
-
if ((platform !== "android" && platform !== "ios") || !appId) {
|
|
9
|
-
console.error("Usage: npx tsx smoke-test.ts <android|ios> <appId>");
|
|
10
|
-
process.exit(1);
|
|
11
|
-
}
|
|
12
|
-
console.log(`\n🚀 Starting smoke test for ${platform} app: ${appId}`);
|
|
13
|
-
try {
|
|
14
|
-
// 1. Start App
|
|
15
|
-
console.log(`[1/4] Starting app...`);
|
|
16
|
-
let startResult;
|
|
17
|
-
let launchTimeMs;
|
|
18
|
-
if (platform === "android") {
|
|
19
|
-
const result = await startAndroidApp(appId);
|
|
20
|
-
startResult = result.appStarted;
|
|
21
|
-
launchTimeMs = result.launchTimeMs;
|
|
22
|
-
}
|
|
23
|
-
else {
|
|
24
|
-
const result = await startIOSApp(appId);
|
|
25
|
-
startResult = result.appStarted;
|
|
26
|
-
launchTimeMs = result.launchTimeMs;
|
|
27
|
-
}
|
|
28
|
-
if (startResult) {
|
|
29
|
-
console.log(`✅ App started successfully (Launch time: ${launchTimeMs}ms)`);
|
|
30
|
-
}
|
|
31
|
-
else {
|
|
32
|
-
throw new Error("Failed to start app");
|
|
33
|
-
}
|
|
34
|
-
// Wait for app to settle
|
|
35
|
-
console.log(`⏳ Waiting 3s for app to load...`);
|
|
36
|
-
await new Promise(r => setTimeout(r, 3000));
|
|
37
|
-
// 2. Capture Screenshot
|
|
38
|
-
console.log(`[2/4] Capturing screenshot...`);
|
|
39
|
-
let screenshotBase64;
|
|
40
|
-
let resolution;
|
|
41
|
-
if (platform === "android") {
|
|
42
|
-
const result = await captureAndroidScreen(appId);
|
|
43
|
-
screenshotBase64 = result.screenshot;
|
|
44
|
-
resolution = result.resolution;
|
|
45
|
-
}
|
|
46
|
-
else {
|
|
47
|
-
const result = await captureIOSScreenshot();
|
|
48
|
-
screenshotBase64 = result.screenshot;
|
|
49
|
-
resolution = result.resolution;
|
|
50
|
-
}
|
|
51
|
-
if (screenshotBase64) {
|
|
52
|
-
const fileName = `smoke-test-${platform}.png`;
|
|
53
|
-
await fs.writeFile(fileName, Buffer.from(screenshotBase64, 'base64'));
|
|
54
|
-
console.log(`✅ Screenshot saved to ./${fileName} (${resolution.width}x${resolution.height})`);
|
|
55
|
-
}
|
|
56
|
-
else {
|
|
57
|
-
throw new Error("Failed to capture screenshot");
|
|
58
|
-
}
|
|
59
|
-
// 3. Get Logs
|
|
60
|
-
console.log(`[3/4] Fetching logs...`);
|
|
61
|
-
let logsCount = 0;
|
|
62
|
-
let logs = [];
|
|
63
|
-
if (platform === "android") {
|
|
64
|
-
const result = await getAndroidLogs(appId, 50);
|
|
65
|
-
logsCount = result.logCount;
|
|
66
|
-
logs = result.logs;
|
|
67
|
-
}
|
|
68
|
-
else {
|
|
69
|
-
const result = await getIOSLogs();
|
|
70
|
-
logsCount = result.logCount;
|
|
71
|
-
logs = result.logs;
|
|
72
|
-
}
|
|
73
|
-
console.log(`✅ Retrieved ${logsCount} log lines`);
|
|
74
|
-
// Print last log line as sample
|
|
75
|
-
if (logs.length > 0) {
|
|
76
|
-
console.log(` Sample: "${logs[logs.length - 1].substring(0, 80)}..."`);
|
|
77
|
-
}
|
|
78
|
-
// 4. Terminate App
|
|
79
|
-
console.log(`[4/4] Terminating app...`);
|
|
80
|
-
let termResult;
|
|
81
|
-
if (platform === "android") {
|
|
82
|
-
const result = await terminateAndroidApp(appId);
|
|
83
|
-
termResult = result.appTerminated;
|
|
84
|
-
}
|
|
85
|
-
else {
|
|
86
|
-
const result = await terminateIOSApp(appId);
|
|
87
|
-
termResult = result.appTerminated;
|
|
88
|
-
}
|
|
89
|
-
if (termResult) {
|
|
90
|
-
console.log(`✅ App terminated successfully`);
|
|
91
|
-
}
|
|
92
|
-
else {
|
|
93
|
-
throw new Error("Failed to terminate app");
|
|
94
|
-
}
|
|
95
|
-
console.log(`\n✨ Smoke test COMPLETED SUCCESSFULLY! ✨\n`);
|
|
96
|
-
}
|
|
97
|
-
catch (error) {
|
|
98
|
-
console.error(`\n❌ Smoke test FAILED:`, error);
|
|
99
|
-
process.exit(1);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
main();
|
package/test/run-real-test.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
// This script wraps the real test execution for ease of use
|
|
2
|
-
// It sets ADB_PATH and invokes the test file
|
|
3
|
-
import { spawn } from 'child_process';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import { fileURLToPath } from 'url';
|
|
6
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
-
const __dirname = path.dirname(__filename);
|
|
8
|
-
const ADB_PATH = process.env.ADB_PATH || process.env.ADB || 'adb';
|
|
9
|
-
const TEST_FILE = path.join(__dirname, 'wait_for_element_real.js');
|
|
10
|
-
|
|
11
|
-
// Merge ADB_PATH into the child environment if provided
|
|
12
|
-
const childEnv = { ...process.env, ADB_PATH };
|
|
13
|
-
|
|
14
|
-
// Use NODE to execute the JS test by default, or respect RUNNER env if set (e.g., 'npx tsx')
|
|
15
|
-
const runner = process.env.RUNNER || 'node';
|
|
16
|
-
const runnerArgs = [TEST_FILE];
|
|
17
|
-
|
|
18
|
-
const child = spawn(runner, runnerArgs, {
|
|
19
|
-
env: childEnv,
|
|
20
|
-
stdio: 'inherit'
|
|
21
|
-
});
|
|
22
|
-
child.on('exit', (code) => {
|
|
23
|
-
process.exit(code || 0);
|
|
24
|
-
});
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import { AndroidInteract } from "../src/android/interact.js";
|
|
2
|
-
import { AndroidObserve } from "../src/android/observe.js";
|
|
3
|
-
// Mock the observe method
|
|
4
|
-
// We need to override the prototype method directly
|
|
5
|
-
// Since we are using ES modules, we need to handle the import correctly or just override the instance method if possible.
|
|
6
|
-
// But getUITree is an instance method on AndroidObserve, which AndroidInteract instantiates internally.
|
|
7
|
-
// We can override the prototype of AndroidObserve to affect all instances.
|
|
8
|
-
const originalGetUITree = AndroidObserve.prototype.getUITree;
|
|
9
|
-
async function runTests() {
|
|
10
|
-
console.log("Starting tests for wait_for_element...");
|
|
11
|
-
const interact = new AndroidInteract();
|
|
12
|
-
// ---------------------------------------------------------
|
|
13
|
-
// Test 1: Element found immediately
|
|
14
|
-
// ---------------------------------------------------------
|
|
15
|
-
console.log("\nTest 1: Element found immediately");
|
|
16
|
-
AndroidObserve.prototype.getUITree = async () => ({
|
|
17
|
-
device: { platform: "android", id: "mock", osVersion: "12", model: "Pixel", simulator: true },
|
|
18
|
-
screen: "",
|
|
19
|
-
resolution: { width: 1080, height: 1920 },
|
|
20
|
-
elements: [{
|
|
21
|
-
text: "Target",
|
|
22
|
-
type: "Button",
|
|
23
|
-
contentDescription: null,
|
|
24
|
-
clickable: true,
|
|
25
|
-
enabled: true,
|
|
26
|
-
visible: true,
|
|
27
|
-
bounds: [0, 0, 100, 100],
|
|
28
|
-
resourceId: null
|
|
29
|
-
}]
|
|
30
|
-
});
|
|
31
|
-
const start1 = Date.now();
|
|
32
|
-
const result1 = await interact.waitForElement("Target", 1000);
|
|
33
|
-
const elapsed1 = Date.now() - start1;
|
|
34
|
-
console.log("Result:", result1.found === true ? "PASS" : "FAIL");
|
|
35
|
-
console.log("Element:", result1.element ? "FOUND" : "MISSING");
|
|
36
|
-
console.log("Elapsed:", elapsed1, "ms");
|
|
37
|
-
// ---------------------------------------------------------
|
|
38
|
-
// Test 2: Element not found (timeout)
|
|
39
|
-
// ---------------------------------------------------------
|
|
40
|
-
console.log("\nTest 2: Element not found (timeout)");
|
|
41
|
-
AndroidObserve.prototype.getUITree = async () => ({
|
|
42
|
-
device: { platform: "android", id: "mock", osVersion: "12", model: "Pixel", simulator: true },
|
|
43
|
-
screen: "",
|
|
44
|
-
resolution: { width: 1080, height: 1920 },
|
|
45
|
-
elements: []
|
|
46
|
-
});
|
|
47
|
-
const start2 = Date.now();
|
|
48
|
-
// Use a short timeout for test speed, but long enough to sleep at least once
|
|
49
|
-
const result2 = await interact.waitForElement("Target", 1200);
|
|
50
|
-
const elapsed2 = Date.now() - start2;
|
|
51
|
-
console.log("Result:", result2.found === false ? "PASS" : "FAIL");
|
|
52
|
-
// Should wait at least 1200ms
|
|
53
|
-
console.log("Elapsed time (should be >= 1200ms):", elapsed2, elapsed2 >= 1200 ? "PASS" : "FAIL");
|
|
54
|
-
// ---------------------------------------------------------
|
|
55
|
-
// Test 3: Element found after polling
|
|
56
|
-
// ---------------------------------------------------------
|
|
57
|
-
console.log("\nTest 3: Element found after polling");
|
|
58
|
-
let calls = 0;
|
|
59
|
-
AndroidObserve.prototype.getUITree = async () => {
|
|
60
|
-
calls++;
|
|
61
|
-
if (calls < 3) {
|
|
62
|
-
return {
|
|
63
|
-
device: { platform: "android", id: "mock", osVersion: "12", model: "Pixel", simulator: true },
|
|
64
|
-
screen: "",
|
|
65
|
-
resolution: { width: 1080, height: 1920 },
|
|
66
|
-
elements: []
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
return {
|
|
70
|
-
device: { platform: "android", id: "mock", osVersion: "12", model: "Pixel", simulator: true },
|
|
71
|
-
screen: "",
|
|
72
|
-
resolution: { width: 1080, height: 1920 },
|
|
73
|
-
elements: [{
|
|
74
|
-
text: "Target",
|
|
75
|
-
type: "Button",
|
|
76
|
-
contentDescription: null,
|
|
77
|
-
clickable: true,
|
|
78
|
-
enabled: true,
|
|
79
|
-
visible: true,
|
|
80
|
-
bounds: [0, 0, 100, 100],
|
|
81
|
-
resourceId: null
|
|
82
|
-
}]
|
|
83
|
-
};
|
|
84
|
-
};
|
|
85
|
-
const start3 = Date.now();
|
|
86
|
-
const result3 = await interact.waitForElement("Target", 2000);
|
|
87
|
-
const elapsed3 = Date.now() - start3;
|
|
88
|
-
console.log("Result:", result3.found === true ? "PASS" : "FAIL");
|
|
89
|
-
console.log("Calls:", calls, calls === 3 ? "PASS" : "FAIL");
|
|
90
|
-
// Expected calls: 0ms (fail), 500ms (fail), 1000ms (success). Elapsed should be >= 1000ms.
|
|
91
|
-
console.log("Elapsed time (should be >= 1000ms):", elapsed3, elapsed3 >= 1000 ? "PASS" : "FAIL");
|
|
92
|
-
// ---------------------------------------------------------
|
|
93
|
-
// Test 4: Error handling (fast failure)
|
|
94
|
-
// ---------------------------------------------------------
|
|
95
|
-
console.log("\nTest 4: Error handling (fast failure)");
|
|
96
|
-
AndroidObserve.prototype.getUITree = async () => ({
|
|
97
|
-
device: { platform: "android", id: "mock", osVersion: "12", model: "Pixel", simulator: true },
|
|
98
|
-
screen: "",
|
|
99
|
-
resolution: { width: 0, height: 0 },
|
|
100
|
-
elements: [],
|
|
101
|
-
error: "ADB Connection Failed"
|
|
102
|
-
});
|
|
103
|
-
const start4 = Date.now();
|
|
104
|
-
const result4 = await interact.waitForElement("Target", 5000);
|
|
105
|
-
const elapsed4 = Date.now() - start4;
|
|
106
|
-
console.log("Result:", result4.found === false && result4.error === "ADB Connection Failed" ? "PASS" : "FAIL");
|
|
107
|
-
console.log("Error Message:", result4.error);
|
|
108
|
-
// Should fail fast, not wait for timeout
|
|
109
|
-
console.log("Elapsed time (should be < 500ms):", elapsed4, elapsed4 < 500 ? "PASS" : "FAIL");
|
|
110
|
-
// Restore original
|
|
111
|
-
AndroidObserve.prototype.getUITree = originalGetUITree;
|
|
112
|
-
}
|
|
113
|
-
runTests().catch(console.error);
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { AndroidInteract } from "../src/android/interact.js";
|
|
2
|
-
import { AndroidObserve } from "../src/android/observe.js";
|
|
3
|
-
// Configure ADB path and target via env vars or CLI args.
|
|
4
|
-
// Usage: node test/wait_for_element_real.js [deviceId] [appId]
|
|
5
|
-
// Priority: CLI args > environment variables
|
|
6
|
-
const args = process.argv.slice(2);
|
|
7
|
-
const DEVICE_ID = args[0] || process.env.DEVICE_ID;
|
|
8
|
-
const APP_ID = args[1] || process.env.APP_ID;
|
|
9
|
-
|
|
10
|
-
// Do not hard-code ADB_PATH here. If the user supplied ADB_PATH in the environment, leave it as-is.
|
|
11
|
-
// (process.env.ADB_PATH may be set externally before running this script.)
|
|
12
|
-
|
|
13
|
-
if (!DEVICE_ID || !APP_ID) {
|
|
14
|
-
console.error("Usage: node test/wait_for_element_real.js <deviceId> <appId> or set DEVICE_ID and APP_ID env vars");
|
|
15
|
-
process.exit(1);
|
|
16
|
-
}
|
|
17
|
-
async function runRealTest() {
|
|
18
|
-
console.log(`Connecting to device ${DEVICE_ID}...`);
|
|
19
|
-
const interact = new AndroidInteract();
|
|
20
|
-
const observe = new AndroidObserve();
|
|
21
|
-
try {
|
|
22
|
-
// 1. Start App
|
|
23
|
-
console.log(`\nStarting app ${APP_ID}...`);
|
|
24
|
-
await interact.startApp(APP_ID, DEVICE_ID);
|
|
25
|
-
// Give it a moment to render
|
|
26
|
-
console.log("Waiting 3s for app to render...");
|
|
27
|
-
await new Promise(r => setTimeout(r, 3000));
|
|
28
|
-
// 2. Get UI Tree to find a valid text target
|
|
29
|
-
console.log("\nFetching UI Tree to find a target text...");
|
|
30
|
-
const tree = await observe.getUITree(DEVICE_ID);
|
|
31
|
-
if (tree.error) {
|
|
32
|
-
console.error("Failed to get UI Tree:", tree.error);
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
// Find first visible element with text
|
|
36
|
-
const targetElement = tree.elements.find(e => e.text && e.text.length > 0 && e.visible);
|
|
37
|
-
if (!targetElement || !targetElement.text) {
|
|
38
|
-
console.warn("No visible text elements found on screen to test with.");
|
|
39
|
-
console.log("Elements found:", tree.elements.length);
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
const targetText = targetElement.text;
|
|
43
|
-
console.log(`Found target element: "${targetText}"`);
|
|
44
|
-
// 3. Test waitForElement (Success Case)
|
|
45
|
-
console.log(`\nTest 1: Waiting for existing element "${targetText}" (should succeed)...`);
|
|
46
|
-
const start1 = Date.now();
|
|
47
|
-
const result1 = await interact.waitForElement(targetText, 5000, DEVICE_ID);
|
|
48
|
-
const elapsed1 = Date.now() - start1;
|
|
49
|
-
console.log(`Result: ${result1.found ? "PASS" : "FAIL"}`);
|
|
50
|
-
console.log(`Found Element: ${result1.element?.text}`);
|
|
51
|
-
console.log(`Time taken: ${elapsed1}ms`);
|
|
52
|
-
// 4. Test waitForElement (Timeout Case)
|
|
53
|
-
const missingText = "THIS_TEXT_SHOULD_NOT_EXIST_XYZ_123";
|
|
54
|
-
console.log(`\nTest 2: Waiting for missing element "${missingText}" (should timeout)...`);
|
|
55
|
-
const start2 = Date.now();
|
|
56
|
-
// Use short timeout 2s
|
|
57
|
-
const result2 = await interact.waitForElement(missingText, 2000, DEVICE_ID);
|
|
58
|
-
const elapsed2 = Date.now() - start2;
|
|
59
|
-
console.log(`Result: ${!result2.found ? "PASS" : "FAIL"}`);
|
|
60
|
-
console.log(`Found: ${result2.found}`);
|
|
61
|
-
console.log(`Time taken: ${elapsed2}ms (expected ~2000ms)`);
|
|
62
|
-
}
|
|
63
|
-
catch (error) {
|
|
64
|
-
console.error("Test failed with error:", error);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
runRealTest();
|
package/test-ui-tree.js
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Test script for verify UI Tree functionality.
|
|
3
|
-
*
|
|
4
|
-
* Usage:
|
|
5
|
-
* npx tsx test-ui-tree.ts [android|ios] [deviceId]
|
|
6
|
-
*
|
|
7
|
-
* Examples:
|
|
8
|
-
* npx tsx test-ui-tree.ts android
|
|
9
|
-
* npx tsx test-ui-tree.ts ios booted
|
|
10
|
-
*/
|
|
11
|
-
import { AndroidObserve } from "./src/android/observe.js";
|
|
12
|
-
import { iOSObserve } from "./src/ios/observe.js";
|
|
13
|
-
async function main() {
|
|
14
|
-
const args = process.argv.slice(2);
|
|
15
|
-
const platform = (args[0] || 'android').toLowerCase();
|
|
16
|
-
const deviceId = args[1];
|
|
17
|
-
console.log(`Starting UI Tree Test for ${platform}...`);
|
|
18
|
-
if (deviceId)
|
|
19
|
-
console.log(`Targeting device: ${deviceId}`);
|
|
20
|
-
try {
|
|
21
|
-
let result;
|
|
22
|
-
if (platform === 'ios') {
|
|
23
|
-
const observer = new iOSObserve();
|
|
24
|
-
result = await observer.getUITree(deviceId || 'booted');
|
|
25
|
-
}
|
|
26
|
-
else {
|
|
27
|
-
const observer = new AndroidObserve();
|
|
28
|
-
result = await observer.getUITree(deviceId);
|
|
29
|
-
}
|
|
30
|
-
console.log("\nUI Tree Result Summary:");
|
|
31
|
-
console.log("-----------------------");
|
|
32
|
-
if (result.error) {
|
|
33
|
-
console.error("❌ Error:", result.error);
|
|
34
|
-
process.exit(1);
|
|
35
|
-
}
|
|
36
|
-
console.log(`Device: ${result.device.platform} (${result.device.model || 'Unknown Model'})`);
|
|
37
|
-
console.log(`Resolution: ${result.resolution.width}x${result.resolution.height}`);
|
|
38
|
-
console.log(`Elements Found: ${result.elements.length}`);
|
|
39
|
-
if (result.elements.length === 0) {
|
|
40
|
-
console.warn("⚠️ Warning: No elements found. Is the screen empty or locked?");
|
|
41
|
-
}
|
|
42
|
-
else {
|
|
43
|
-
// Print sample element to verify structure
|
|
44
|
-
const first = result.elements[0];
|
|
45
|
-
console.log("\nSample Element (First):");
|
|
46
|
-
console.log(JSON.stringify(first, null, 2));
|
|
47
|
-
// Check for new fields
|
|
48
|
-
if (first.center && first.depth !== undefined) {
|
|
49
|
-
console.log("\n✅ Verified 'center' and 'depth' fields exist.");
|
|
50
|
-
}
|
|
51
|
-
else {
|
|
52
|
-
console.error("\n❌ 'center' or 'depth' fields missing!");
|
|
53
|
-
process.exit(1);
|
|
54
|
-
}
|
|
55
|
-
// Check for filtering
|
|
56
|
-
const interactive = result.elements.filter(e => e.clickable).length;
|
|
57
|
-
const withText = result.elements.filter(e => e.text).length;
|
|
58
|
-
console.log(`\nStats:`);
|
|
59
|
-
console.log(`- Interactive elements: ${interactive}`);
|
|
60
|
-
console.log(`- Elements with text: ${withText}`);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
catch (error) {
|
|
64
|
-
console.error("\n❌ Test Failed:", error);
|
|
65
|
-
process.exit(1);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
main();
|