ghostrun-cli 1.0.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/mcp-server.js ADDED
@@ -0,0 +1,377 @@
1
+ #!/usr/bin/env node
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (let key of __getOwnPropNames(from))
11
+ if (!__hasOwnProp.call(to, key) && key !== except)
12
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
13
+ }
14
+ return to;
15
+ };
16
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
17
+ // If the importer is in node compatibility mode or this is not an ESM
18
+ // file that has been converted to a CommonJS file using a Babel-
19
+ // compatible transform (i.e. "__esModule" has not been set), then set
20
+ // "default" to the CommonJS "module.exports" for node compatibility.
21
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
+ mod
23
+ ));
24
+
25
+ // mcp-server.ts
26
+ var import_server = require("@modelcontextprotocol/sdk/server/index.js");
27
+ var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
28
+ var import_types = require("@modelcontextprotocol/sdk/types.js");
29
+ var import_child_process = require("child_process");
30
+ var import_better_sqlite3 = __toESM(require("better-sqlite3"));
31
+ var fs = __toESM(require("fs"));
32
+ var path = __toESM(require("path"));
33
+ var HOME_DIR = process.env.HOME || process.env.USERPROFILE || ".";
34
+ var DATA_PATH = path.join(HOME_DIR, ".ghostrun");
35
+ var DB_PATH = path.join(DATA_PATH, "data", "ghostrun.db");
36
+ var GHOSTRUN_BIN = path.join(__dirname, "ghostrun.js");
37
+ function openDb() {
38
+ if (!fs.existsSync(DB_PATH)) return null;
39
+ const db = new import_better_sqlite3.default(DB_PATH, { readonly: true });
40
+ db.pragma("foreign_keys = ON");
41
+ return db;
42
+ }
43
+ function mapFlow(r) {
44
+ return {
45
+ id: r.id,
46
+ name: r.name,
47
+ description: r.description,
48
+ appUrl: r.app_url,
49
+ graph: r.graph,
50
+ createdAt: r.created_at,
51
+ updatedAt: r.updated_at
52
+ };
53
+ }
54
+ function mapRun(r) {
55
+ return {
56
+ id: r.id,
57
+ flowId: r.flow_id,
58
+ status: r.status,
59
+ startedAt: r.started_at,
60
+ completedAt: r.completed_at,
61
+ duration: r.duration,
62
+ errorMessage: r.error_message,
63
+ summary: r.summary
64
+ };
65
+ }
66
+ function mapStep(r) {
67
+ return {
68
+ id: r.id,
69
+ runId: r.run_id,
70
+ stepNumber: r.step_number,
71
+ name: r.name,
72
+ action: r.action,
73
+ selector: r.selector,
74
+ value: r.value,
75
+ status: r.status,
76
+ duration: r.duration,
77
+ errorMessage: r.error_message,
78
+ screenshotPath: r.screenshot_path
79
+ };
80
+ }
81
+ function runFlowViaCli(flowId, vars) {
82
+ return new Promise((resolve, reject) => {
83
+ const varArgs = [];
84
+ if (vars) {
85
+ for (const [k, v] of Object.entries(vars)) varArgs.push("--var", `${k}=${v}`);
86
+ }
87
+ const proc = (0, import_child_process.spawn)("node", [GHOSTRUN_BIN, "run", flowId, "--output", "json", ...varArgs], {
88
+ env: { ...process.env },
89
+ stdio: ["ignore", "pipe", "pipe"]
90
+ });
91
+ let stdout = "";
92
+ let stderr = "";
93
+ proc.stdout.on("data", (d) => {
94
+ stdout += d.toString();
95
+ });
96
+ proc.stderr.on("data", (d) => {
97
+ stderr += d.toString();
98
+ });
99
+ proc.on("close", () => {
100
+ const jsonLine = stdout.split("\n").find((l) => l.trim().startsWith("{"));
101
+ if (jsonLine) {
102
+ try {
103
+ resolve(JSON.parse(jsonLine));
104
+ return;
105
+ } catch {
106
+ }
107
+ }
108
+ reject(new Error(`ghostrun run failed: ${stderr || stdout || "(no output)"}`));
109
+ });
110
+ proc.on("error", (err) => reject(new Error(`Failed to spawn ghostrun: ${err.message}`)));
111
+ });
112
+ }
113
+ var server = new import_server.Server(
114
+ { name: "ghostrun", version: "1.0.0" },
115
+ { capabilities: { tools: {} } }
116
+ );
117
+ server.setRequestHandler(import_types.ListToolsRequestSchema, async () => ({
118
+ tools: [
119
+ {
120
+ name: "list_flows",
121
+ description: "List all saved GhostRun flows with their IDs, names, step counts, and last updated date. Works for both browser automation flows and API test flows.",
122
+ inputSchema: { type: "object", properties: {} }
123
+ },
124
+ {
125
+ name: "get_flow",
126
+ description: "Get detailed information about a specific GhostRun flow, including all action steps with their selectors and values.",
127
+ inputSchema: {
128
+ type: "object",
129
+ properties: {
130
+ flowId: { type: "string", description: "Flow ID or first 8 characters of it" }
131
+ },
132
+ required: ["flowId"]
133
+ }
134
+ },
135
+ {
136
+ name: "run_flow",
137
+ description: "Execute a GhostRun flow. Handles browser automation flows (Playwright), pure API test flows (no browser needed), and hybrid flows. Returns run ID, pass/fail status, per-step results, extracted data, and error details.",
138
+ inputSchema: {
139
+ type: "object",
140
+ properties: {
141
+ flowId: { type: "string", description: "Flow ID or first 8 characters of it (also accepts flow name)" },
142
+ vars: {
143
+ type: "object",
144
+ description: 'Optional key=value variables to inject into the flow (e.g. { "username": "alice", "env": "staging" })',
145
+ additionalProperties: { type: "string" }
146
+ }
147
+ },
148
+ required: ["flowId"]
149
+ }
150
+ },
151
+ {
152
+ name: "get_run_result",
153
+ description: "Get detailed results of a previous flow run: step-by-step status, timing, error messages, extracted data, AI failure summary, and screenshot paths.",
154
+ inputSchema: {
155
+ type: "object",
156
+ properties: {
157
+ runId: { type: "string", description: "Run ID or first 8 characters of it" }
158
+ },
159
+ required: ["runId"]
160
+ }
161
+ },
162
+ {
163
+ name: "list_runs",
164
+ description: "List recent flow runs with status, duration, and flow name. Optionally filter by flow ID.",
165
+ inputSchema: {
166
+ type: "object",
167
+ properties: {
168
+ flowId: { type: "string", description: "Optional: filter runs to a specific flow" },
169
+ limit: { type: "number", description: "Max results to return (default 20)" }
170
+ }
171
+ }
172
+ },
173
+ {
174
+ name: "delete_flow",
175
+ description: "Delete a GhostRun flow and all its run history.",
176
+ inputSchema: {
177
+ type: "object",
178
+ properties: {
179
+ flowId: { type: "string", description: "Flow ID or first 8 characters of it" }
180
+ },
181
+ required: ["flowId"]
182
+ }
183
+ },
184
+ {
185
+ name: "get_status",
186
+ description: "Get GhostRun system statistics: total flows, total runs, pass/fail counts, success rate, and data path.",
187
+ inputSchema: { type: "object", properties: {} }
188
+ }
189
+ ]
190
+ }));
191
+ server.setRequestHandler(import_types.CallToolRequestSchema, async (request) => {
192
+ const { name, arguments: toolArgs } = request.params;
193
+ try {
194
+ switch (name) {
195
+ case "list_flows": {
196
+ const db = openDb();
197
+ if (!db) return text("No flows yet. Record your first flow with: ghostrun learn <url>");
198
+ const flows = db.prepare("SELECT * FROM flows ORDER BY updated_at DESC").all().map(mapFlow);
199
+ db.close();
200
+ if (flows.length === 0) return text("No flows saved yet. Use `ghostrun learn <url>` to record your first flow.");
201
+ const rows = flows.map((f) => {
202
+ let stepCount = 0;
203
+ let isApiFlow = false;
204
+ try {
205
+ const nodes = (JSON.parse(f.graph).nodes || []).filter((n) => n.type === "action");
206
+ stepCount = nodes.length;
207
+ const apiActions = /* @__PURE__ */ new Set(["http:request", "assert:response", "set:variable", "extract:json", "env:switch"]);
208
+ isApiFlow = nodes.length > 0 && nodes.every((n) => apiActions.has(n.action));
209
+ } catch {
210
+ }
211
+ return { id: f.id.slice(0, 8), name: f.name, description: f.description || "", steps: stepCount, type: isApiFlow ? "api" : "browser", url: f.appUrl || "", updated: f.updatedAt };
212
+ });
213
+ return text(JSON.stringify({ total: flows.length, flows: rows }, null, 2));
214
+ }
215
+ case "get_flow": {
216
+ const { flowId } = toolArgs;
217
+ const db = openDb();
218
+ if (!db) return error("No database found. Run `ghostrun init` first.");
219
+ const row = db.prepare("SELECT * FROM flows WHERE id LIKE ? OR name = ?").get(flowId + "%", flowId);
220
+ db.close();
221
+ if (!row) return error(`Flow not found: ${flowId}`);
222
+ const flow = mapFlow(row);
223
+ const graph = JSON.parse(flow.graph);
224
+ const actionNodes = (graph.nodes || []).filter((n) => n.type === "action");
225
+ return text(JSON.stringify({
226
+ id: flow.id,
227
+ name: flow.name,
228
+ description: flow.description,
229
+ appUrl: flow.appUrl,
230
+ createdAt: flow.createdAt,
231
+ updatedAt: flow.updatedAt,
232
+ stepCount: actionNodes.length,
233
+ steps: actionNodes.map((n, i) => ({
234
+ step: i + 1,
235
+ label: n.label,
236
+ action: n.action,
237
+ selector: n.selector || null,
238
+ value: n.value ? "***" : null,
239
+ url: n.url || null
240
+ }))
241
+ }, null, 2));
242
+ }
243
+ case "run_flow": {
244
+ const { flowId, vars } = toolArgs;
245
+ let result;
246
+ try {
247
+ result = await runFlowViaCli(flowId, vars);
248
+ } catch (err) {
249
+ return error(err instanceof Error ? err.message : String(err));
250
+ }
251
+ return text(JSON.stringify({
252
+ runId: result.runId,
253
+ runIdShort: result.runId?.slice(0, 8),
254
+ status: result.passed ? "passed" : "failed",
255
+ flowName: result.flowName,
256
+ duration: result.duration ? `${result.duration}ms` : null,
257
+ stepsTotal: result.steps?.length ?? 0,
258
+ stepsPassed: result.steps?.filter((s) => s.status === "passed").length ?? 0,
259
+ stepsFailed: result.steps?.filter((s) => s.status === "failed").length ?? 0,
260
+ extractedData: result.extractedData || {},
261
+ steps: result.steps,
262
+ errorMessage: result.steps?.find((s) => s.errorMessage)?.errorMessage ?? null,
263
+ hint: !result.passed ? `Run failed. Use get_run_result with runId "${result.runId?.slice(0, 8)}" for details.` : "All steps passed."
264
+ }, null, 2));
265
+ }
266
+ case "get_run_result": {
267
+ const { runId } = toolArgs;
268
+ const db = openDb();
269
+ if (!db) return error("No database found.");
270
+ const runRow = db.prepare("SELECT * FROM runs WHERE id LIKE ?").get(runId + "%");
271
+ if (!runRow) {
272
+ db.close();
273
+ return error(`Run not found: ${runId}`);
274
+ }
275
+ const run = mapRun(runRow);
276
+ const flowRow = db.prepare("SELECT * FROM flows WHERE id = ?").get(run.flowId);
277
+ const flow = flowRow ? mapFlow(flowRow) : null;
278
+ const steps = db.prepare("SELECT * FROM steps WHERE run_id = ? ORDER BY step_number").all(run.id).map(mapStep);
279
+ db.close();
280
+ return text(JSON.stringify({
281
+ runId: run.id,
282
+ flowName: flow?.name || "Unknown",
283
+ status: run.status,
284
+ startedAt: run.startedAt,
285
+ completedAt: run.completedAt,
286
+ duration: run.duration ? `${run.duration}ms` : null,
287
+ errorMessage: run.errorMessage,
288
+ aiSummary: run.summary || null,
289
+ steps: steps.map((s) => ({
290
+ step: s.stepNumber,
291
+ name: s.name,
292
+ action: s.action,
293
+ selector: s.selector,
294
+ status: s.status,
295
+ duration: s.duration ? `${s.duration}ms` : null,
296
+ errorMessage: s.errorMessage,
297
+ screenshotPath: s.screenshotPath
298
+ }))
299
+ }, null, 2));
300
+ }
301
+ case "list_runs": {
302
+ const { flowId, limit = 20 } = toolArgs || {};
303
+ const db = openDb();
304
+ if (!db) return text('{ "runs": [] }');
305
+ const sql = flowId ? "SELECT r.*, f.name as flow_name FROM runs r LEFT JOIN flows f ON r.flow_id = f.id WHERE r.flow_id LIKE ? ORDER BY r.started_at DESC LIMIT ?" : "SELECT r.*, f.name as flow_name FROM runs r LEFT JOIN flows f ON r.flow_id = f.id ORDER BY r.started_at DESC LIMIT ?";
306
+ const params = flowId ? [flowId + "%", limit] : [limit];
307
+ const rows = db.prepare(sql).all(...params);
308
+ db.close();
309
+ return text(JSON.stringify({
310
+ total: rows.length,
311
+ runs: rows.map((r) => ({
312
+ id: r.id.slice(0, 8),
313
+ flowName: r.flow_name || "Unknown",
314
+ status: r.status,
315
+ startedAt: r.started_at,
316
+ duration: r.duration ? `${r.duration}ms` : null,
317
+ hasAiSummary: !!r.summary
318
+ }))
319
+ }, null, 2));
320
+ }
321
+ case "delete_flow": {
322
+ const { flowId } = toolArgs;
323
+ const db = openDb();
324
+ if (!db) return error("No database found.");
325
+ const row = db.prepare("SELECT * FROM flows WHERE id LIKE ? OR name = ?").get(flowId + "%", flowId);
326
+ if (!row) {
327
+ db.close();
328
+ return error(`Flow not found: ${flowId}`);
329
+ }
330
+ const flow = mapFlow(row);
331
+ const writeDb = new import_better_sqlite3.default(DB_PATH);
332
+ writeDb.prepare("DELETE FROM flows WHERE id = ?").run(flow.id);
333
+ writeDb.close();
334
+ db.close();
335
+ return text(`Deleted flow "${flow.name}" (${flow.id.slice(0, 8)})`);
336
+ }
337
+ case "get_status": {
338
+ const db = openDb();
339
+ if (!db) return text(JSON.stringify({ flows: 0, totalRuns: 0, passed: 0, failed: 0, successRate: "N/A", dataPath: DATA_PATH, aiEnabled: !!process.env.ANTHROPIC_API_KEY }, null, 2));
340
+ const flowCount = db.prepare("SELECT COUNT(*) as c FROM flows").get().c;
341
+ const runCount = db.prepare("SELECT COUNT(*) as c FROM runs").get().c;
342
+ const passedCount = db.prepare("SELECT COUNT(*) as c FROM runs WHERE status = 'passed'").get().c;
343
+ const failedCount = db.prepare("SELECT COUNT(*) as c FROM runs WHERE status = 'failed'").get().c;
344
+ db.close();
345
+ return text(JSON.stringify({
346
+ flows: flowCount,
347
+ totalRuns: runCount,
348
+ passed: passedCount,
349
+ failed: failedCount,
350
+ successRate: runCount > 0 ? `${Math.round(passedCount / runCount * 100)}%` : "N/A",
351
+ dataPath: DATA_PATH,
352
+ aiEnabled: !!process.env.ANTHROPIC_API_KEY
353
+ }, null, 2));
354
+ }
355
+ default:
356
+ return error(`Unknown tool: ${name}`);
357
+ }
358
+ } catch (err) {
359
+ return error(err instanceof Error ? err.message : String(err));
360
+ }
361
+ });
362
+ function text(content) {
363
+ return { content: [{ type: "text", text: content }] };
364
+ }
365
+ function error(message) {
366
+ return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
367
+ }
368
+ async function main() {
369
+ const transport = new import_stdio.StdioServerTransport();
370
+ await server.connect(transport);
371
+ process.stderr.write("GhostRun MCP Server running. Connect via Claude Desktop or any MCP client.\n");
372
+ }
373
+ main().catch((err) => {
374
+ process.stderr.write(`Fatal: ${err}
375
+ `);
376
+ process.exit(1);
377
+ });
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "ghostrun-cli",
3
+ "version": "1.0.0",
4
+ "description": "Browser automation + API testing + load testing in one tool. Record flows, test REST APIs, run VU-based load tests, export to k6. Entirely local.",
5
+ "main": "ghostrun.js",
6
+ "bin": {
7
+ "ghostrun": "ghostrun.js",
8
+ "ghostrun-mcp": "mcp-server.js"
9
+ },
10
+ "keywords": [
11
+ "browser-automation", "api-testing", "load-testing", "playwright",
12
+ "test-runner", "cli", "k6", "local-first", "mcp", "e2e-testing"
13
+ ],
14
+ "homepage": "https://ghostrun.builtbysharan.com",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/builtbysharan/ghostrun"
18
+ },
19
+ "license": "MIT",
20
+ "scripts": {
21
+ "start": "node ghostrun.js",
22
+ "cli": "node ghostrun.js",
23
+ "mcp": "npx tsx mcp-server.ts",
24
+ "build": "npx esbuild ghostrun.ts --platform=node --format=cjs --outfile=ghostrun.js --bundle --external:playwright --external:better-sqlite3 --external:chalk --external:uuid --external:@anthropic-ai/sdk --external:@clack/prompts && npx esbuild mcp-server.ts --platform=node --format=cjs --outfile=mcp-server.js --bundle --external:playwright --external:better-sqlite3 --external:uuid --external:@anthropic-ai/sdk --external:@modelcontextprotocol/sdk",
25
+ "test": "./TEST-DEMO.sh",
26
+ "demo": "./TEST-DEMO.sh"
27
+ },
28
+ "dependencies": {
29
+ "@anthropic-ai/sdk": "^0.37.0",
30
+ "@clack/prompts": "^1.2.0",
31
+ "@modelcontextprotocol/sdk": "^1.0.0",
32
+ "better-sqlite3": "^9.4.3",
33
+ "chalk": "^4.1.2",
34
+ "node-cron": "^4.2.1",
35
+ "pixelmatch": "^7.1.0",
36
+ "playwright": "^1.41.0",
37
+ "pngjs": "^7.0.0",
38
+ "uuid": "^9.0.1"
39
+ },
40
+ "engines": {
41
+ "node": ">=18.0.0"
42
+ },
43
+ "devDependencies": {
44
+ "@types/better-sqlite3": "^7.6.8",
45
+ "@types/node": "^20.11.0",
46
+ "@types/node-cron": "^3.0.11",
47
+ "@types/pngjs": "^6.0.5",
48
+ "@types/uuid": "^9.0.7",
49
+ "electron": "^41.2.0",
50
+ "tsx": "^4.21.0",
51
+ "typescript": "^5.3.3"
52
+ }
53
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "version": "1.0.0",
3
+ "template": true,
4
+ "name": "API Health Check",
5
+ "description": "Hit the /api/health or /api/status endpoint and assert it returns OK",
6
+ "tags": ["api", "health", "monitoring", "ci"],
7
+ "variables": ["BASE_URL"],
8
+ "flow": {
9
+ "name": "API Health Check",
10
+ "description": "Verify API health endpoint returns OK status",
11
+ "appUrl": "{{BASE_URL}}",
12
+ "graph": {
13
+ "nodes": [
14
+ { "id": "start", "type": "start", "label": "Start", "url": "{{BASE_URL}}" },
15
+ { "id": "step-1", "type": "action", "label": "Navigate to API health endpoint", "action": "navigate", "url": "{{BASE_URL}}/api/health" },
16
+ { "id": "step-2", "type": "action", "label": "Assert OK response", "action": "assert:text", "value": "ok" },
17
+ { "id": "end", "type": "end", "label": "End" }
18
+ ],
19
+ "edges": [
20
+ { "id": "e0", "source": "start", "target": "step-1" },
21
+ { "id": "e1", "source": "step-1", "target": "step-2" },
22
+ { "id": "e2", "source": "step-2", "target": "end" }
23
+ ]
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,38 @@
1
+ {
2
+ "version": "1.0.0",
3
+ "template": true,
4
+ "name": "Checkout Guest",
5
+ "description": "Add a product to cart and complete guest checkout flow",
6
+ "tags": ["ecommerce", "checkout", "cart"],
7
+ "variables": ["BASE_URL", "EMAIL", "FIRST_NAME", "LAST_NAME"],
8
+ "flow": {
9
+ "name": "Checkout Guest",
10
+ "description": "Add to cart → guest checkout → assert order confirmation",
11
+ "appUrl": "{{BASE_URL}}",
12
+ "graph": {
13
+ "nodes": [
14
+ { "id": "start", "type": "start", "label": "Start", "url": "{{BASE_URL}}" },
15
+ { "id": "step-1", "type": "action", "label": "Navigate to products", "action": "navigate", "url": "{{BASE_URL}}/products" },
16
+ { "id": "step-2", "type": "action", "label": "Click first product", "action": "click", "selector": ".product-card:first-child, .product:first-child a, [data-testid='product']:first-child" },
17
+ { "id": "step-3", "type": "action", "label": "Add to cart", "action": "click", "selector": "button:has-text('Add to cart'), .add-to-cart, #add-to-cart" },
18
+ { "id": "step-4", "type": "action", "label": "View cart", "action": "navigate", "url": "{{BASE_URL}}/cart" },
19
+ { "id": "step-5", "type": "action", "label": "Proceed to checkout", "action": "click", "selector": "button:has-text('Checkout'), a:has-text('Checkout'), .checkout-btn" },
20
+ { "id": "step-6", "type": "action", "label": "Fill email", "action": "fill", "selector": "input[type='email'], input[name='email'], #email", "value": "{{EMAIL}}" },
21
+ { "id": "step-7", "type": "action", "label": "Continue as guest", "action": "click", "selector": "button:has-text('Continue'), button:has-text('Guest'), .guest-checkout" },
22
+ { "id": "step-8", "type": "action", "label": "Assert on checkout page", "action": "assert:url", "value": "/checkout" },
23
+ { "id": "end", "type": "end", "label": "End" }
24
+ ],
25
+ "edges": [
26
+ { "id": "e0", "source": "start", "target": "step-1" },
27
+ { "id": "e1", "source": "step-1", "target": "step-2" },
28
+ { "id": "e2", "source": "step-2", "target": "step-3" },
29
+ { "id": "e3", "source": "step-3", "target": "step-4" },
30
+ { "id": "e4", "source": "step-4", "target": "step-5" },
31
+ { "id": "e5", "source": "step-5", "target": "step-6" },
32
+ { "id": "e6", "source": "step-6", "target": "step-7" },
33
+ { "id": "e7", "source": "step-7", "target": "step-8" },
34
+ { "id": "e8", "source": "step-8", "target": "end" }
35
+ ]
36
+ }
37
+ }
38
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "version": "1.0.0",
3
+ "template": true,
4
+ "name": "Contact Form",
5
+ "description": "Fill and submit a contact form, assert success message",
6
+ "tags": ["forms", "contact", "marketing"],
7
+ "variables": ["BASE_URL", "NAME", "EMAIL", "MESSAGE"],
8
+ "flow": {
9
+ "name": "Contact Form",
10
+ "description": "Fill contact form and verify submission",
11
+ "appUrl": "{{BASE_URL}}",
12
+ "graph": {
13
+ "nodes": [
14
+ { "id": "start", "type": "start", "label": "Start", "url": "{{BASE_URL}}" },
15
+ { "id": "step-1", "type": "action", "label": "Navigate to contact page", "action": "navigate", "url": "{{BASE_URL}}/contact" },
16
+ { "id": "step-2", "type": "action", "label": "Fill name", "action": "fill", "selector": "input[name='name'], #name, input[placeholder*='name' i]", "value": "{{NAME}}" },
17
+ { "id": "step-3", "type": "action", "label": "Fill email", "action": "fill", "selector": "input[type='email'], input[name='email'], #email", "value": "{{EMAIL}}" },
18
+ { "id": "step-4", "type": "action", "label": "Fill message", "action": "fill", "selector": "textarea, textarea[name='message'], #message", "value": "{{MESSAGE}}" },
19
+ { "id": "step-5", "type": "action", "label": "Submit form", "action": "click", "selector": "button[type='submit'], input[type='submit'], .submit-btn" },
20
+ { "id": "step-6", "type": "action", "label": "Assert success message", "action": "assert:text", "value": "Thank you" },
21
+ { "id": "end", "type": "end", "label": "End" }
22
+ ],
23
+ "edges": [
24
+ { "id": "e0", "source": "start", "target": "step-1" },
25
+ { "id": "e1", "source": "step-1", "target": "step-2" },
26
+ { "id": "e2", "source": "step-2", "target": "step-3" },
27
+ { "id": "e3", "source": "step-3", "target": "step-4" },
28
+ { "id": "e4", "source": "step-4", "target": "step-5" },
29
+ { "id": "e5", "source": "step-5", "target": "step-6" },
30
+ { "id": "e6", "source": "step-6", "target": "end" }
31
+ ]
32
+ }
33
+ }
34
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "version": "1.0.0",
3
+ "template": true,
4
+ "name": "Login Basic",
5
+ "description": "Navigate to login page, fill credentials, submit and assert redirect to dashboard",
6
+ "tags": ["auth", "login", "starter"],
7
+ "variables": ["BASE_URL", "EMAIL", "PASSWORD"],
8
+ "flow": {
9
+ "name": "Login Basic",
10
+ "description": "Standard login flow — fill email + password, submit, assert dashboard",
11
+ "appUrl": "{{BASE_URL}}",
12
+ "graph": {
13
+ "nodes": [
14
+ { "id": "start", "type": "start", "label": "Start", "url": "{{BASE_URL}}" },
15
+ { "id": "step-1", "type": "action", "label": "Navigate to login page", "action": "navigate", "url": "{{BASE_URL}}/login" },
16
+ { "id": "step-2", "type": "action", "label": "Fill email", "action": "fill", "selector": "input[type='email'], input[name='email'], #email", "value": "{{EMAIL}}" },
17
+ { "id": "step-3", "type": "action", "label": "Fill password", "action": "fill", "selector": "input[type='password'], input[name='password'], #password", "value": "{{PASSWORD}}" },
18
+ { "id": "step-4", "type": "action", "label": "Click Sign In", "action": "click", "selector": "button[type='submit'], input[type='submit'], .login-btn, #login-btn" },
19
+ { "id": "step-5", "type": "action", "label": "Assert redirected away from /login", "action": "assert:url", "value": "/dashboard" },
20
+ { "id": "end", "type": "end", "label": "End" }
21
+ ],
22
+ "edges": [
23
+ { "id": "e0", "source": "start", "target": "step-1" },
24
+ { "id": "e1", "source": "step-1", "target": "step-2" },
25
+ { "id": "e2", "source": "step-2", "target": "step-3" },
26
+ { "id": "e3", "source": "step-3", "target": "step-4" },
27
+ { "id": "e4", "source": "step-4", "target": "step-5" },
28
+ { "id": "e5", "source": "step-5", "target": "end" }
29
+ ]
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "version": "1.0.0",
3
+ "template": true,
4
+ "name": "Logout",
5
+ "description": "Click logout button or link and assert redirect to login/home page",
6
+ "tags": ["auth", "logout", "smoke"],
7
+ "variables": ["BASE_URL"],
8
+ "flow": {
9
+ "name": "Logout",
10
+ "description": "Logout and verify session ended",
11
+ "appUrl": "{{BASE_URL}}",
12
+ "graph": {
13
+ "nodes": [
14
+ { "id": "start", "type": "start", "label": "Start", "url": "{{BASE_URL}}" },
15
+ { "id": "step-1", "type": "action", "label": "Navigate to dashboard", "action": "navigate", "url": "{{BASE_URL}}/dashboard" },
16
+ { "id": "step-2", "type": "action", "label": "Click logout", "action": "click", "selector": "a[href*='logout'], button:has-text('Logout'), button:has-text('Sign out'), .logout-btn" },
17
+ { "id": "step-3", "type": "action", "label": "Assert redirected to login", "action": "assert:url", "value": "/login" },
18
+ { "id": "end", "type": "end", "label": "End" }
19
+ ],
20
+ "edges": [
21
+ { "id": "e0", "source": "start", "target": "step-1" },
22
+ { "id": "e1", "source": "step-1", "target": "step-2" },
23
+ { "id": "e2", "source": "step-2", "target": "step-3" },
24
+ { "id": "e3", "source": "step-3", "target": "end" }
25
+ ]
26
+ }
27
+ }
28
+ }
@@ -0,0 +1,38 @@
1
+ {
2
+ "version": "1.0.0",
3
+ "template": true,
4
+ "name": "Navigation Smoke",
5
+ "description": "Visit key pages (home, about, pricing, contact) and assert each loads without error",
6
+ "tags": ["smoke", "navigation", "ci"],
7
+ "variables": ["BASE_URL"],
8
+ "flow": {
9
+ "name": "Navigation Smoke",
10
+ "description": "Visit home, about, pricing, contact — assert no 404s",
11
+ "appUrl": "{{BASE_URL}}",
12
+ "graph": {
13
+ "nodes": [
14
+ { "id": "start", "type": "start", "label": "Start", "url": "{{BASE_URL}}" },
15
+ { "id": "step-1", "type": "action", "label": "Visit homepage", "action": "navigate", "url": "{{BASE_URL}}" },
16
+ { "id": "step-2", "type": "action", "label": "Assert homepage loaded", "action": "assert:element", "selector": "body" },
17
+ { "id": "step-3", "type": "action", "label": "Visit /about", "action": "navigate", "url": "{{BASE_URL}}/about" },
18
+ { "id": "step-4", "type": "action", "label": "Assert about page loaded", "action": "assert:element", "selector": "body" },
19
+ { "id": "step-5", "type": "action", "label": "Visit /pricing", "action": "navigate", "url": "{{BASE_URL}}/pricing" },
20
+ { "id": "step-6", "type": "action", "label": "Assert pricing page loaded", "action": "assert:element", "selector": "body" },
21
+ { "id": "step-7", "type": "action", "label": "Visit /contact", "action": "navigate", "url": "{{BASE_URL}}/contact" },
22
+ { "id": "step-8", "type": "action", "label": "Assert contact page loaded", "action": "assert:element", "selector": "body" },
23
+ { "id": "end", "type": "end", "label": "End" }
24
+ ],
25
+ "edges": [
26
+ { "id": "e0", "source": "start", "target": "step-1" },
27
+ { "id": "e1", "source": "step-1", "target": "step-2" },
28
+ { "id": "e2", "source": "step-2", "target": "step-3" },
29
+ { "id": "e3", "source": "step-3", "target": "step-4" },
30
+ { "id": "e4", "source": "step-4", "target": "step-5" },
31
+ { "id": "e5", "source": "step-5", "target": "step-6" },
32
+ { "id": "e6", "source": "step-6", "target": "step-7" },
33
+ { "id": "e7", "source": "step-7", "target": "step-8" },
34
+ { "id": "e8", "source": "step-8", "target": "end" }
35
+ ]
36
+ }
37
+ }
38
+ }