mobile-debug-mcp 0.20.1 → 0.21.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/dist/interact/index.js +154 -118
- package/dist/scripts/capture_ui_after_tap.mjs +43 -0
- package/dist/scripts/check_play_observe.mjs +18 -0
- package/dist/scripts/check_play_substring.mjs +38 -0
- package/dist/scripts/dump_ui_tree.mjs +20 -0
- package/dist/scripts/observe-test.mjs +32 -0
- package/dist/scripts/press_and_observe.mjs +90 -0
- package/dist/scripts/press_and_wait_ui.mjs +85 -0
- package/dist/scripts/test_generate_and_wait.mjs +123 -0
- package/dist/server.js +18 -0
- package/dist/system/gradle.js +4 -4
- package/dist/utils/android/utils.js +2 -2
- package/docs/CHANGELOG.md +3 -0
- package/package.json +1 -1
- package/src/interact/index.ts +96 -68
- package/src/server.ts +20 -0
- package/src/system/gradle.ts +4 -4
- package/src/utils/android/utils.ts +2 -2
- package/test/observe/unit/observe_until_edge_cases.test.ts +41 -0
- package/test/observe/unit/observe_until_stability.test.ts +30 -0
- package/test/interact/device/observe_until_device.ts +0 -24
package/dist/interact/index.js
CHANGED
|
@@ -3,7 +3,6 @@ import { iOSInteract } from './ios.js';
|
|
|
3
3
|
export { AndroidInteract, iOSInteract };
|
|
4
4
|
import { resolveTargetDevice } from '../utils/resolve-device.js';
|
|
5
5
|
import { ToolsObserve } from '../observe/index.js';
|
|
6
|
-
const STABLE_IDLE_MS = 1000;
|
|
7
6
|
export class ToolsInteract {
|
|
8
7
|
static async getInteractionService(platform, deviceId) {
|
|
9
8
|
const effectivePlatform = platform || 'android';
|
|
@@ -259,167 +258,204 @@ export class ToolsInteract {
|
|
|
259
258
|
}
|
|
260
259
|
return { success: false, reason: 'timeout', lastFingerprint, elapsedMs: Date.now() - start };
|
|
261
260
|
}
|
|
262
|
-
static async observeUntilHandler({ type, query, timeoutMs =
|
|
261
|
+
static async observeUntilHandler({ type = 'ui', query, timeoutMs = 30000, pollIntervalMs = 300, includeSnapshotOnFailure = true, match = 'present', stability_ms = 700, observationDelayMs = 0, platform, deviceId }) {
|
|
263
262
|
const start = Date.now();
|
|
264
263
|
const deadline = start + (timeoutMs || 0);
|
|
265
264
|
const q = (query === null || query === undefined) ? '' : String(query);
|
|
266
|
-
//
|
|
265
|
+
// Clamp polling interval to 250-500ms for consistent behavior
|
|
266
|
+
const pollInterval = Math.max(250, Math.min(pollIntervalMs || 300, 500));
|
|
267
|
+
// Baseline state (fetch in parallel but bound to short timeouts so observation starts promptly)
|
|
267
268
|
let initialFingerprint = null;
|
|
268
|
-
try {
|
|
269
|
-
const fpRes = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId });
|
|
270
|
-
initialFingerprint = fpRes?.fingerprint ?? null;
|
|
271
|
-
}
|
|
272
|
-
catch (err) {
|
|
273
|
-
console.error('observeUntil: error getting initial fingerprint', err);
|
|
274
|
-
initialFingerprint = null;
|
|
275
|
-
}
|
|
276
|
-
// For logs, capture a baseline snapshot (count or last line) to avoid matching historical lines
|
|
277
269
|
let baselineLastLine = null;
|
|
278
270
|
try {
|
|
279
|
-
const
|
|
280
|
-
const
|
|
281
|
-
|
|
271
|
+
const fpPromise = ToolsObserve.getScreenFingerprintHandler({ platform, deviceId });
|
|
272
|
+
const logsPromise = ToolsObserve.getLogsHandler({ platform, deviceId, lines: 200 });
|
|
273
|
+
const withTimeout = (p, ms) => Promise.race([p, new Promise(resolve => setTimeout(() => resolve(null), ms))]);
|
|
274
|
+
const [fpRes, gl] = await Promise.all([withTimeout(fpPromise, 300), withTimeout(logsPromise, 500)]);
|
|
275
|
+
if (fpRes && typeof fpRes === 'object')
|
|
276
|
+
initialFingerprint = fpRes.fingerprint ?? null;
|
|
277
|
+
if (gl) {
|
|
278
|
+
const logsArr = Array.isArray(gl.logs) ? gl.logs : [];
|
|
279
|
+
baselineLastLine = logsArr.length ? logsArr[logsArr.length - 1] : null;
|
|
280
|
+
}
|
|
282
281
|
}
|
|
283
282
|
catch (err) {
|
|
284
|
-
// non-fatal but surface warning to aid debugging
|
|
285
283
|
try {
|
|
286
|
-
console.warn('observeUntil: failed to get baseline
|
|
284
|
+
console.warn('observeUntil: failed to get baseline data (non-fatal):', err instanceof Error ? err.message : String(err));
|
|
287
285
|
}
|
|
288
286
|
catch { }
|
|
289
287
|
}
|
|
288
|
+
// Network-based waiting removed. Rely on UI and screen fingerprints for determinism.
|
|
290
289
|
let lastChangeAt = Date.now();
|
|
291
290
|
let prevFingerprint = initialFingerprint;
|
|
292
291
|
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
292
|
+
// Optional initial observation delay requested by caller
|
|
293
|
+
if (typeof observationDelayMs === 'number' && observationDelayMs > 0) {
|
|
294
|
+
try {
|
|
295
|
+
console.log(`observeUntil: delaying observation for ${observationDelayMs}ms`);
|
|
296
|
+
}
|
|
297
|
+
catch { }
|
|
298
|
+
await sleep(observationDelayMs);
|
|
299
|
+
}
|
|
293
300
|
// Telemetry
|
|
294
301
|
let pollCount = 0;
|
|
295
|
-
let
|
|
302
|
+
let matchedAt = null;
|
|
303
|
+
let lastObservedState = null;
|
|
304
|
+
let stableDuration = 0;
|
|
296
305
|
let matchSource = null;
|
|
297
306
|
while (Date.now() <= deadline) {
|
|
298
307
|
pollCount++;
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
308
|
+
const now = Date.now();
|
|
309
|
+
// Evaluate condition per type
|
|
310
|
+
if (type === 'ui') {
|
|
311
|
+
try {
|
|
312
|
+
// Lightweight UI check: fetch UI tree and perform a normalized substring match to reduce overhead
|
|
302
313
|
try {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
else if (type === 'log') {
|
|
322
|
-
try {
|
|
323
|
-
// Try reading from active stream first
|
|
324
|
-
const stream = await ToolsObserve.readLogStreamHandler({ platform, sessionId: 'default', limit: 200 });
|
|
325
|
-
const entries = (stream && Array.isArray(stream.entries)) ? stream.entries : [];
|
|
326
|
-
for (const ent of entries) {
|
|
327
|
-
const msg = ent && (ent.message || ent.msg || ent) ? (ent.message || ent.msg || ent) : '';
|
|
328
|
-
if (q && String(msg).includes(q)) {
|
|
329
|
-
timeToMatch = Date.now() - start;
|
|
330
|
-
matchSource = 'log-stream';
|
|
331
|
-
return { success: true, type: 'log', matched: true, details: `Log matched '${q}'`, timestamp: Date.now(), log: { message: msg, raw: ent }, telemetry: { pollCount, timeToMatch, elapsedMs: Date.now() - start, matchSource } };
|
|
314
|
+
// Bound the UI tree fetch to avoid long blocking calls; prefer quick failure over hanging a poll
|
|
315
|
+
const withTimeout = (p, ms) => Promise.race([p, new Promise(resolve => setTimeout(() => resolve(null), ms))]);
|
|
316
|
+
const tree = await withTimeout(ToolsObserve.getUITreeHandler({ platform, deviceId }), Math.min(pollInterval, 500));
|
|
317
|
+
const elems = Array.isArray(tree && tree.elements) ? tree.elements : [];
|
|
318
|
+
const qnorm = q.toLowerCase();
|
|
319
|
+
let matched = null;
|
|
320
|
+
for (const el of elems) {
|
|
321
|
+
try {
|
|
322
|
+
const txt = ((el && (el.text || el.label || el.value || el.contentDescription || el.accessibilityLabel)) || '');
|
|
323
|
+
if (!txt)
|
|
324
|
+
continue;
|
|
325
|
+
if (String(txt).toLowerCase().includes(qnorm)) {
|
|
326
|
+
matched = el;
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
continue;
|
|
332
332
|
}
|
|
333
333
|
}
|
|
334
|
-
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
timeToMatch = Date.now() - start;
|
|
347
|
-
matchSource = 'log-snapshot';
|
|
348
|
-
return { success: true, type: 'log', matched: true, details: `Log matched '${q}'`, timestamp: Date.now(), log: { message: line }, telemetry: { pollCount, timeToMatch, elapsedMs: Date.now() - start, matchSource } };
|
|
334
|
+
const isPresent = !!matched;
|
|
335
|
+
const conditionTrue = (match === 'present') ? isPresent : !isPresent;
|
|
336
|
+
if (conditionTrue) {
|
|
337
|
+
if (matchedAt === null)
|
|
338
|
+
matchedAt = Date.now();
|
|
339
|
+
stableDuration = Date.now() - matchedAt;
|
|
340
|
+
lastObservedState = true;
|
|
341
|
+
if (stableDuration >= stability_ms) {
|
|
342
|
+
matchSource = 'ui-tree-' + (match === 'present' ? 'present' : 'absent');
|
|
343
|
+
const element = isPresent ? matched : null;
|
|
344
|
+
const now2 = Date.now();
|
|
345
|
+
return { success: true, condition: match, query: q, poll_count: pollCount, duration_ms: now2 - start, stable_duration_ms: stableDuration, matchedElement: element, matchSource, timestamp: now2, type: 'ui', observed_state: lastObservedState ?? null };
|
|
349
346
|
}
|
|
350
347
|
}
|
|
348
|
+
else {
|
|
349
|
+
matchedAt = null;
|
|
350
|
+
stableDuration = 0;
|
|
351
|
+
lastObservedState = false;
|
|
352
|
+
}
|
|
351
353
|
}
|
|
352
354
|
catch (err) {
|
|
353
|
-
console.error('observeUntil(
|
|
355
|
+
console.error('observeUntil(ui) tree error:', err);
|
|
354
356
|
}
|
|
355
357
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
}
|
|
371
|
-
catch (err) {
|
|
372
|
-
console.error('observeUntil(screen) find error:', err);
|
|
373
|
-
}
|
|
374
|
-
// If query provided but not matched yet, continue polling until timeout
|
|
375
|
-
}
|
|
376
|
-
else {
|
|
377
|
-
timeToMatch = Date.now() - start;
|
|
378
|
-
matchSource = 'screen-fingerprint';
|
|
379
|
-
return { success: true, type: 'screen', matched: true, details: 'Screen fingerprint changed', timestamp: Date.now(), newFingerprint: fp, telemetry: { pollCount, timeToMatch, elapsedMs: Date.now() - start, matchSource } };
|
|
380
|
-
}
|
|
358
|
+
catch (err) {
|
|
359
|
+
console.error('observeUntil(ui) find error:', err);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
else if (type === 'log') {
|
|
363
|
+
try {
|
|
364
|
+
// Logs: presence semantics only (match 'present'). Stability not applicable (immediate)
|
|
365
|
+
const stream = await ToolsObserve.readLogStreamHandler({ platform, sessionId: 'default', limit: 200 });
|
|
366
|
+
const entries = (stream && Array.isArray(stream.entries)) ? stream.entries : [];
|
|
367
|
+
for (const ent of entries) {
|
|
368
|
+
const msg = ent && (ent.message || ent.msg || ent) ? (ent.message || ent.msg || ent) : '';
|
|
369
|
+
if (q && String(msg).includes(q)) {
|
|
370
|
+
const now2 = Date.now();
|
|
371
|
+
return { success: true, condition: 'present', query: q, poll_count: pollCount, duration_ms: now2 - start, stable_duration_ms: 0, matchedLog: { message: msg, raw: ent }, matchSource: 'log-stream', timestamp: now2, type: 'log', observed_state: true };
|
|
381
372
|
}
|
|
382
373
|
}
|
|
383
|
-
|
|
384
|
-
|
|
374
|
+
const gl = await ToolsObserve.getLogsHandler({ platform, deviceId, lines: 200 });
|
|
375
|
+
const logsArr = Array.isArray(gl && gl.logs) ? gl.logs : [];
|
|
376
|
+
let startIndex = 0;
|
|
377
|
+
if (baselineLastLine) {
|
|
378
|
+
const idx = logsArr.lastIndexOf(baselineLastLine);
|
|
379
|
+
startIndex = idx >= 0 ? idx + 1 : 0;
|
|
380
|
+
}
|
|
381
|
+
for (let i = startIndex; i < logsArr.length; i++) {
|
|
382
|
+
const line = logsArr[i];
|
|
383
|
+
if (q && String(line).includes(q)) {
|
|
384
|
+
const now2 = Date.now();
|
|
385
|
+
return { success: true, condition: 'present', query: q, poll_count: pollCount, duration_ms: now2 - start, stable_duration_ms: 0, matchedLog: { message: line }, matchSource: 'log-snapshot', timestamp: now2, type: 'log', observed_state: true };
|
|
386
|
+
}
|
|
385
387
|
}
|
|
386
388
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
389
|
+
catch (err) {
|
|
390
|
+
console.error('observeUntil(log) error:', err);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
else if (type === 'screen') {
|
|
394
|
+
try {
|
|
395
|
+
const fpRes = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId });
|
|
396
|
+
const fp = fpRes?.fingerprint ?? null;
|
|
397
|
+
if (fp !== null && fp !== undefined && fp !== initialFingerprint) {
|
|
398
|
+
// when screen changed, require stability_ms where fingerprint remains the same
|
|
399
|
+
if (matchedAt === null)
|
|
400
|
+
matchedAt = now;
|
|
401
|
+
const confirmFp = (await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId }))?.fingerprint ?? null;
|
|
402
|
+
if (confirmFp === fp) {
|
|
403
|
+
stableDuration = Date.now() - matchedAt;
|
|
404
|
+
lastObservedState = true;
|
|
405
|
+
if (stableDuration >= stability_ms) {
|
|
406
|
+
const now2 = Date.now();
|
|
407
|
+
return { success: true, condition: 'present', query: q, poll_count: pollCount, duration_ms: now2 - start, stable_duration_ms: stableDuration, newFingerprint: fp, matchSource: 'screen-fingerprint', timestamp: now2, type: 'screen', observed_state: lastObservedState ?? null };
|
|
408
|
+
}
|
|
394
409
|
}
|
|
395
410
|
else {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
return { success: true, type: 'idle', matched: true, details: `UI stable for ${STABLE_IDLE_MS}ms`, timestamp: Date.now(), fingerprint: fp, telemetry: { pollCount, timeToMatch, elapsedMs: Date.now() - start, matchSource } };
|
|
400
|
-
}
|
|
411
|
+
matchedAt = null;
|
|
412
|
+
stableDuration = 0;
|
|
413
|
+
lastObservedState = false;
|
|
401
414
|
}
|
|
402
415
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
416
|
+
}
|
|
417
|
+
catch (err) {
|
|
418
|
+
console.error('observeUntil(screen) error:', err);
|
|
406
419
|
}
|
|
407
420
|
}
|
|
408
|
-
|
|
409
|
-
|
|
421
|
+
else if (type === 'idle') {
|
|
422
|
+
try {
|
|
423
|
+
const fpRes = await ToolsObserve.getScreenFingerprintHandler({ platform, deviceId });
|
|
424
|
+
const fp = fpRes?.fingerprint ?? null;
|
|
425
|
+
if (fp !== prevFingerprint) {
|
|
426
|
+
prevFingerprint = fp;
|
|
427
|
+
lastChangeAt = Date.now();
|
|
428
|
+
matchedAt = null;
|
|
429
|
+
stableDuration = 0;
|
|
430
|
+
lastObservedState = false;
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
const idleMs = Date.now() - lastChangeAt;
|
|
434
|
+
lastObservedState = true;
|
|
435
|
+
if (idleMs >= stability_ms) {
|
|
436
|
+
const now2 = Date.now();
|
|
437
|
+
return { success: true, condition: 'present', query: q, poll_count: pollCount, duration_ms: now2 - start, stable_duration_ms: idleMs, matchSource: 'idle-stable', timestamp: now2, type: 'idle', observed_state: lastObservedState ?? null };
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
catch (err) {
|
|
442
|
+
console.error('observeUntil(idle) error:', err);
|
|
443
|
+
}
|
|
410
444
|
}
|
|
411
445
|
// Respect poll interval and avoid tight loop
|
|
412
|
-
await sleep(
|
|
446
|
+
await sleep(pollInterval);
|
|
413
447
|
}
|
|
414
|
-
// On timeout, capture a failure snapshot to aid debugging (best-effort)
|
|
448
|
+
// On timeout, optionally capture a failure snapshot to aid debugging (best-effort)
|
|
415
449
|
let snapshot = null;
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
450
|
+
if (includeSnapshotOnFailure) {
|
|
451
|
+
try {
|
|
452
|
+
snapshot = await ToolsObserve.captureDebugSnapshotHandler({ reason: `observe_until timeout for ${type}`, includeLogs: true, platform, deviceId });
|
|
453
|
+
}
|
|
454
|
+
catch (err) {
|
|
455
|
+
snapshot = { error: err instanceof Error ? err.message : String(err) };
|
|
456
|
+
}
|
|
421
457
|
}
|
|
422
458
|
const elapsed = Date.now() - start;
|
|
423
|
-
return { success: false,
|
|
459
|
+
return { success: false, condition: match, query: q, poll_count: pollCount, duration_ms: elapsed, stable_duration_ms: stableDuration, error: 'Timeout waiting for condition', snapshot, observed_state: lastObservedState ?? null };
|
|
424
460
|
}
|
|
425
461
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ToolsObserve } from '../observe/index.js'
|
|
2
|
+
import { ToolsInteract } from '../interact/index.js'
|
|
3
|
+
|
|
4
|
+
async function main() {
|
|
5
|
+
const platform = process.argv.includes('--platform') ? process.argv[process.argv.indexOf('--platform')+1] : 'android'
|
|
6
|
+
const device = process.argv.includes('--device') ? process.argv[process.argv.indexOf('--device')+1] : undefined
|
|
7
|
+
|
|
8
|
+
console.log('Tap then capture UI tree snapshots')
|
|
9
|
+
try {
|
|
10
|
+
// Tap generate
|
|
11
|
+
const found = await (await import('../interact/index.js')).ToolsInteract.findElementHandler({ query: 'Generate Session', platform, timeoutMs: 3000, exact: true, deviceId: device })
|
|
12
|
+
console.log('Found generate?', !!(found && found.found), JSON.stringify(found && found.element || {}))
|
|
13
|
+
if (found && found.found && found.element && found.element.tapCoordinates) {
|
|
14
|
+
await (await import('../interact/index.js')).ToolsInteract.tapHandler({ platform, x: found.element.tapCoordinates.x, y: found.element.tapCoordinates.y, deviceId: device })
|
|
15
|
+
console.log('Tapped Generate at', found.element.tapCoordinates)
|
|
16
|
+
} else {
|
|
17
|
+
console.log('Could not find generate element — aborting capture')
|
|
18
|
+
process.exit(2)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Capture UI trees and screenshot repeatedly for 20s
|
|
22
|
+
const start = Date.now()
|
|
23
|
+
const outDir = '/tmp/mcp_ui_captures'
|
|
24
|
+
try { await import('fs').then(fs=>fs.promises.mkdir(outDir,{recursive:true})) } catch(e){}
|
|
25
|
+
let i = 0
|
|
26
|
+
while (Date.now() - start < 20000) {
|
|
27
|
+
const snap = await ToolsObserve.getUITreeHandler({ platform, deviceId: device })
|
|
28
|
+
const fname = `${outDir}/ui_${Date.now()}_${i}.json`
|
|
29
|
+
await import('fs').then(fs=>fs.promises.writeFile(fname, JSON.stringify(snap,null,2)))
|
|
30
|
+
console.log('Saved', fname)
|
|
31
|
+
i++
|
|
32
|
+
await new Promise(r=>setTimeout(r,500))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
console.log('Capture complete')
|
|
36
|
+
process.exit(0)
|
|
37
|
+
} catch (e) {
|
|
38
|
+
console.error('Error during capture:', e)
|
|
39
|
+
process.exit(1)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
main()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ToolsInteract } from '../interact/index.js'
|
|
2
|
+
|
|
3
|
+
async function main() {
|
|
4
|
+
const platform = process.argv.includes('--platform') ? process.argv[process.argv.indexOf('--platform')+1] : 'android'
|
|
5
|
+
const device = process.argv.includes('--device') ? process.argv[process.argv.indexOf('--device')+1] : undefined
|
|
6
|
+
const timeout = parseInt(process.argv.includes('--timeout') ? process.argv[process.argv.indexOf('--timeout')+1] : '120000')
|
|
7
|
+
console.log('Running observeUntil for Play session, platform=', platform, 'device=', device)
|
|
8
|
+
try {
|
|
9
|
+
const res = await ToolsInteract.observeUntilHandler({ type: 'ui', query: 'Play session', match: 'present', timeoutMs: timeout, pollIntervalMs: 300, stability_ms: 700, platform, deviceId: device })
|
|
10
|
+
console.log('observeUntil result:', JSON.stringify(res, null, 2))
|
|
11
|
+
process.exit(res && res.success ? 0 : 2)
|
|
12
|
+
} catch (e) {
|
|
13
|
+
console.error('Error running observeUntil:', e)
|
|
14
|
+
process.exit(1)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
main()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ToolsObserve } from '../observe/index.js'
|
|
2
|
+
|
|
3
|
+
async function main() {
|
|
4
|
+
const platform = process.argv.includes('--platform') ? process.argv[process.argv.indexOf('--platform')+1] : 'android'
|
|
5
|
+
const device = process.argv.includes('--device') ? process.argv[process.argv.indexOf('--device')+1] : undefined
|
|
6
|
+
const timeout = parseInt(process.argv.includes('--timeout') ? process.argv[process.argv.indexOf('--timeout')+1] : '120000')
|
|
7
|
+
const start = Date.now()
|
|
8
|
+
const out = []
|
|
9
|
+
console.log('Searching UI trees for elements containing "play" (case-insensitive)')
|
|
10
|
+
while (Date.now() - start < timeout) {
|
|
11
|
+
try {
|
|
12
|
+
const tree = await ToolsObserve.getUITreeHandler({ platform, deviceId: device })
|
|
13
|
+
const elements = Array.isArray((tree && tree.elements)) ? tree.elements : []
|
|
14
|
+
const matches = []
|
|
15
|
+
const norm = (s)=> s ? String(s).toLowerCase() : ''
|
|
16
|
+
for (const el of elements) {
|
|
17
|
+
const text = norm(el.text)
|
|
18
|
+
const content = norm(el.contentDescription || el.contentDesc || el.accessibilityLabel)
|
|
19
|
+
const rid = norm(el.resourceId || el.id)
|
|
20
|
+
const cls = norm(el.type || el.class)
|
|
21
|
+
if (text.includes('play') || content.includes('play') || rid.includes('play') || cls.includes('play') || text.includes('session') || content.includes('session')) {
|
|
22
|
+
matches.push({ el: el, foundIn: { text: text.includes('play')? 'text': text.includes('session')? 'text-session': null } })
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (matches.length) {
|
|
26
|
+
console.log('Matches found:', JSON.stringify(matches, null, 2))
|
|
27
|
+
out.push({ ts: Date.now(), matches })
|
|
28
|
+
break
|
|
29
|
+
}
|
|
30
|
+
} catch (e) { console.error('UITree fetch err:', e) }
|
|
31
|
+
await new Promise(r=>setTimeout(r, 500))
|
|
32
|
+
}
|
|
33
|
+
console.log('Search complete. Results count=', out.length)
|
|
34
|
+
if (out.length > 0) process.exit(0)
|
|
35
|
+
process.exit(2)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
main()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ToolsObserve } from '../observe/index.js'
|
|
2
|
+
|
|
3
|
+
async function main() {
|
|
4
|
+
const argv = process.argv.slice(2)
|
|
5
|
+
const opts = {}
|
|
6
|
+
for (let i = 0; i < argv.length; i++) {
|
|
7
|
+
if (argv[i] === '--platform') opts.platform = argv[++i]
|
|
8
|
+
else if (argv[i] === '--deviceId') opts.deviceId = argv[++i]
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
const tree = await ToolsObserve.getUITreeHandler({ platform: opts.platform || 'android', deviceId: opts.deviceId })
|
|
12
|
+
console.log(JSON.stringify(tree, null, 2))
|
|
13
|
+
process.exit(0)
|
|
14
|
+
} catch (e) {
|
|
15
|
+
console.error('failed to get ui tree:', e instanceof Error ? e.message : String(e))
|
|
16
|
+
process.exit(2)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
main()
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ToolsObserve } from '../observe/index.js'
|
|
2
|
+
import { ToolsInteract } from '../interact/index.js'
|
|
3
|
+
|
|
4
|
+
async function main() {
|
|
5
|
+
console.log('Starting log stream for com.ideamechanics.modul8')
|
|
6
|
+
try {
|
|
7
|
+
const start = await ToolsObserve.startLogStreamHandler({ platform: 'ios', packageName: 'com.ideamechanics.modul8', sessionId: 'test-session' })
|
|
8
|
+
console.log('startLogStream result:', JSON.stringify(start))
|
|
9
|
+
} catch (e) {
|
|
10
|
+
console.error('startLogStream failed:', e instanceof Error ? e.message : String(e))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
console.log('\nPlease press the Generate Session button in the app now. Observing for network idle (15s timeout)...')
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const res = await ToolsInteract.observeUntilHandler({ type: 'network_idle', timeoutMs: 15000, pollIntervalMs: 300, platform: 'ios' })
|
|
17
|
+
console.log('observeUntil result:')
|
|
18
|
+
console.log(JSON.stringify(res, null, 2))
|
|
19
|
+
} catch (e) {
|
|
20
|
+
console.error('observeUntil failed:', e instanceof Error ? e.message : String(e))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
console.log('\nStopping log stream (best-effort)')
|
|
24
|
+
try {
|
|
25
|
+
const stop = await ToolsObserve.stopLogStreamHandler({ platform: 'ios', sessionId: 'test-session' })
|
|
26
|
+
console.log('stopLogStream result:', JSON.stringify(stop))
|
|
27
|
+
} catch (e) { console.error('stopLogStream failed:', e) }
|
|
28
|
+
|
|
29
|
+
process.exit(0)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
main().catch(err => { console.error(err); process.exit(1) })
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { ToolsObserve } from '../observe/index.js'
|
|
2
|
+
import { ToolsInteract } from '../interact/index.js'
|
|
3
|
+
|
|
4
|
+
async function main() {
|
|
5
|
+
const bundle = 'com.ideamechanics.modul8'
|
|
6
|
+
const sessionId = 'press-test'
|
|
7
|
+
console.log('Starting log stream for', bundle)
|
|
8
|
+
try {
|
|
9
|
+
const start = await ToolsObserve.startLogStreamHandler({ platform: 'ios', packageName: bundle, sessionId })
|
|
10
|
+
console.log('startLogStream result:', JSON.stringify(start))
|
|
11
|
+
} catch (e) {
|
|
12
|
+
console.error('startLogStream failed:', e instanceof Error ? e.message : String(e))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Try to find the Generate Session element
|
|
16
|
+
console.log('\nSearching for "Generate Session" UI element...')
|
|
17
|
+
let el = null
|
|
18
|
+
try {
|
|
19
|
+
const found = await ToolsInteract.findElementHandler({ query: 'Generate Session', exact: false, timeoutMs: 3000, platform: 'ios' })
|
|
20
|
+
const foundFlag = found && typeof (found).found !== 'undefined' ? found.found : false
|
|
21
|
+
console.log('findElementHandler:', foundFlag ? 'found' : 'not found')
|
|
22
|
+
if (foundFlag) {
|
|
23
|
+
el = found.element
|
|
24
|
+
console.log('Matched element:', JSON.stringify(el))
|
|
25
|
+
}
|
|
26
|
+
} catch (e) {
|
|
27
|
+
console.error('findElementHandler error:', e instanceof Error ? e.message : String(e))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// If found, tap it. Otherwise, try tapping a best-effort coordinate in center of screen
|
|
31
|
+
try {
|
|
32
|
+
if (el && el.tapCoordinates) {
|
|
33
|
+
console.log('Tapping matched element at', JSON.stringify(el.tapCoordinates))
|
|
34
|
+
await ToolsInteract.tapHandler({ platform: 'ios', x: el.tapCoordinates.x, y: el.tapCoordinates.y })
|
|
35
|
+
} else {
|
|
36
|
+
console.log('Element not found; attempting center tap as fallback')
|
|
37
|
+
// attempt a center tap (may be harmless)
|
|
38
|
+
await ToolsInteract.tapHandler({ platform: 'ios', x: 200, y: 400 })
|
|
39
|
+
}
|
|
40
|
+
} catch (e) {
|
|
41
|
+
console.error('Tap failed:', e instanceof Error ? e.message : String(e))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
console.log('\nObserving until network_idle (30s timeout)')
|
|
45
|
+
// Start a parallel log monitor to print network-like lines as they appear
|
|
46
|
+
// Print all log lines originating from the app bundle for inspection
|
|
47
|
+
let seenIds = new Set()
|
|
48
|
+
let monitorActive = true
|
|
49
|
+
const monitor = (async () => {
|
|
50
|
+
while (monitorActive) {
|
|
51
|
+
try {
|
|
52
|
+
const stream = await ToolsObserve.readLogStreamHandler({ platform: 'ios', sessionId, limit: 200 })
|
|
53
|
+
const entries = (stream && Array.isArray(stream.entries)) ? stream.entries : []
|
|
54
|
+
for (const ent of entries) {
|
|
55
|
+
const id = JSON.stringify(ent).slice(0,200)
|
|
56
|
+
if (seenIds.has(id)) continue
|
|
57
|
+
seenIds.add(id)
|
|
58
|
+
const msg = ent && (ent.message || ent.msg || ent) ? (ent.message || ent.msg || ent) : ''
|
|
59
|
+
// Print only lines that reference the app bundle
|
|
60
|
+
if (String(msg).includes('Modul8') || String(msg).includes('modul8') || (ent && ent.process && String(ent.process).toLowerCase().includes('modul8'))) {
|
|
61
|
+
console.log('[APP LOG]', new Date().toISOString(), JSON.stringify({ message: msg }))
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} catch (e) { console.error('log monitor error:', e instanceof Error ? e.message : String(e)) }
|
|
65
|
+
await new Promise(r => setTimeout(r, 300))
|
|
66
|
+
}
|
|
67
|
+
})()
|
|
68
|
+
|
|
69
|
+
let observeResult = null
|
|
70
|
+
try {
|
|
71
|
+
observeResult = await ToolsInteract.observeUntilHandler({ type: 'network_idle', timeoutMs: 30000, pollIntervalMs: 300, platform: 'ios', includeSnapshotOnFailure: true })
|
|
72
|
+
console.log('observeUntil result:')
|
|
73
|
+
console.log(JSON.stringify(observeResult, null, 2))
|
|
74
|
+
} catch (e) {
|
|
75
|
+
console.error('observeUntil failed:', e instanceof Error ? e.message : String(e))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Stop monitor
|
|
79
|
+
monitorActive = false
|
|
80
|
+
|
|
81
|
+
console.log('\nStopping log stream (best-effort)')
|
|
82
|
+
try {
|
|
83
|
+
const stop = await ToolsObserve.stopLogStreamHandler({ platform: 'ios', sessionId })
|
|
84
|
+
console.log('stopLogStream result:', JSON.stringify(stop))
|
|
85
|
+
} catch (e) { console.error('stopLogStream failed:', e) }
|
|
86
|
+
|
|
87
|
+
process.exit(0)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
main().catch(err => { console.error(err); process.exit(1) })
|