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.
Files changed (51) hide show
  1. package/README.md +18 -443
  2. package/dist/android/interact.js +96 -1
  3. package/dist/android/utils.js +404 -12
  4. package/dist/ios/interact.js +105 -0
  5. package/dist/ios/utils.js +154 -0
  6. package/dist/resolve-device.js +70 -0
  7. package/dist/server.js +126 -194
  8. package/dist/tools/app.js +45 -0
  9. package/dist/tools/devices.js +5 -0
  10. package/dist/tools/install.js +47 -0
  11. package/dist/tools/logs.js +62 -0
  12. package/dist/tools/screenshot.js +17 -0
  13. package/dist/tools/ui.js +57 -0
  14. package/docs/CHANGELOG.md +19 -0
  15. package/docs/TOOLS.md +272 -0
  16. package/package.json +6 -2
  17. package/src/android/interact.ts +100 -1
  18. package/src/android/utils.ts +395 -10
  19. package/src/ios/interact.ts +102 -0
  20. package/src/ios/utils.ts +157 -0
  21. package/src/resolve-device.ts +80 -0
  22. package/src/server.ts +149 -276
  23. package/src/tools/app.ts +46 -0
  24. package/src/tools/devices.ts +6 -0
  25. package/src/tools/install.ts +43 -0
  26. package/src/tools/logs.ts +62 -0
  27. package/src/tools/screenshot.ts +18 -0
  28. package/src/tools/ui.ts +62 -0
  29. package/src/types.ts +7 -0
  30. package/test/integration/index.ts +8 -0
  31. package/test/integration/install.integration.ts +64 -0
  32. package/test/integration/logstream-real.ts +35 -0
  33. package/test/integration/run-install-android.ts +21 -0
  34. package/test/integration/run-install-ios.ts +21 -0
  35. package/test/integration/run-real-test.ts +19 -0
  36. package/{smoke-test.ts → test/integration/smoke-test.ts} +17 -25
  37. package/test/integration/test-dist.mjs +41 -0
  38. package/test/integration/test-dist.ts +41 -0
  39. package/{test-ui-tree.ts → test/integration/test-ui-tree.ts} +2 -2
  40. package/test/integration/wait_for_element_real.ts +80 -0
  41. package/test/unit/index.ts +7 -0
  42. package/test/unit/install.test.ts +82 -0
  43. package/test/unit/logparse.test.ts +41 -0
  44. package/test/unit/logstream.test.ts +46 -0
  45. package/test/unit/wait_for_element_mock.ts +104 -0
  46. package/tsconfig.json +1 -1
  47. package/smoke-test.js +0 -102
  48. package/test/run-real-test.js +0 -24
  49. package/test/wait_for_element_mock.js +0 -113
  50. package/test/wait_for_element_real.js +0 -67
  51. 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
@@ -9,5 +9,5 @@
9
9
  "skipLibCheck": true,
10
10
  "esModuleInterop": true
11
11
  },
12
- "exclude": ["smoke-test.ts", "test-ui-tree.ts"]
12
+ "exclude": ["smoke-test.ts", "test-ui-tree.ts", "test/**/*.ts"]
13
13
  }
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();
@@ -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();