test_report
Generate automatic test reports for mobile app testing. Start report collection automatically on first interaction and produce a markdown report when testing ends.
Instructions
Gère le rapport de test automatique. Le rapport démarre AUTOMATIQUEMENT dès la première interaction (tap, type_text, etc.) — tu n'as PAS besoin d'appeler 'start'. Appelle UNIQUEMENT action='end' quand tu as fini de tester pour générer le rapport markdown. Si tu veux nommer le rapport, appelle action='start' avec un nom AVANT de commencer.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| action | Yes | 'start' pour commencer le suivi automatique, 'end' pour générer le rapport | |
| name | No | Nom du test (requis pour 'start') |
Implementation Reference
- src/tools/test-report.ts:6-45 (handler)The registerTestReport function registers the 'test_report' MCP tool. The handler supports two actions: 'start' (begins a named test report session) and 'end' (generates and returns the markdown report). It uses resolveDevice, startAutoReport, and endAutoReport from the auto-report utility.
export function registerTestReport(server: McpServer): void { server.tool( "test_report", "Gère le rapport de test automatique. Le rapport démarre AUTOMATIQUEMENT dès la première interaction (tap, type_text, etc.) — tu n'as PAS besoin d'appeler 'start'. Appelle UNIQUEMENT action='end' quand tu as fini de tester pour générer le rapport markdown. Si tu veux nommer le rapport, appelle action='start' avec un nom AVANT de commencer.", { action: z.enum(["start", "end"]).describe("'start' pour commencer le suivi automatique, 'end' pour générer le rapport"), name: z.string().optional().describe("Nom du test (requis pour 'start')"), }, async ({ action, name }) => { if (action === "start") { if (isAutoReportActive()) { return { content: [{ type: "text", text: "Rapport déjà en cours. Termine-le avec action='end' d'abord." }], isError: true }; } if (!name) { return { content: [{ type: "text", text: "Le paramètre 'name' est requis pour démarrer un rapport." }], isError: true }; } const result = await resolveDevice(); if ("error" in result) return { content: [{ type: "text", text: result.error }], isError: true }; const dev = result.device; const reportDir = startAutoReport(name, dev.name, dev.platform, dev.id); return { content: [{ type: "text", text: `Rapport "${name}" démarré sur ${dev.name} (${dev.platform}).\n\nToutes les actions suivantes seront enregistrées automatiquement avec un screenshot à chaque étape.\n\nQuand tu as terminé, appelle test_report(action='end') pour générer le rapport.\n\nDossier : ${reportDir}` }], }; } // --- END --- const report = await endAutoReport(); if (!report) { return { content: [{ type: "text", text: "Aucun rapport en cours. Lance action='start' d'abord." }], isError: true }; } return { content: [{ type: "text", text: report.markdown }], }; } ); } - src/index.ts:68-68 (registration)Registration call: registerTestReport(server) adds the 'test_report' tool to the MCP server.
registerTestReport(server); - src/tools/test-report.ts:10-13 (schema)Zod schema for 'test_report' tool: action is a required enum ('start'|'end'), name is an optional string (required for 'start').
{ action: z.enum(["start", "end"]).describe("'start' pour commencer le suivi automatique, 'end' pour générer le rapport"), name: z.string().optional().describe("Nom du test (requis pour 'start')"), }, - src/utils/auto-report.ts:1-161 (helper)Auto-report utility managing ReportSession lifecycle: startAutoReport, autoStartIfNeeded, autoLogStep, endAutoReport, getAutoReportSession. endAutoReport generates the markdown report with step-by-step screenshots and pass/fail summary.
import { mkdir, writeFile } from "fs/promises"; import { takeScreenshot } from "./screenshot.js"; interface ReportStep { index: number; tool: string; description: string; status: "pass" | "fail"; screenshotPath: string; timestamp: number; } interface ReportSession { name: string; startedAt: number; deviceName: string; platform: "ios" | "android"; deviceId: string; steps: ReportStep[]; reportDir: string; } const MAX_AUTO_STEPS = 50; let session: ReportSession | null = null; export function isAutoReportActive(): boolean { return session !== null; } export function startAutoReport(name: string, deviceName: string, platform: "ios" | "android", deviceId: string): string { const timestamp = Date.now(); const reportDir = `/tmp/phantom-report-${timestamp}`; session = { name, startedAt: timestamp, deviceName, platform, deviceId, steps: [], reportDir }; return reportDir; } /** * Auto-start a report if none is active. Called by logAction on first interaction. * This makes the report 100% automatic — no need to call test_report(start). */ export function autoStartIfNeeded(deviceName: string, platform: "ios" | "android", deviceId: string): void { if (session) return; // Already active const timestamp = Date.now(); const reportDir = `/tmp/phantom-report-${timestamp}`; session = { name: "Test automatique", startedAt: timestamp, deviceName, platform, deviceId, steps: [], reportDir, }; console.error(`[phantom] Rapport de test démarré automatiquement → ${reportDir}`); } /** * Auto-log a step from any tool. Fire-and-forget — never throws, never blocks the caller on failure. * Auto-starts a report if none is active. */ // Screenshot every N steps to avoid bloating disk + context const SCREENSHOT_INTERVAL = 3; // Tools that are important enough to always get a screenshot const ALWAYS_SCREENSHOT_TOOLS = new Set(["assert_visible", "assert_not_visible", "accessibility_audit", "launch_app"]); export async function autoLogStep(tool: string, description: string, isError: boolean, platform: "ios" | "android", deviceId: string): Promise<void> { if (session && session.steps.length >= MAX_AUTO_STEPS) return; if (!session) return; const stepIndex = session.steps.length + 1; try { await mkdir(session.reportDir, { recursive: true }); let screenshotPath = ""; // Take screenshot only on errors, important tools, or every N steps const shouldScreenshot = isError || ALWAYS_SCREENSHOT_TOOLS.has(tool) || stepIndex % SCREENSHOT_INTERVAL === 0 || stepIndex === 1; // Always screenshot first step if (shouldScreenshot) { screenshotPath = `${session.reportDir}/step-${stepIndex}.png`; try { const buffer = await takeScreenshot(platform, deviceId); await writeFile(screenshotPath, buffer); } catch (err) { console.error(`[phantom] auto-report: screenshot failed for step ${stepIndex}: ${err instanceof Error ? err.message : err}`); screenshotPath = ""; } } session.steps.push({ index: stepIndex, tool, description, status: isError ? "fail" : "pass", screenshotPath, timestamp: Date.now(), }); } catch (err) { console.error(`[phantom] auto-report: failed to log step: ${err instanceof Error ? err.message : err}`); } } export async function endAutoReport(): Promise<{ reportPath: string; markdown: string } | null> { if (!session) return null; const s = session; const duration = ((Date.now() - s.startedAt) / 1000).toFixed(1); const passed = s.steps.filter((st) => st.status === "pass").length; const failed = s.steps.filter((st) => st.status === "fail").length; const overall = failed === 0 ? "PASS" : "FAIL"; const md: string[] = [ `# Test Report: ${s.name}`, "", `| | |`, `|---|---|`, `| **Device** | ${s.deviceName} (${s.platform}) |`, `| **Date** | ${new Date(s.startedAt).toISOString()} |`, `| **Durée** | ${duration}s |`, `| **Résultat** | ${overall} |`, "", `## Résumé`, "", `- Total : ${s.steps.length} étape(s)`, `- Pass : ${passed}`, `- Fail : ${failed}`, "", `## Étapes`, "", ]; for (const step of s.steps) { const icon = step.status === "pass" ? "PASS" : "FAIL"; md.push(`### Étape ${step.index} — [${icon}] ${step.tool}: ${step.description}`); md.push(""); if (step.screenshotPath) { md.push(``); md.push(""); } } md.push("---"); md.push("*Généré automatiquement par Phantom MCP*"); const markdown = md.join("\n"); await mkdir(s.reportDir, { recursive: true }); const reportPath = `${s.reportDir}/report.md`; await writeFile(reportPath, markdown); session = null; return { reportPath, markdown: `${overall} — ${passed} pass, ${failed} fail en ${duration}s\n\nRapport : ${reportPath}\nOuvre le dossier : open "${s.reportDir}"` }; } export function getAutoReportSession(): ReportSession | null { return session; } - src/utils/tool-wrapper.ts:1-26 (helper)logAction helper auto-starts a report on first tool interaction and logs each step. getReportSuffix appends a reminder to tool responses to call test_report(end) when done.
import { autoLogStep, isAutoReportActive, autoStartIfNeeded, getAutoReportSession } from "./auto-report.js"; /** * Auto-log a tool action to the active test report. * Auto-starts a report on the first interaction tool call. * Fire-and-forget — never blocks, never throws. */ export function logAction(tool: string, description: string, isError: boolean, platform: "ios" | "android", deviceId: string, deviceName?: string): void { if (!isAutoReportActive()) { autoStartIfNeeded(deviceName ?? deviceId, platform, deviceId); } autoLogStep(tool, description, isError, platform, deviceId).catch((err) => { console.error(`[phantom] auto-report log failed: ${err instanceof Error ? err.message : err}`); }); } /** * Returns a suffix to append to tool responses when a report is active. * This reminds Claude to call test_report(end) when testing is done. */ export function getReportSuffix(): string { const session = getAutoReportSession(); if (!session) return ""; return `\n\n📋 Rapport "${session.name}" en cours (${session.steps.length} étape(s)). Appelle test_report(end) quand tu as fini de tester.`; }