nodebench-mcp 2.20.2 → 2.21.1
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/__tests__/tools.test.js +4 -2
- package/dist/__tests__/tools.test.js.map +1 -1
- package/dist/db.js +109 -0
- package/dist/db.js.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/tools/progressiveDiscoveryTools.js +2 -2
- package/dist/tools/progressiveDiscoveryTools.js.map +1 -1
- package/dist/tools/toolRegistry.js +101 -0
- package/dist/tools/toolRegistry.js.map +1 -1
- package/dist/tools/uiUxDiveAdvancedTools.d.ts +20 -0
- package/dist/tools/uiUxDiveAdvancedTools.js +883 -0
- package/dist/tools/uiUxDiveAdvancedTools.js.map +1 -0
- package/dist/toolsetRegistry.js +2 -0
- package/dist/toolsetRegistry.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,883 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI/UX Full Dive v2 — Advanced Tools
|
|
3
|
+
*
|
|
4
|
+
* Deep interaction testing, screenshot capture, design auditing,
|
|
5
|
+
* backend context linking, changelog tracking, and walkthrough generation.
|
|
6
|
+
*
|
|
7
|
+
* These tools complement the base dive tools (uiUxDiveTools.ts) and work
|
|
8
|
+
* with the MCP Bridge (playwright-mcp) for browser automation.
|
|
9
|
+
*
|
|
10
|
+
* Architecture:
|
|
11
|
+
* - Agent uses MCP Bridge to drive the browser (navigate, click, type, screenshot)
|
|
12
|
+
* - These tools provide structured storage and analysis on top of bridge actions
|
|
13
|
+
* - Screenshots are saved to disk + thumbnail stored in DB for fast retrieval
|
|
14
|
+
* - Interaction tests define preconditions → steps → expected/actual per step
|
|
15
|
+
* - Design audits compare computed styles across components for inconsistencies
|
|
16
|
+
* - Backend links connect UI components to API endpoints, Convex functions, DB tables
|
|
17
|
+
* - Changelogs track before/after with screenshots when fixes are applied
|
|
18
|
+
*/
|
|
19
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync, readdirSync } from "node:fs";
|
|
20
|
+
import { join, basename } from "node:path";
|
|
21
|
+
import { homedir } from "node:os";
|
|
22
|
+
import { createConnection } from "node:net";
|
|
23
|
+
import { getDb } from "../db.js";
|
|
24
|
+
function genId(prefix) {
|
|
25
|
+
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
|
|
26
|
+
}
|
|
27
|
+
function screenshotDir() {
|
|
28
|
+
const dir = join(homedir(), ".nodebench", "dive-screenshots");
|
|
29
|
+
mkdirSync(dir, { recursive: true });
|
|
30
|
+
return dir;
|
|
31
|
+
}
|
|
32
|
+
/** Try to connect to a TCP port. Resolves true if something is listening. */
|
|
33
|
+
function checkPort(port, host = "127.0.0.1", timeoutMs = 800) {
|
|
34
|
+
return new Promise((resolve) => {
|
|
35
|
+
const sock = createConnection({ port, host });
|
|
36
|
+
const timer = setTimeout(() => { sock.destroy(); resolve(false); }, timeoutMs);
|
|
37
|
+
sock.on("connect", () => { clearTimeout(timer); sock.destroy(); resolve(true); });
|
|
38
|
+
sock.on("error", () => { clearTimeout(timer); resolve(false); });
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/** Recursively find files matching a test, up to maxDepth. */
|
|
42
|
+
function findFiles(dir, test, maxDepth = 4, depth = 0) {
|
|
43
|
+
if (depth > maxDepth || !existsSync(dir))
|
|
44
|
+
return [];
|
|
45
|
+
const results = [];
|
|
46
|
+
try {
|
|
47
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
48
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist" || entry.name === ".next")
|
|
49
|
+
continue;
|
|
50
|
+
const full = join(dir, entry.name);
|
|
51
|
+
if (entry.isFile() && test(entry.name))
|
|
52
|
+
results.push(full);
|
|
53
|
+
else if (entry.isDirectory())
|
|
54
|
+
results.push(...findFiles(full, test, maxDepth, depth + 1));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch { /* permission errors etc */ }
|
|
58
|
+
return results;
|
|
59
|
+
}
|
|
60
|
+
/** Extract route paths from source code using common patterns. */
|
|
61
|
+
function extractRoutes(srcDir) {
|
|
62
|
+
const routes = [];
|
|
63
|
+
const seen = new Set();
|
|
64
|
+
// Find files that likely contain route definitions
|
|
65
|
+
const routeFiles = findFiles(srcDir, (name) => /\.(tsx?|jsx?)$/.test(name) && (/[Rr]out/.test(name) || /[Aa]pp/.test(name) || /[Ll]ayout/.test(name) ||
|
|
66
|
+
/[Nn]avigation/.test(name) || /[Ss]idebar/.test(name) || /pages/.test(name)));
|
|
67
|
+
for (const file of routeFiles.slice(0, 30)) {
|
|
68
|
+
try {
|
|
69
|
+
const content = readFileSync(file, "utf-8");
|
|
70
|
+
// Match React Router <Route path="..." patterns
|
|
71
|
+
const routeMatches = content.matchAll(/path\s*[:=]\s*["'`](\/[^"'`]*?)["'`]/g);
|
|
72
|
+
for (const m of routeMatches) {
|
|
73
|
+
const p = m[1];
|
|
74
|
+
if (!seen.has(p)) {
|
|
75
|
+
seen.add(p);
|
|
76
|
+
// Try to find component name nearby
|
|
77
|
+
const compMatch = content.slice(Math.max(0, m.index - 200), m.index + 200)
|
|
78
|
+
.match(/(?:element|component)\s*[:=]\s*[{<]?\s*(\w+)/);
|
|
79
|
+
routes.push({ path: p, file: file.replace(/\\/g, "/"), component: compMatch?.[1] });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch { /* unreadable */ }
|
|
84
|
+
}
|
|
85
|
+
return routes.sort((a, b) => a.path.localeCompare(b.path));
|
|
86
|
+
}
|
|
87
|
+
export const uiUxDiveAdvancedTools = [
|
|
88
|
+
// ── 0. Project preflight — analyze project before diving ──────────────
|
|
89
|
+
{
|
|
90
|
+
name: "dive_preflight",
|
|
91
|
+
description: "Analyze a project BEFORE starting a UI dive. Scans the project directory to detect: framework (Vite, Next.js, CRA, etc.), dev scripts, required services (frontend, backend like Convex/Supabase/Firebase), port assignments, whether services are already running, route definitions from source code, and environment requirements. Returns a structured launch plan the agent should follow to get the app running before navigating. This is always Step 0 of a dive.",
|
|
92
|
+
inputSchema: {
|
|
93
|
+
type: "object",
|
|
94
|
+
properties: {
|
|
95
|
+
projectPath: { type: "string", description: "Absolute path to the project root directory" },
|
|
96
|
+
checkPorts: {
|
|
97
|
+
type: "boolean",
|
|
98
|
+
description: "Whether to probe common ports to see what is already running (default: true)",
|
|
99
|
+
},
|
|
100
|
+
scanRoutes: {
|
|
101
|
+
type: "boolean",
|
|
102
|
+
description: "Whether to scan source code for route definitions (default: true)",
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
required: ["projectPath"],
|
|
106
|
+
},
|
|
107
|
+
handler: async (args) => {
|
|
108
|
+
const { projectPath, checkPorts: doCheckPorts, scanRoutes: doScanRoutes } = args;
|
|
109
|
+
if (!existsSync(projectPath)) {
|
|
110
|
+
return { error: true, message: `Project path not found: ${projectPath}` };
|
|
111
|
+
}
|
|
112
|
+
// ── 1. Read package.json ──
|
|
113
|
+
const pkgPath = join(projectPath, "package.json");
|
|
114
|
+
let pkg = null;
|
|
115
|
+
if (existsSync(pkgPath)) {
|
|
116
|
+
try {
|
|
117
|
+
pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
118
|
+
}
|
|
119
|
+
catch { /* */ }
|
|
120
|
+
}
|
|
121
|
+
// ── 2. Detect framework ──
|
|
122
|
+
const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
|
|
123
|
+
const framework = { name: "unknown" };
|
|
124
|
+
const frameworkChecks = [
|
|
125
|
+
{ name: "next", dep: "next", configs: ["next.config.js", "next.config.ts", "next.config.mjs"] },
|
|
126
|
+
{ name: "vite", dep: "vite", configs: ["vite.config.ts", "vite.config.js", "vite.config.mjs"] },
|
|
127
|
+
{ name: "create-react-app", dep: "react-scripts", configs: [] },
|
|
128
|
+
{ name: "remix", dep: "@remix-run/react", configs: ["remix.config.js"] },
|
|
129
|
+
{ name: "nuxt", dep: "nuxt", configs: ["nuxt.config.ts", "nuxt.config.js"] },
|
|
130
|
+
{ name: "sveltekit", dep: "@sveltejs/kit", configs: ["svelte.config.js"] },
|
|
131
|
+
{ name: "astro", dep: "astro", configs: ["astro.config.mjs", "astro.config.ts"] },
|
|
132
|
+
{ name: "angular", dep: "@angular/core", configs: ["angular.json"] },
|
|
133
|
+
{ name: "gatsby", dep: "gatsby", configs: ["gatsby-config.js", "gatsby-config.ts"] },
|
|
134
|
+
];
|
|
135
|
+
for (const check of frameworkChecks) {
|
|
136
|
+
if (deps?.[check.dep]) {
|
|
137
|
+
framework.name = check.name;
|
|
138
|
+
framework.version = deps[check.dep];
|
|
139
|
+
for (const cfg of check.configs) {
|
|
140
|
+
if (existsSync(join(projectPath, cfg))) {
|
|
141
|
+
framework.configFile = cfg;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// ── 3. Detect dev scripts ──
|
|
149
|
+
const scripts = pkg?.scripts ?? {};
|
|
150
|
+
const devScripts = [];
|
|
151
|
+
const scriptPriority = ["dev", "dev:frontend", "start", "dev:web", "serve", "develop"];
|
|
152
|
+
for (const name of Object.keys(scripts)) {
|
|
153
|
+
let likely = "unknown";
|
|
154
|
+
const cmd = scripts[name];
|
|
155
|
+
if (/vite|next dev|react-scripts start|nuxt dev|astro dev/.test(cmd))
|
|
156
|
+
likely = "frontend";
|
|
157
|
+
else if (/convex dev|convex deploy/.test(cmd))
|
|
158
|
+
likely = "backend (convex)";
|
|
159
|
+
else if (/node.*server|express|fastify|hono/.test(cmd))
|
|
160
|
+
likely = "backend (api)";
|
|
161
|
+
else if (/tsc|typescript/.test(cmd))
|
|
162
|
+
likely = "build";
|
|
163
|
+
else if (/vitest|jest|playwright|cypress/.test(cmd))
|
|
164
|
+
likely = "test";
|
|
165
|
+
else if (/lint|eslint|prettier/.test(cmd))
|
|
166
|
+
likely = "lint";
|
|
167
|
+
if (likely === "frontend" || likely.startsWith("backend") || scriptPriority.includes(name)) {
|
|
168
|
+
devScripts.push({ name, command: cmd, likely });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// Sort by priority
|
|
172
|
+
devScripts.sort((a, b) => {
|
|
173
|
+
const ai = scriptPriority.indexOf(a.name);
|
|
174
|
+
const bi = scriptPriority.indexOf(b.name);
|
|
175
|
+
return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi);
|
|
176
|
+
});
|
|
177
|
+
// ── 4. Detect backend services ──
|
|
178
|
+
const services = [];
|
|
179
|
+
// Convex
|
|
180
|
+
if (existsSync(join(projectPath, "convex")) && (deps?.["convex"] || existsSync(join(projectPath, "convex.json")))) {
|
|
181
|
+
const convexScript = Object.entries(scripts).find(([, cmd]) => cmd.includes("convex dev"));
|
|
182
|
+
services.push({
|
|
183
|
+
name: "Convex",
|
|
184
|
+
type: "backend",
|
|
185
|
+
detected: "convex/ directory + convex dependency",
|
|
186
|
+
startCommand: convexScript ? `npm run ${convexScript[0]}` : "npx convex dev",
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
// Supabase
|
|
190
|
+
if (deps?.["@supabase/supabase-js"] || existsSync(join(projectPath, "supabase"))) {
|
|
191
|
+
services.push({ name: "Supabase", type: "backend", detected: "supabase dependency or supabase/ directory" });
|
|
192
|
+
}
|
|
193
|
+
// Firebase
|
|
194
|
+
if (deps?.["firebase"] || existsSync(join(projectPath, "firebase.json"))) {
|
|
195
|
+
services.push({ name: "Firebase", type: "backend", detected: "firebase dependency or firebase.json" });
|
|
196
|
+
}
|
|
197
|
+
// Prisma
|
|
198
|
+
if (deps?.["prisma"] || existsSync(join(projectPath, "prisma"))) {
|
|
199
|
+
services.push({ name: "Prisma", type: "orm", detected: "prisma dependency or prisma/ directory" });
|
|
200
|
+
}
|
|
201
|
+
// Docker
|
|
202
|
+
if (existsSync(join(projectPath, "docker-compose.yml")) || existsSync(join(projectPath, "docker-compose.yaml"))) {
|
|
203
|
+
services.push({ name: "Docker Compose", type: "infrastructure", detected: "docker-compose.yml found" });
|
|
204
|
+
}
|
|
205
|
+
// ── 5. Detect ports from config ──
|
|
206
|
+
let frontendPort = 3000; // default
|
|
207
|
+
if (framework.name === "vite")
|
|
208
|
+
frontendPort = 5173;
|
|
209
|
+
else if (framework.name === "next")
|
|
210
|
+
frontendPort = 3000;
|
|
211
|
+
else if (framework.name === "create-react-app")
|
|
212
|
+
frontendPort = 3000;
|
|
213
|
+
else if (framework.name === "nuxt")
|
|
214
|
+
frontendPort = 3000;
|
|
215
|
+
else if (framework.name === "astro")
|
|
216
|
+
frontendPort = 4321;
|
|
217
|
+
// Try to read port from vite config
|
|
218
|
+
if (framework.configFile && existsSync(join(projectPath, framework.configFile))) {
|
|
219
|
+
try {
|
|
220
|
+
const cfgContent = readFileSync(join(projectPath, framework.configFile), "utf-8");
|
|
221
|
+
const portMatch = cfgContent.match(/port\s*[:=]\s*(\d+)/);
|
|
222
|
+
if (portMatch)
|
|
223
|
+
frontendPort = parseInt(portMatch[1], 10);
|
|
224
|
+
}
|
|
225
|
+
catch { /* */ }
|
|
226
|
+
}
|
|
227
|
+
// ── 6. Check running ports ──
|
|
228
|
+
const portStatus = {};
|
|
229
|
+
if (doCheckPorts !== false) {
|
|
230
|
+
const portsToCheck = [frontendPort, 3000, 3001, 4321, 5173, 5174, 8080, 8788];
|
|
231
|
+
const uniquePorts = [...new Set(portsToCheck)];
|
|
232
|
+
await Promise.all(uniquePorts.map(async (p) => {
|
|
233
|
+
portStatus[p] = await checkPort(p);
|
|
234
|
+
}));
|
|
235
|
+
}
|
|
236
|
+
const frontendRunning = portStatus[frontendPort] === true;
|
|
237
|
+
// ── 7. Scan routes ──
|
|
238
|
+
let routes = [];
|
|
239
|
+
if (doScanRoutes !== false) {
|
|
240
|
+
const srcDir = existsSync(join(projectPath, "src")) ? join(projectPath, "src") :
|
|
241
|
+
existsSync(join(projectPath, "app")) ? join(projectPath, "app") : projectPath;
|
|
242
|
+
routes = extractRoutes(srcDir);
|
|
243
|
+
}
|
|
244
|
+
// ── 8. Check env files ──
|
|
245
|
+
const envFiles = [];
|
|
246
|
+
for (const name of [".env", ".env.local", ".env.development", ".env.development.local"]) {
|
|
247
|
+
if (existsSync(join(projectPath, name)))
|
|
248
|
+
envFiles.push(name);
|
|
249
|
+
}
|
|
250
|
+
// ── 9. Build launch plan ──
|
|
251
|
+
const launchSteps = [];
|
|
252
|
+
const runningServices = [];
|
|
253
|
+
if (!frontendRunning) {
|
|
254
|
+
const devCmd = devScripts.find(s => s.likely === "frontend");
|
|
255
|
+
launchSteps.push(devCmd
|
|
256
|
+
? `Start frontend: npm run ${devCmd.name} (runs: ${devCmd.command})`
|
|
257
|
+
: `Start frontend: npm run dev (port ${frontendPort})`);
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
runningServices.push(`Frontend already running on port ${frontendPort}`);
|
|
261
|
+
}
|
|
262
|
+
for (const svc of services) {
|
|
263
|
+
if (svc.type === "backend") {
|
|
264
|
+
launchSteps.push(svc.startCommand
|
|
265
|
+
? `Start ${svc.name}: ${svc.startCommand}`
|
|
266
|
+
: `Start ${svc.name} (check project docs for startup command)`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
launchSteps.push(`Verify app is accessible at http://localhost:${frontendPort}`);
|
|
270
|
+
launchSteps.push("Then: start_ui_dive → navigate routes → discover components → test interactions");
|
|
271
|
+
return {
|
|
272
|
+
project: {
|
|
273
|
+
name: pkg?.name ?? basename(projectPath),
|
|
274
|
+
path: projectPath,
|
|
275
|
+
version: pkg?.version,
|
|
276
|
+
},
|
|
277
|
+
framework,
|
|
278
|
+
devScripts,
|
|
279
|
+
services,
|
|
280
|
+
ports: {
|
|
281
|
+
frontend: frontendPort,
|
|
282
|
+
frontendRunning,
|
|
283
|
+
status: portStatus,
|
|
284
|
+
},
|
|
285
|
+
routes: {
|
|
286
|
+
count: routes.length,
|
|
287
|
+
discovered: routes.slice(0, 50),
|
|
288
|
+
},
|
|
289
|
+
envFiles,
|
|
290
|
+
launchPlan: {
|
|
291
|
+
alreadyRunning: runningServices,
|
|
292
|
+
stepsNeeded: launchSteps,
|
|
293
|
+
appUrl: `http://localhost:${frontendPort}`,
|
|
294
|
+
},
|
|
295
|
+
_hint: frontendRunning
|
|
296
|
+
? `App is running at http://localhost:${frontendPort}. Proceed with start_ui_dive({ appUrl: "http://localhost:${frontendPort}" }) then navigate routes with Playwright.`
|
|
297
|
+
: `App is NOT running. Execute the launch plan steps first, then start the dive.`,
|
|
298
|
+
};
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
// ── 1. Save a labeled screenshot ──────────────────────────────────────
|
|
302
|
+
{
|
|
303
|
+
name: "dive_save_screenshot",
|
|
304
|
+
description: "Save a screenshot during a dive session. Pass base64 image data (from bridge's browser_take_screenshot) or a file path. The screenshot is stored on disk and indexed in the DB with labels, route, component, and test references. Returns a screenshot_id you can link to bugs, test steps, design issues, and changelogs. This creates the visual evidence trail for the entire dive.",
|
|
305
|
+
inputSchema: {
|
|
306
|
+
type: "object",
|
|
307
|
+
properties: {
|
|
308
|
+
sessionId: { type: "string", description: "Dive session ID" },
|
|
309
|
+
label: { type: "string", description: "Human-readable label (e.g. 'Login form - initial state', 'After clicking submit')" },
|
|
310
|
+
base64Data: { type: "string", description: "Base64-encoded image data (from browser_take_screenshot)" },
|
|
311
|
+
filePath: { type: "string", description: "Alternative: path to an existing screenshot file" },
|
|
312
|
+
componentId: { type: "string", description: "Component this screenshot is for (optional)" },
|
|
313
|
+
route: { type: "string", description: "Current route/URL (optional)" },
|
|
314
|
+
testId: { type: "string", description: "Interaction test this belongs to (optional)" },
|
|
315
|
+
stepIndex: { type: "number", description: "Step index within a test (optional)" },
|
|
316
|
+
metadata: { type: "object", description: "Additional metadata (optional)" },
|
|
317
|
+
},
|
|
318
|
+
required: ["sessionId", "label"],
|
|
319
|
+
},
|
|
320
|
+
handler: async (args) => {
|
|
321
|
+
const { sessionId, label, base64Data, filePath, componentId, route, testId, stepIndex, metadata } = args;
|
|
322
|
+
const db = getDb();
|
|
323
|
+
const session = db.prepare("SELECT id FROM ui_dive_sessions WHERE id = ?").get(sessionId);
|
|
324
|
+
if (!session)
|
|
325
|
+
return { error: true, message: `Session not found: ${sessionId}` };
|
|
326
|
+
const id = genId("ss");
|
|
327
|
+
let savedPath = filePath ?? null;
|
|
328
|
+
// Save base64 data to disk
|
|
329
|
+
if (base64Data && !filePath) {
|
|
330
|
+
const dir = screenshotDir();
|
|
331
|
+
const filename = `${sessionId}_${id}.png`;
|
|
332
|
+
savedPath = join(dir, filename);
|
|
333
|
+
try {
|
|
334
|
+
const buffer = Buffer.from(base64Data, "base64");
|
|
335
|
+
writeFileSync(savedPath, buffer);
|
|
336
|
+
}
|
|
337
|
+
catch (e) {
|
|
338
|
+
return { error: true, message: `Failed to save screenshot: ${e.message}` };
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// Store a small thumbnail (first 500 chars of base64 for quick preview)
|
|
342
|
+
const thumbnail = base64Data ? base64Data.slice(0, 500) : null;
|
|
343
|
+
db.prepare(`INSERT INTO ui_dive_screenshots (id, session_id, component_id, test_id, step_index, label, route, file_path, base64_thumbnail, metadata)
|
|
344
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, sessionId, componentId ?? null, testId ?? null, stepIndex ?? null, label, route ?? null, savedPath, thumbnail, metadata ? JSON.stringify(metadata) : null);
|
|
345
|
+
return {
|
|
346
|
+
screenshotId: id,
|
|
347
|
+
label,
|
|
348
|
+
filePath: savedPath,
|
|
349
|
+
componentId: componentId ?? null,
|
|
350
|
+
route: route ?? null,
|
|
351
|
+
_hint: `Screenshot saved. Reference it in bugs: tag_ui_bug({ screenshotRef: "${id}" }), test steps, design issues, or changelogs.`,
|
|
352
|
+
};
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
// ── 2. Run a structured interaction test ──────────────────────────────
|
|
356
|
+
{
|
|
357
|
+
name: "dive_interaction_test",
|
|
358
|
+
description: "Define and track a structured interaction test for a component. Provide preconditions and a sequence of test steps (action, target, expected outcome). The agent executes each step via the MCP Bridge (browser_click, browser_type, etc.), takes screenshots, and records actual results here. Each step gets pass/fail status. The test aggregates into an overall result. This creates the detailed walkthrough with preconditions, steps, expected vs actual, and visual evidence at each step.",
|
|
359
|
+
inputSchema: {
|
|
360
|
+
type: "object",
|
|
361
|
+
properties: {
|
|
362
|
+
sessionId: { type: "string", description: "Dive session ID" },
|
|
363
|
+
componentId: { type: "string", description: "Component being tested" },
|
|
364
|
+
testName: { type: "string", description: "Test name (e.g. 'Login form submission', 'Dark mode toggle')" },
|
|
365
|
+
description: { type: "string", description: "What this test validates" },
|
|
366
|
+
preconditions: {
|
|
367
|
+
type: "array",
|
|
368
|
+
description: "List of preconditions (e.g. ['User is logged out', 'Browser at /login', 'Dark mode is off'])",
|
|
369
|
+
items: { type: "string" },
|
|
370
|
+
},
|
|
371
|
+
steps: {
|
|
372
|
+
type: "array",
|
|
373
|
+
description: "Test steps to execute and track",
|
|
374
|
+
items: {
|
|
375
|
+
type: "object",
|
|
376
|
+
properties: {
|
|
377
|
+
action: { type: "string", description: "Action: click, type, navigate, hover, scroll, assert, wait, screenshot" },
|
|
378
|
+
target: { type: "string", description: "CSS selector, URL, or description" },
|
|
379
|
+
inputValue: { type: "string", description: "Value to type/enter (for type action)" },
|
|
380
|
+
expected: { type: "string", description: "Expected outcome (e.g. 'Form submits', 'Error message appears', 'Redirects to /dashboard')" },
|
|
381
|
+
screenshotLabel: { type: "string", description: "Label for the screenshot at this step (optional)" },
|
|
382
|
+
},
|
|
383
|
+
required: ["action", "expected"],
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
metadata: { type: "object", description: "Optional metadata" },
|
|
387
|
+
},
|
|
388
|
+
required: ["sessionId", "componentId", "testName", "steps"],
|
|
389
|
+
},
|
|
390
|
+
handler: async (args) => {
|
|
391
|
+
const { sessionId, componentId, testName, description, preconditions, steps, metadata } = args;
|
|
392
|
+
const db = getDb();
|
|
393
|
+
const session = db.prepare("SELECT id FROM ui_dive_sessions WHERE id = ?").get(sessionId);
|
|
394
|
+
if (!session)
|
|
395
|
+
return { error: true, message: `Session not found: ${sessionId}` };
|
|
396
|
+
const comp = db.prepare("SELECT id FROM ui_dive_components WHERE id = ?").get(componentId);
|
|
397
|
+
if (!comp)
|
|
398
|
+
return { error: true, message: `Component not found: ${componentId}` };
|
|
399
|
+
const testId = genId("test");
|
|
400
|
+
db.prepare(`INSERT INTO ui_dive_interaction_tests (id, session_id, component_id, test_name, description, preconditions, steps_total, metadata)
|
|
401
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(testId, sessionId, componentId, testName, description ?? null, preconditions ? JSON.stringify(preconditions) : null, steps.length, metadata ? JSON.stringify(metadata) : null);
|
|
402
|
+
// Create step rows
|
|
403
|
+
const stepIds = [];
|
|
404
|
+
for (let i = 0; i < steps.length; i++) {
|
|
405
|
+
const s = steps[i];
|
|
406
|
+
const stepId = genId("step");
|
|
407
|
+
db.prepare(`INSERT INTO ui_dive_test_steps (id, test_id, step_index, action, target, input_value, expected)
|
|
408
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`).run(stepId, testId, i, s.action, s.target ?? null, s.inputValue ?? null, s.expected);
|
|
409
|
+
stepIds.push(stepId);
|
|
410
|
+
}
|
|
411
|
+
return {
|
|
412
|
+
testId,
|
|
413
|
+
testName,
|
|
414
|
+
componentId,
|
|
415
|
+
stepsTotal: steps.length,
|
|
416
|
+
stepIds,
|
|
417
|
+
status: "pending",
|
|
418
|
+
_workflow: [
|
|
419
|
+
"For each step, the agent should:",
|
|
420
|
+
"1. Execute the action via MCP Bridge (browser_click, browser_type, etc.)",
|
|
421
|
+
"2. Take a screenshot via bridge (browser_take_screenshot)",
|
|
422
|
+
"3. Save it: dive_save_screenshot({ testId, stepIndex, label, base64Data })",
|
|
423
|
+
"4. Record result: dive_record_test_step({ testId, stepIndex, actual, status, screenshotId })",
|
|
424
|
+
"5. After all steps: dive completes the test automatically",
|
|
425
|
+
],
|
|
426
|
+
_hint: `Test created with ${steps.length} steps. Execute each step and record results with dive_record_test_step.`,
|
|
427
|
+
};
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
// ── 3. Record a test step result ──────────────────────────────────────
|
|
431
|
+
{
|
|
432
|
+
name: "dive_record_test_step",
|
|
433
|
+
description: "Record the actual result of a test step after executing it via the MCP Bridge. Compare expected vs actual, attach a screenshot, and mark pass/fail. When all steps are recorded, the test is automatically completed with an overall status.",
|
|
434
|
+
inputSchema: {
|
|
435
|
+
type: "object",
|
|
436
|
+
properties: {
|
|
437
|
+
testId: { type: "string", description: "Interaction test ID from dive_interaction_test" },
|
|
438
|
+
stepIndex: { type: "number", description: "0-based step index" },
|
|
439
|
+
actual: { type: "string", description: "What actually happened" },
|
|
440
|
+
status: {
|
|
441
|
+
type: "string",
|
|
442
|
+
description: "Step result: passed, failed, skipped, blocked",
|
|
443
|
+
enum: ["passed", "failed", "skipped", "blocked"],
|
|
444
|
+
},
|
|
445
|
+
screenshotId: { type: "string", description: "Screenshot ID from dive_save_screenshot (optional)" },
|
|
446
|
+
observation: { type: "string", description: "Additional notes about this step" },
|
|
447
|
+
durationMs: { type: "number", description: "How long the step took" },
|
|
448
|
+
},
|
|
449
|
+
required: ["testId", "stepIndex", "status", "actual"],
|
|
450
|
+
},
|
|
451
|
+
handler: async (args) => {
|
|
452
|
+
const { testId, stepIndex, actual, status, screenshotId, observation, durationMs } = args;
|
|
453
|
+
const db = getDb();
|
|
454
|
+
const step = db.prepare("SELECT id, expected FROM ui_dive_test_steps WHERE test_id = ? AND step_index = ?").get(testId, stepIndex);
|
|
455
|
+
if (!step)
|
|
456
|
+
return { error: true, message: `Step not found: test=${testId}, index=${stepIndex}` };
|
|
457
|
+
db.prepare("UPDATE ui_dive_test_steps SET actual = ?, status = ?, screenshot_id = ?, observation = ?, duration_ms = ? WHERE id = ?").run(actual, status, screenshotId ?? null, observation ?? null, durationMs ?? null, step.id);
|
|
458
|
+
// Check if all steps are done → auto-complete the test
|
|
459
|
+
const test = db.prepare("SELECT steps_total FROM ui_dive_interaction_tests WHERE id = ?").get(testId);
|
|
460
|
+
const completed = db.prepare("SELECT COUNT(*) as c FROM ui_dive_test_steps WHERE test_id = ? AND status != 'pending'").get(testId);
|
|
461
|
+
const passed = db.prepare("SELECT COUNT(*) as c FROM ui_dive_test_steps WHERE test_id = ? AND status = 'passed'").get(testId);
|
|
462
|
+
const failed = db.prepare("SELECT COUNT(*) as c FROM ui_dive_test_steps WHERE test_id = ? AND status = 'failed'").get(testId);
|
|
463
|
+
const allDone = completed.c >= test.steps_total;
|
|
464
|
+
if (allDone) {
|
|
465
|
+
const overallStatus = failed.c > 0 ? "failed" : "passed";
|
|
466
|
+
db.prepare("UPDATE ui_dive_interaction_tests SET status = ?, steps_passed = ?, steps_failed = ?, completed_at = datetime('now') WHERE id = ?").run(overallStatus, passed.c, failed.c, testId);
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
db.prepare("UPDATE ui_dive_interaction_tests SET steps_passed = ?, steps_failed = ? WHERE id = ?").run(passed.c, failed.c, testId);
|
|
470
|
+
}
|
|
471
|
+
return {
|
|
472
|
+
stepId: step.id,
|
|
473
|
+
stepIndex,
|
|
474
|
+
expected: step.expected,
|
|
475
|
+
actual,
|
|
476
|
+
status,
|
|
477
|
+
match: status === "passed",
|
|
478
|
+
screenshotId: screenshotId ?? null,
|
|
479
|
+
testProgress: `${completed.c}/${test.steps_total}`,
|
|
480
|
+
testComplete: allDone,
|
|
481
|
+
...(allDone ? { testStatus: failed.c > 0 ? "failed" : "passed" } : {}),
|
|
482
|
+
_hint: allDone
|
|
483
|
+
? `Test complete: ${passed.c} passed, ${failed.c} failed.`
|
|
484
|
+
: `Step ${stepIndex} recorded. ${test.steps_total - completed.c} steps remaining.`,
|
|
485
|
+
};
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
// ── 4. Tag a design inconsistency ─────────────────────────────────────
|
|
489
|
+
{
|
|
490
|
+
name: "dive_design_issue",
|
|
491
|
+
description: "Tag a design inconsistency found during the dive. Covers visual problems like color mismatches, spacing deviations, font inconsistencies, alignment issues, contrast failures, responsive breakage, missing hover/focus states, and more. Link to a screenshot and the specific element. The agent uses bridge's browser_evaluate to extract computed styles and compare across components.",
|
|
492
|
+
inputSchema: {
|
|
493
|
+
type: "object",
|
|
494
|
+
properties: {
|
|
495
|
+
sessionId: { type: "string", description: "Dive session ID" },
|
|
496
|
+
componentId: { type: "string", description: "Component with the issue (optional)" },
|
|
497
|
+
issueType: {
|
|
498
|
+
type: "string",
|
|
499
|
+
description: "Type: color, spacing, font, alignment, contrast, responsive, hover_state, focus_state, animation, icon, border, shadow, z_index, overflow, consistency",
|
|
500
|
+
},
|
|
501
|
+
severity: {
|
|
502
|
+
type: "string",
|
|
503
|
+
description: "Severity: critical (broken UX), high (obvious visual bug), medium (noticeable deviation), low (minor polish)",
|
|
504
|
+
enum: ["critical", "high", "medium", "low"],
|
|
505
|
+
},
|
|
506
|
+
title: { type: "string", description: "Short description (e.g. 'Button color mismatch between header and sidebar')" },
|
|
507
|
+
description: { type: "string", description: "Detailed explanation" },
|
|
508
|
+
elementSelector: { type: "string", description: "CSS selector of the affected element" },
|
|
509
|
+
expectedValue: { type: "string", description: "What the design should be (e.g. '#3B82F6', '16px', 'Inter')" },
|
|
510
|
+
actualValue: { type: "string", description: "What was actually found (e.g. '#2563EB', '12px', 'system-ui')" },
|
|
511
|
+
screenshotId: { type: "string", description: "Screenshot showing the issue" },
|
|
512
|
+
route: { type: "string", description: "Route where the issue was found" },
|
|
513
|
+
metadata: { type: "object", description: "Additional context (e.g. { breakpoint: '768px', theme: 'dark' })" },
|
|
514
|
+
},
|
|
515
|
+
required: ["sessionId", "issueType", "title"],
|
|
516
|
+
},
|
|
517
|
+
handler: async (args) => {
|
|
518
|
+
const { sessionId, componentId, issueType, severity, title, description, elementSelector, expectedValue, actualValue, screenshotId, route, metadata } = args;
|
|
519
|
+
const db = getDb();
|
|
520
|
+
const session = db.prepare("SELECT id FROM ui_dive_sessions WHERE id = ?").get(sessionId);
|
|
521
|
+
if (!session)
|
|
522
|
+
return { error: true, message: `Session not found: ${sessionId}` };
|
|
523
|
+
const id = genId("design");
|
|
524
|
+
db.prepare(`INSERT INTO ui_dive_design_issues (id, session_id, component_id, issue_type, severity, title, description, element_selector, expected_value, actual_value, screenshot_id, route, metadata)
|
|
525
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, sessionId, componentId ?? null, issueType, severity ?? "medium", title, description ?? null, elementSelector ?? null, expectedValue ?? null, actualValue ?? null, screenshotId ?? null, route ?? null, metadata ? JSON.stringify(metadata) : null);
|
|
526
|
+
return {
|
|
527
|
+
designIssueId: id,
|
|
528
|
+
issueType,
|
|
529
|
+
severity: severity ?? "medium",
|
|
530
|
+
title,
|
|
531
|
+
expectedValue: expectedValue ?? null,
|
|
532
|
+
actualValue: actualValue ?? null,
|
|
533
|
+
_hint: `Design issue tagged. View all issues in the dive report. Fix it, then track with dive_changelog.`,
|
|
534
|
+
};
|
|
535
|
+
},
|
|
536
|
+
},
|
|
537
|
+
// ── 5. Link UI component to backend context ───────────────────────────
|
|
538
|
+
{
|
|
539
|
+
name: "dive_link_backend",
|
|
540
|
+
description: "Link a UI component to its backend dependencies. Connect components to API endpoints, Convex queries/mutations/actions, database tables, auth guards, WebSocket channels, or external services. This creates the full-stack traceability map — when a UI bug is found, you can immediately see which backend code is involved.",
|
|
541
|
+
inputSchema: {
|
|
542
|
+
type: "object",
|
|
543
|
+
properties: {
|
|
544
|
+
sessionId: { type: "string", description: "Dive session ID" },
|
|
545
|
+
componentId: { type: "string", description: "Component to link" },
|
|
546
|
+
links: {
|
|
547
|
+
type: "array",
|
|
548
|
+
description: "Backend references to link",
|
|
549
|
+
items: {
|
|
550
|
+
type: "object",
|
|
551
|
+
properties: {
|
|
552
|
+
linkType: {
|
|
553
|
+
type: "string",
|
|
554
|
+
description: "Type: convex_query, convex_mutation, convex_action, api_endpoint, db_table, auth_guard, websocket, external_service, env_var, cron_job",
|
|
555
|
+
},
|
|
556
|
+
path: { type: "string", description: "Path/identifier (e.g. 'api.domains.documents.documents.getSidebar', '/api/users', 'documents' table)" },
|
|
557
|
+
description: { type: "string", description: "What this backend dependency does for the component" },
|
|
558
|
+
method: { type: "string", description: "HTTP method for API endpoints (GET, POST, etc.)" },
|
|
559
|
+
},
|
|
560
|
+
required: ["linkType", "path"],
|
|
561
|
+
},
|
|
562
|
+
},
|
|
563
|
+
},
|
|
564
|
+
required: ["sessionId", "componentId", "links"],
|
|
565
|
+
},
|
|
566
|
+
handler: async (args) => {
|
|
567
|
+
const { sessionId, componentId, links } = args;
|
|
568
|
+
const db = getDb();
|
|
569
|
+
const session = db.prepare("SELECT id FROM ui_dive_sessions WHERE id = ?").get(sessionId);
|
|
570
|
+
if (!session)
|
|
571
|
+
return { error: true, message: `Session not found: ${sessionId}` };
|
|
572
|
+
const comp = db.prepare("SELECT id, name FROM ui_dive_components WHERE id = ?").get(componentId);
|
|
573
|
+
if (!comp)
|
|
574
|
+
return { error: true, message: `Component not found: ${componentId}` };
|
|
575
|
+
const ids = [];
|
|
576
|
+
for (const link of links) {
|
|
577
|
+
const id = genId("blink");
|
|
578
|
+
db.prepare(`INSERT INTO ui_dive_backend_links (id, session_id, component_id, link_type, path, description, method)
|
|
579
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`).run(id, sessionId, componentId, link.linkType, link.path, link.description ?? null, link.method ?? null);
|
|
580
|
+
ids.push(id);
|
|
581
|
+
}
|
|
582
|
+
return {
|
|
583
|
+
componentId,
|
|
584
|
+
componentName: comp.name,
|
|
585
|
+
linksCreated: ids.length,
|
|
586
|
+
links: links.map((l, i) => ({ linkId: ids[i], ...l })),
|
|
587
|
+
_hint: `${ids.length} backend link(s) created for ${comp.name}. These will appear in the dive report and walkthrough.`,
|
|
588
|
+
};
|
|
589
|
+
},
|
|
590
|
+
},
|
|
591
|
+
// ── 6. Track a change (changelog entry) ───────────────────────────────
|
|
592
|
+
{
|
|
593
|
+
name: "dive_changelog",
|
|
594
|
+
description: "Record a change made to fix a bug, design issue, or improve a component. Links before/after screenshots to show what changed visually. Optionally references git commits and changed files. When the dive is re-run after fixes, the changelog provides a clear audit trail of what was wrong, what was changed, and how it looks now.",
|
|
595
|
+
inputSchema: {
|
|
596
|
+
type: "object",
|
|
597
|
+
properties: {
|
|
598
|
+
sessionId: { type: "string", description: "Dive session ID" },
|
|
599
|
+
componentId: { type: "string", description: "Component that was changed (optional)" },
|
|
600
|
+
changeType: {
|
|
601
|
+
type: "string",
|
|
602
|
+
description: "Type: bugfix, design_fix, feature, refactor, accessibility, performance, content, responsive",
|
|
603
|
+
},
|
|
604
|
+
description: { type: "string", description: "What was changed and why" },
|
|
605
|
+
beforeScreenshotId: { type: "string", description: "Screenshot before the change (from dive_save_screenshot)" },
|
|
606
|
+
afterScreenshotId: { type: "string", description: "Screenshot after the change" },
|
|
607
|
+
filesChanged: {
|
|
608
|
+
type: "array",
|
|
609
|
+
description: "List of files that were modified",
|
|
610
|
+
items: { type: "string" },
|
|
611
|
+
},
|
|
612
|
+
gitCommit: { type: "string", description: "Git commit hash (optional)" },
|
|
613
|
+
metadata: { type: "object", description: "Additional context (e.g. { bugId: '...', designIssueId: '...' })" },
|
|
614
|
+
},
|
|
615
|
+
required: ["sessionId", "changeType", "description"],
|
|
616
|
+
},
|
|
617
|
+
handler: async (args) => {
|
|
618
|
+
const { sessionId, componentId, changeType, description, beforeScreenshotId, afterScreenshotId, filesChanged, gitCommit, metadata } = args;
|
|
619
|
+
const db = getDb();
|
|
620
|
+
const session = db.prepare("SELECT id FROM ui_dive_sessions WHERE id = ?").get(sessionId);
|
|
621
|
+
if (!session)
|
|
622
|
+
return { error: true, message: `Session not found: ${sessionId}` };
|
|
623
|
+
const id = genId("chg");
|
|
624
|
+
db.prepare(`INSERT INTO ui_dive_changelogs (id, session_id, component_id, change_type, description, before_screenshot_id, after_screenshot_id, files_changed, git_commit, metadata)
|
|
625
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, sessionId, componentId ?? null, changeType, description, beforeScreenshotId ?? null, afterScreenshotId ?? null, filesChanged ? JSON.stringify(filesChanged) : null, gitCommit ?? null, metadata ? JSON.stringify(metadata) : null);
|
|
626
|
+
return {
|
|
627
|
+
changelogId: id,
|
|
628
|
+
changeType,
|
|
629
|
+
description,
|
|
630
|
+
beforeScreenshotId: beforeScreenshotId ?? null,
|
|
631
|
+
afterScreenshotId: afterScreenshotId ?? null,
|
|
632
|
+
filesChanged: filesChanged ?? [],
|
|
633
|
+
gitCommit: gitCommit ?? null,
|
|
634
|
+
_hint: "Changelog entry recorded. It will appear in the dive report and walkthrough.",
|
|
635
|
+
};
|
|
636
|
+
},
|
|
637
|
+
},
|
|
638
|
+
// ── 7. Generate a complete walkthrough ────────────────────────────────
|
|
639
|
+
{
|
|
640
|
+
name: "dive_walkthrough",
|
|
641
|
+
description: "Generate a comprehensive page-by-page, component-by-component walkthrough document for a dive session. Includes: route map with source files, component tree, interaction test results with pass/fail per step, screenshots referenced at each point, design issues found, backend dependencies, console errors, and changelog entries. This is the final deliverable — a complete QA document that an agent or human can review.",
|
|
642
|
+
inputSchema: {
|
|
643
|
+
type: "object",
|
|
644
|
+
properties: {
|
|
645
|
+
sessionId: { type: "string", description: "Dive session ID" },
|
|
646
|
+
format: {
|
|
647
|
+
type: "string",
|
|
648
|
+
description: "Output format: markdown (readable), json (structured), summary (condensed)",
|
|
649
|
+
enum: ["markdown", "json", "summary"],
|
|
650
|
+
},
|
|
651
|
+
includeScreenshotPaths: {
|
|
652
|
+
type: "boolean",
|
|
653
|
+
description: "Include file paths to screenshots (default: true)",
|
|
654
|
+
},
|
|
655
|
+
},
|
|
656
|
+
required: ["sessionId"],
|
|
657
|
+
},
|
|
658
|
+
handler: async (args) => {
|
|
659
|
+
const { sessionId, format, includeScreenshotPaths } = args;
|
|
660
|
+
const db = getDb();
|
|
661
|
+
const session = db.prepare("SELECT * FROM ui_dive_sessions WHERE id = ?").get(sessionId);
|
|
662
|
+
if (!session)
|
|
663
|
+
return { error: true, message: `Session not found: ${sessionId}` };
|
|
664
|
+
const components = db.prepare("SELECT * FROM ui_dive_components WHERE session_id = ? ORDER BY created_at").all(sessionId);
|
|
665
|
+
const bugs = db.prepare("SELECT * FROM ui_dive_bugs WHERE session_id = ? ORDER BY severity, created_at").all(sessionId);
|
|
666
|
+
const screenshots = db.prepare("SELECT * FROM ui_dive_screenshots WHERE session_id = ? ORDER BY created_at").all(sessionId);
|
|
667
|
+
const tests = db.prepare("SELECT * FROM ui_dive_interaction_tests WHERE session_id = ? ORDER BY created_at").all(sessionId);
|
|
668
|
+
const designIssues = db.prepare("SELECT * FROM ui_dive_design_issues WHERE session_id = ? ORDER BY severity, created_at").all(sessionId);
|
|
669
|
+
const backendLinks = db.prepare("SELECT * FROM ui_dive_backend_links WHERE session_id = ? ORDER BY component_id").all(sessionId);
|
|
670
|
+
const changelogs = db.prepare("SELECT * FROM ui_dive_changelogs WHERE session_id = ? ORDER BY created_at").all(sessionId);
|
|
671
|
+
// Load test steps for each test
|
|
672
|
+
const testSteps = {};
|
|
673
|
+
for (const test of tests) {
|
|
674
|
+
testSteps[test.id] = db.prepare("SELECT * FROM ui_dive_test_steps WHERE test_id = ? ORDER BY step_index").all(test.id);
|
|
675
|
+
}
|
|
676
|
+
// Group components by route (from metadata)
|
|
677
|
+
const routeGroups = new Map();
|
|
678
|
+
for (const comp of components) {
|
|
679
|
+
const meta = comp.metadata ? JSON.parse(comp.metadata) : {};
|
|
680
|
+
const route = meta.route ?? "(unrouted)";
|
|
681
|
+
if (!routeGroups.has(route))
|
|
682
|
+
routeGroups.set(route, []);
|
|
683
|
+
routeGroups.get(route).push({ ...comp, _meta: meta });
|
|
684
|
+
}
|
|
685
|
+
if (format === "json") {
|
|
686
|
+
return {
|
|
687
|
+
session: {
|
|
688
|
+
id: session.id,
|
|
689
|
+
appUrl: session.app_url,
|
|
690
|
+
appName: session.app_name,
|
|
691
|
+
status: session.status,
|
|
692
|
+
createdAt: session.created_at,
|
|
693
|
+
},
|
|
694
|
+
stats: {
|
|
695
|
+
routes: routeGroups.size,
|
|
696
|
+
components: components.length,
|
|
697
|
+
bugs: bugs.length,
|
|
698
|
+
screenshots: screenshots.length,
|
|
699
|
+
tests: tests.length,
|
|
700
|
+
testsPassed: tests.filter((t) => t.status === "passed").length,
|
|
701
|
+
testsFailed: tests.filter((t) => t.status === "failed").length,
|
|
702
|
+
designIssues: designIssues.length,
|
|
703
|
+
backendLinks: backendLinks.length,
|
|
704
|
+
changelogs: changelogs.length,
|
|
705
|
+
},
|
|
706
|
+
routes: Object.fromEntries([...routeGroups.entries()].map(([route, comps]) => [
|
|
707
|
+
route,
|
|
708
|
+
{
|
|
709
|
+
components: comps.map(c => ({
|
|
710
|
+
id: c.id,
|
|
711
|
+
name: c.name,
|
|
712
|
+
type: c.component_type,
|
|
713
|
+
status: c.status,
|
|
714
|
+
sourceFiles: c._meta.sourceFiles ?? [],
|
|
715
|
+
bugs: bugs.filter(b => b.component_id === c.id).map(b => ({ id: b.id, severity: b.severity, title: b.title })),
|
|
716
|
+
backendLinks: backendLinks.filter(l => l.component_id === c.id).map(l => ({ type: l.link_type, path: l.path })),
|
|
717
|
+
tests: tests.filter(t => t.component_id === c.id).map(t => ({
|
|
718
|
+
id: t.id,
|
|
719
|
+
name: t.test_name,
|
|
720
|
+
status: t.status,
|
|
721
|
+
passed: t.steps_passed,
|
|
722
|
+
failed: t.steps_failed,
|
|
723
|
+
total: t.steps_total,
|
|
724
|
+
steps: (testSteps[t.id] ?? []).map(s => ({
|
|
725
|
+
index: s.step_index,
|
|
726
|
+
action: s.action,
|
|
727
|
+
expected: s.expected,
|
|
728
|
+
actual: s.actual,
|
|
729
|
+
status: s.status,
|
|
730
|
+
screenshotId: s.screenshot_id,
|
|
731
|
+
})),
|
|
732
|
+
})),
|
|
733
|
+
})),
|
|
734
|
+
designIssues: designIssues.filter(d => comps.some(c => c.id === d.component_id)).map(d => ({
|
|
735
|
+
id: d.id,
|
|
736
|
+
type: d.issue_type,
|
|
737
|
+
severity: d.severity,
|
|
738
|
+
title: d.title,
|
|
739
|
+
expected: d.expected_value,
|
|
740
|
+
actual: d.actual_value,
|
|
741
|
+
})),
|
|
742
|
+
},
|
|
743
|
+
])),
|
|
744
|
+
changelogs: changelogs.map(c => ({
|
|
745
|
+
id: c.id,
|
|
746
|
+
type: c.change_type,
|
|
747
|
+
description: c.description,
|
|
748
|
+
filesChanged: c.files_changed ? JSON.parse(c.files_changed) : [],
|
|
749
|
+
gitCommit: c.git_commit,
|
|
750
|
+
})),
|
|
751
|
+
screenshots: includeScreenshotPaths !== false
|
|
752
|
+
? screenshots.map(s => ({ id: s.id, label: s.label, filePath: s.file_path, route: s.route }))
|
|
753
|
+
: undefined,
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
// Markdown format
|
|
757
|
+
const lines = [];
|
|
758
|
+
lines.push(`# UI/UX Dive Walkthrough: ${session.app_name ?? session.app_url}`);
|
|
759
|
+
lines.push(`**Session:** ${session.id} `);
|
|
760
|
+
lines.push(`**URL:** ${session.app_url} `);
|
|
761
|
+
lines.push(`**Date:** ${session.created_at} `);
|
|
762
|
+
lines.push(`**Status:** ${session.status}\n`);
|
|
763
|
+
// Stats
|
|
764
|
+
lines.push("## Summary\n");
|
|
765
|
+
lines.push(`| Metric | Value |`);
|
|
766
|
+
lines.push(`|--------|-------|`);
|
|
767
|
+
lines.push(`| Routes | ${routeGroups.size} |`);
|
|
768
|
+
lines.push(`| Components | ${components.length} |`);
|
|
769
|
+
lines.push(`| Interaction Tests | ${tests.length} (${tests.filter((t) => t.status === "passed").length} passed, ${tests.filter((t) => t.status === "failed").length} failed) |`);
|
|
770
|
+
lines.push(`| Bugs | ${bugs.length} |`);
|
|
771
|
+
lines.push(`| Design Issues | ${designIssues.length} |`);
|
|
772
|
+
lines.push(`| Screenshots | ${screenshots.length} |`);
|
|
773
|
+
lines.push(`| Backend Links | ${backendLinks.length} |`);
|
|
774
|
+
lines.push(`| Changelogs | ${changelogs.length} |`);
|
|
775
|
+
lines.push("");
|
|
776
|
+
// Route-by-route walkthrough
|
|
777
|
+
lines.push("## Route-by-Route Walkthrough\n");
|
|
778
|
+
for (const [route, comps] of routeGroups) {
|
|
779
|
+
const sourceFiles = comps[0]?._meta?.sourceFiles ?? [];
|
|
780
|
+
lines.push(`### ${route}\n`);
|
|
781
|
+
if (sourceFiles.length > 0)
|
|
782
|
+
lines.push(`**Source files:** ${sourceFiles.join(", ")} `);
|
|
783
|
+
lines.push(`**Components:** ${comps.length}\n`);
|
|
784
|
+
for (const comp of comps) {
|
|
785
|
+
lines.push(`#### ${comp.name} (${comp.component_type})`);
|
|
786
|
+
lines.push(`- **Status:** ${comp.status}`);
|
|
787
|
+
lines.push(`- **Interactions:** ${comp.interaction_count}`);
|
|
788
|
+
// Backend links
|
|
789
|
+
const compLinks = backendLinks.filter(l => l.component_id === comp.id);
|
|
790
|
+
if (compLinks.length > 0) {
|
|
791
|
+
lines.push(`- **Backend dependencies:**`);
|
|
792
|
+
for (const link of compLinks) {
|
|
793
|
+
lines.push(` - \`[${link.link_type}]\` ${link.path}${link.description ? ` -- ${link.description}` : ""}`);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
// Tests for this component
|
|
797
|
+
const compTests = tests.filter(t => t.component_id === comp.id);
|
|
798
|
+
if (compTests.length > 0) {
|
|
799
|
+
lines.push(`\n**Interaction Tests:**\n`);
|
|
800
|
+
for (const test of compTests) {
|
|
801
|
+
const icon = test.status === "passed" ? "PASS" : test.status === "failed" ? "FAIL" : "PENDING";
|
|
802
|
+
lines.push(`##### [${icon}] ${test.test_name}`);
|
|
803
|
+
if (test.description)
|
|
804
|
+
lines.push(`${test.description}`);
|
|
805
|
+
if (test.preconditions) {
|
|
806
|
+
const preconds = JSON.parse(test.preconditions);
|
|
807
|
+
lines.push(`\n**Preconditions:**`);
|
|
808
|
+
for (const p of preconds)
|
|
809
|
+
lines.push(`- ${p}`);
|
|
810
|
+
}
|
|
811
|
+
lines.push(`\n| Step | Action | Expected | Actual | Status | Screenshot |`);
|
|
812
|
+
lines.push(`|------|--------|----------|--------|--------|------------|`);
|
|
813
|
+
for (const step of (testSteps[test.id] ?? [])) {
|
|
814
|
+
const stepIcon = step.status === "passed" ? "PASS" : step.status === "failed" ? "FAIL" : step.status;
|
|
815
|
+
const ssRef = step.screenshot_id ?? "-";
|
|
816
|
+
lines.push(`| ${step.step_index} | ${step.action} ${step.target ?? ""} | ${step.expected ?? ""} | ${step.actual ?? "-"} | ${stepIcon} | ${ssRef} |`);
|
|
817
|
+
}
|
|
818
|
+
lines.push("");
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
// Bugs
|
|
822
|
+
const compBugs = bugs.filter(b => b.component_id === comp.id);
|
|
823
|
+
if (compBugs.length > 0) {
|
|
824
|
+
lines.push(`\n**Bugs:**\n`);
|
|
825
|
+
for (const bug of compBugs) {
|
|
826
|
+
lines.push(`- **[${bug.severity.toUpperCase()}]** ${bug.title}`);
|
|
827
|
+
if (bug.description)
|
|
828
|
+
lines.push(` ${bug.description}`);
|
|
829
|
+
if (bug.screenshot_ref)
|
|
830
|
+
lines.push(` Screenshot: ${bug.screenshot_ref}`);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
lines.push("");
|
|
834
|
+
}
|
|
835
|
+
// Design issues for this route
|
|
836
|
+
const routeDesignIssues = designIssues.filter(d => d.route === route);
|
|
837
|
+
if (routeDesignIssues.length > 0) {
|
|
838
|
+
lines.push(`**Design Issues on ${route}:**\n`);
|
|
839
|
+
for (const issue of routeDesignIssues) {
|
|
840
|
+
lines.push(`- **[${issue.severity.toUpperCase()}] ${issue.issue_type}:** ${issue.title}`);
|
|
841
|
+
if (issue.expected_value || issue.actual_value) {
|
|
842
|
+
lines.push(` Expected: ${issue.expected_value ?? "?"} | Actual: ${issue.actual_value ?? "?"}`);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
lines.push("");
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
// Changelog
|
|
849
|
+
if (changelogs.length > 0) {
|
|
850
|
+
lines.push("## Changelog\n");
|
|
851
|
+
for (const chg of changelogs) {
|
|
852
|
+
lines.push(`### [${chg.change_type}] ${chg.description}`);
|
|
853
|
+
if (chg.files_changed) {
|
|
854
|
+
const files = JSON.parse(chg.files_changed);
|
|
855
|
+
lines.push(`**Files changed:** ${files.join(", ")}`);
|
|
856
|
+
}
|
|
857
|
+
if (chg.git_commit)
|
|
858
|
+
lines.push(`**Commit:** ${chg.git_commit}`);
|
|
859
|
+
if (chg.before_screenshot_id || chg.after_screenshot_id) {
|
|
860
|
+
lines.push(`**Before:** ${chg.before_screenshot_id ?? "-"} | **After:** ${chg.after_screenshot_id ?? "-"}`);
|
|
861
|
+
}
|
|
862
|
+
lines.push("");
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
const markdown = lines.join("\n");
|
|
866
|
+
return {
|
|
867
|
+
format: "markdown",
|
|
868
|
+
walkthrough: format === "summary" ? markdown.slice(0, 3000) : markdown,
|
|
869
|
+
stats: {
|
|
870
|
+
routes: routeGroups.size,
|
|
871
|
+
components: components.length,
|
|
872
|
+
tests: tests.length,
|
|
873
|
+
bugs: bugs.length,
|
|
874
|
+
designIssues: designIssues.length,
|
|
875
|
+
screenshots: screenshots.length,
|
|
876
|
+
backendLinks: backendLinks.length,
|
|
877
|
+
changelogs: changelogs.length,
|
|
878
|
+
},
|
|
879
|
+
};
|
|
880
|
+
},
|
|
881
|
+
},
|
|
882
|
+
];
|
|
883
|
+
//# sourceMappingURL=uiUxDiveAdvancedTools.js.map
|