mcp-maestro-mobile-ai 1.1.0 → 1.3.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.
@@ -14,6 +14,13 @@ import {
14
14
  clearAppContext,
15
15
  listAppContexts,
16
16
  } from "../utils/appContext.js";
17
+ import {
18
+ YAML_GENERATION_INSTRUCTIONS,
19
+ getYamlGenerationContext,
20
+ TEST_PATTERNS,
21
+ validateYamlStructure,
22
+ getScreenAnalysisInstructions,
23
+ } from "../utils/yamlTemplate.js";
17
24
 
18
25
  /**
19
26
  * Format result as MCP response
@@ -180,6 +187,118 @@ export async function listContexts() {
180
187
  return formatResponse(result);
181
188
  }
182
189
 
190
+ /**
191
+ * Get YAML generation instructions
192
+ * AI MUST call this before generating any Maestro YAML
193
+ */
194
+ export async function getYamlInstructions(appId) {
195
+ if (!appId) {
196
+ // Return generic instructions without app context
197
+ return formatResponse({
198
+ success: true,
199
+ instructions: YAML_GENERATION_INSTRUCTIONS,
200
+ patterns: TEST_PATTERNS,
201
+ message: "Use these instructions when generating Maestro YAML. Always follow the tapOn → inputText pattern for text input.",
202
+ });
203
+ }
204
+
205
+ // Get app-specific context
206
+ const appContext = await loadAppContext(appId);
207
+ const instructions = getYamlGenerationContext(appId, appContext);
208
+
209
+ return formatResponse({
210
+ success: true,
211
+ appId,
212
+ instructions,
213
+ patterns: TEST_PATTERNS,
214
+ hasAppContext: Object.keys(appContext.elements || {}).length > 0,
215
+ message: "IMPORTANT: Follow these instructions EXACTLY when generating YAML. Always use tapOn before inputText.",
216
+ });
217
+ }
218
+
219
+ /**
220
+ * Validate YAML before running
221
+ * Checks for common issues like missing tapOn before inputText
222
+ */
223
+ export async function validateYamlBeforeRun(yamlContent) {
224
+ if (!yamlContent) {
225
+ return formatResponse({
226
+ success: false,
227
+ error: "yamlContent is required",
228
+ });
229
+ }
230
+
231
+ const validation = validateYamlStructure(yamlContent);
232
+
233
+ if (!validation.valid) {
234
+ return formatResponse({
235
+ success: false,
236
+ valid: false,
237
+ issues: validation.issues,
238
+ message: "YAML has structural issues that may cause test failures. Please fix before running.",
239
+ fixedYaml: null, // Could auto-fix in future
240
+ });
241
+ }
242
+
243
+ return formatResponse({
244
+ success: true,
245
+ valid: true,
246
+ message: "YAML structure is valid.",
247
+ });
248
+ }
249
+
250
+ /**
251
+ * Get a test pattern template
252
+ */
253
+ export async function getTestPattern(patternName) {
254
+ const patterns = {
255
+ login: TEST_PATTERNS.login,
256
+ search: TEST_PATTERNS.search,
257
+ navigation: TEST_PATTERNS.navigation,
258
+ form: TEST_PATTERNS.form,
259
+ list: TEST_PATTERNS.list,
260
+ settings: TEST_PATTERNS.settings,
261
+ logout: TEST_PATTERNS.logout,
262
+ };
263
+
264
+ const pattern = patterns[patternName?.toLowerCase()];
265
+
266
+ if (!pattern) {
267
+ return formatResponse({
268
+ success: false,
269
+ error: `Unknown pattern: ${patternName}`,
270
+ availablePatterns: Object.keys(patterns),
271
+ hint: "Use: login, form, search, navigation, list, settings, or logout",
272
+ });
273
+ }
274
+
275
+ return formatResponse({
276
+ success: true,
277
+ pattern: patternName,
278
+ template: pattern,
279
+ message: "Replace placeholders in {} with actual values. REMEMBER: Always use tapOn before inputText!",
280
+ });
281
+ }
282
+
283
+ /**
284
+ * Get screen analysis instructions
285
+ * Helps AI understand how to gather UI element information
286
+ */
287
+ export async function getScreenAnalysis() {
288
+ const instructions = getScreenAnalysisInstructions();
289
+
290
+ return formatResponse({
291
+ success: true,
292
+ instructions,
293
+ message: "Use these instructions to gather UI element information before generating YAML.",
294
+ requiredInfo: [
295
+ "Field labels/placeholders for text inputs",
296
+ "Button text for actions",
297
+ "Success/error indicators to verify results",
298
+ ],
299
+ });
300
+ }
301
+
183
302
  export default {
184
303
  registerAppElements,
185
304
  registerAppScreen,
@@ -190,5 +309,9 @@ export default {
190
309
  getAppContext,
191
310
  clearContext,
192
311
  listContexts,
312
+ getYamlInstructions,
313
+ validateYamlBeforeRun,
314
+ getTestPattern,
315
+ getScreenAnalysis,
193
316
  };
194
317
 
@@ -4,12 +4,13 @@
4
4
  */
5
5
 
6
6
  import fs from 'fs/promises';
7
- import path from 'path';
8
7
  import { fileURLToPath } from 'url';
9
8
  import { dirname, join } from 'path';
10
9
  import { logger } from '../utils/logger.js';
11
10
  import { runMaestroFlow, checkDeviceConnection, checkAppInstalled, getConfig } from '../utils/maestro.js';
12
11
  import { validateMaestroYaml } from './validateTools.js';
12
+ import { validateYamlStructure } from '../utils/yamlTemplate.js';
13
+ import { generateReport, getReportsDir, listReports } from '../utils/reportGenerator.js';
13
14
 
14
15
  const __filename = fileURLToPath(import.meta.url);
15
16
  const __dirname = dirname(__filename);
@@ -47,7 +48,49 @@ export async function runTest(yamlContent, testName, options = {}) {
47
48
  };
48
49
  }
49
50
 
50
- // Validate YAML first
51
+ // STEP 1: Validate YAML structure (catches common AI generation errors)
52
+ const structureValidation = validateYamlStructure(yamlContent);
53
+
54
+ if (!structureValidation.valid) {
55
+ logger.warn(`YAML structure validation failed for: ${testName}`, { errors: structureValidation.errors });
56
+
57
+ return {
58
+ content: [
59
+ {
60
+ type: 'text',
61
+ text: JSON.stringify({
62
+ success: false,
63
+ name: testName,
64
+ error: 'YAML STRUCTURE ERROR - Please fix before running',
65
+ structureErrors: structureValidation.errors,
66
+ warnings: structureValidation.warnings,
67
+ summary: structureValidation.summary,
68
+ instructions: `
69
+ ⚠️ YOUR YAML HAS CRITICAL ISSUES. Please follow these rules:
70
+
71
+ 1. ALWAYS use tapOn BEFORE inputText:
72
+ CORRECT:
73
+ - tapOn: "Username"
74
+ - inputText: "value"
75
+
76
+ WRONG (text goes to wrong field!):
77
+ - inputText: "value"
78
+
79
+ 2. ALWAYS start with:
80
+ - clearState
81
+ - launchApp
82
+
83
+ 3. ALWAYS include appId at the top.
84
+
85
+ Please regenerate the YAML following these rules.
86
+ `,
87
+ }),
88
+ },
89
+ ],
90
+ };
91
+ }
92
+
93
+ // STEP 2: Validate YAML syntax
51
94
  const validation = await validateMaestroYaml(yamlContent);
52
95
  const validationResult = JSON.parse(validation.content[0].text);
53
96
 
@@ -59,9 +102,9 @@ export async function runTest(yamlContent, testName, options = {}) {
59
102
  text: JSON.stringify({
60
103
  success: false,
61
104
  name: testName,
62
- error: 'YAML validation failed',
105
+ error: 'YAML syntax validation failed',
63
106
  validationErrors: validationResult.errors,
64
- hint: 'Fix the YAML errors and try again. Common issues: missing appId, invalid syntax.',
107
+ hint: 'Fix the YAML syntax errors and try again.',
65
108
  }),
66
109
  },
67
110
  ],
@@ -351,7 +394,187 @@ async function autoCleanupResults() {
351
394
  }
352
395
  }
353
396
 
397
+ /**
398
+ * Generate a report from test results
399
+ */
400
+ export async function generateTestReport(results, metadata = {}) {
401
+ try {
402
+ const reportResult = await generateReport(results, metadata);
403
+
404
+ return {
405
+ content: [
406
+ {
407
+ type: 'text',
408
+ text: JSON.stringify({
409
+ success: true,
410
+ reportId: reportResult.reportId,
411
+ htmlPath: reportResult.htmlPath,
412
+ jsonPath: reportResult.jsonPath,
413
+ summary: reportResult.summary,
414
+ message: `Report generated successfully! Open the HTML file to view: ${reportResult.htmlPath}`,
415
+ reportsDir: getReportsDir(),
416
+ }),
417
+ },
418
+ ],
419
+ };
420
+ } catch (error) {
421
+ logger.error('Report generation error', { error: error.message });
422
+ return {
423
+ content: [
424
+ {
425
+ type: 'text',
426
+ text: JSON.stringify({
427
+ success: false,
428
+ error: error.message,
429
+ }),
430
+ },
431
+ ],
432
+ };
433
+ }
434
+ }
435
+
436
+ /**
437
+ * List all generated reports
438
+ */
439
+ export async function listTestReports() {
440
+ try {
441
+ const reports = await listReports();
442
+
443
+ return {
444
+ content: [
445
+ {
446
+ type: 'text',
447
+ text: JSON.stringify({
448
+ success: true,
449
+ reportsDir: getReportsDir(),
450
+ reports,
451
+ count: reports.length,
452
+ }),
453
+ },
454
+ ],
455
+ };
456
+ } catch (error) {
457
+ logger.error('List reports error', { error: error.message });
458
+ return {
459
+ content: [
460
+ {
461
+ type: 'text',
462
+ text: JSON.stringify({
463
+ success: false,
464
+ error: error.message,
465
+ }),
466
+ },
467
+ ],
468
+ };
469
+ }
470
+ }
471
+
472
+ /**
473
+ * Run test suite and generate report
474
+ */
475
+ export async function runTestSuiteWithReport(tests, options = {}) {
476
+ try {
477
+ const results = [];
478
+ const startTime = Date.now();
479
+
480
+ for (let i = 0; i < tests.length; i++) {
481
+ const test = tests[i];
482
+ logger.info(`Running test ${i + 1}/${tests.length}: ${test.name}`);
483
+
484
+ // Validate structure first
485
+ const structureValidation = validateYamlStructure(test.yaml);
486
+ if (!structureValidation.valid) {
487
+ results.push({
488
+ name: test.name,
489
+ success: false,
490
+ error: 'YAML structure error: ' + structureValidation.errors.map(e => e.issue).join(', '),
491
+ structureErrors: structureValidation.errors,
492
+ });
493
+ continue;
494
+ }
495
+
496
+ // Validate syntax
497
+ const syntaxValidation = await validateMaestroYaml(test.yaml);
498
+ const syntaxResult = JSON.parse(syntaxValidation.content[0].text);
499
+ if (!syntaxResult.valid) {
500
+ results.push({
501
+ name: test.name,
502
+ success: false,
503
+ error: 'YAML syntax error',
504
+ syntaxErrors: syntaxResult.errors,
505
+ });
506
+ continue;
507
+ }
508
+
509
+ // Run the test
510
+ const testResult = await runMaestroFlow(test.yaml, test.name, {
511
+ retries: options.retries,
512
+ });
513
+
514
+ results.push(testResult);
515
+ }
516
+
517
+ const totalDuration = ((Date.now() - startTime) / 1000).toFixed(2);
518
+
519
+ // Generate report
520
+ const reportResult = await generateReport(results, {
521
+ promptFile: options.promptFile || 'Manual Test Suite',
522
+ appId: options.appId || process.env.APP_ID || 'unknown',
523
+ totalDuration,
524
+ });
525
+
526
+ const passed = results.filter(r => r.success).length;
527
+ const failed = results.length - passed;
528
+
529
+ return {
530
+ content: [
531
+ {
532
+ type: 'text',
533
+ text: JSON.stringify({
534
+ success: failed === 0,
535
+ totalDuration: `${totalDuration}s`,
536
+ summary: {
537
+ total: results.length,
538
+ passed,
539
+ failed,
540
+ passRate: reportResult.summary.passRate,
541
+ },
542
+ report: {
543
+ reportId: reportResult.reportId,
544
+ htmlPath: reportResult.htmlPath,
545
+ jsonPath: reportResult.jsonPath,
546
+ message: `📊 Report generated: ${reportResult.htmlPath}`,
547
+ },
548
+ tests: results.map(r => ({
549
+ name: r.name,
550
+ success: r.success,
551
+ duration: r.duration,
552
+ error: r.error || null,
553
+ })),
554
+ }),
555
+ },
556
+ ],
557
+ };
558
+ } catch (error) {
559
+ logger.error('Test suite with report error', { error: error.message });
560
+ return {
561
+ content: [
562
+ {
563
+ type: 'text',
564
+ text: JSON.stringify({
565
+ success: false,
566
+ error: error.message,
567
+ }),
568
+ },
569
+ ],
570
+ };
571
+ }
572
+ }
573
+
354
574
  export default {
355
575
  runTest,
356
576
  runTestSuite,
577
+ generateTestReport,
578
+ listTestReports,
579
+ runTestSuiteWithReport,
357
580
  };