flowstride 1.0.8 → 1.0.10

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/README.md CHANGED
@@ -24,13 +24,13 @@ Flowstride requires Node.js 18 or higher.
24
24
  **1. Install globally (or as a dev dependency in your project):**
25
25
 
26
26
  ```bash
27
- npm install -g flowstride
27
+ npm i flowstride --save-dev
28
28
  ```
29
29
 
30
30
  **2. Initialize a new Flowstride workspace:**
31
31
 
32
32
  ```bash
33
- flowstride init
33
+ npx flowstride init
34
34
  ```
35
35
 
36
36
  _(This creates the default directory structure (`/flows`, `/reports`, `/plugins`) and generates a `example.flow` sample file.)_
@@ -38,15 +38,23 @@ _(This creates the default directory structure (`/flows`, `/reports`, `/plugins`
38
38
  **3. Run your first test:**
39
39
 
40
40
  ```bash
41
- flowstride run
41
+ npx flowstride run
42
42
 
43
43
  OR
44
44
 
45
- flowstride run flows/example.flow
45
+ npx flowstride run flows/example.flow
46
46
  ```
47
47
 
48
48
  _(This command executes your test while instantly booting up the local Flowstride Dashboard.)_
49
49
 
50
+ **4. Login to Flowstride Cloud:**
51
+
52
+ Authenticate your local project with your Cloud account to unlock Cloud features after signing up at [cloud.flowstride.io](https://cloud.flowstride.io).
53
+
54
+ ```bash
55
+ npx flowstride login
56
+ ```
57
+
50
58
  ---
51
59
 
52
60
  ## Privacy & Data Security
package/dist/cli/index.js CHANGED
@@ -53,7 +53,7 @@ const API_BASE = process.env.FLOWSTRIDE_API_URL || "https://cloud.flowstride.io"
53
53
  program
54
54
  .name("flowstride")
55
55
  .description("Flowstride: The Enterprise Flow-First Automation CLI")
56
- .version("1.0.8");
56
+ .version("1.0.10");
57
57
  program
58
58
  .command("init")
59
59
  .description("Initialise a new Flowstride workspace in the current directory")
@@ -158,11 +158,11 @@ program
158
158
  }
159
159
  catch (installError) {
160
160
  installSpinner.stop();
161
+ console.error(chalk_1.default.bgRed.white("\n[RAW SYSTEM ERROR]:"), installError);
161
162
  console.log(chalk_1.default.red("\n✖ Failed to install the Enterprise Engine. Please check your internet connection or verify your project configuration."));
162
163
  if (fs.existsSync(nodeModulesPath)) {
163
164
  fs.rmSync(nodeModulesPath, { recursive: true, force: true });
164
165
  }
165
- // THE FIX: Strict Rollback on Failure
166
166
  console.log(chalk_1.default.yellow("⚠️ Reverting login: Enterprise Engine is required to sync."));
167
167
  if (fs.existsSync(credentialsPath))
168
168
  fs.unlinkSync(credentialsPath);
@@ -175,7 +175,6 @@ program
175
175
  }
176
176
  }
177
177
  else {
178
- // THE FIX: Strict Rollback on Decline
179
178
  console.log(chalk_1.default.yellow(`\n⚠️ Installation skipped.`));
180
179
  console.log(chalk_1.default.red(`✖ Reverting login: You must install the Enterprise Engine to sync this workspace.`));
181
180
  if (fs.existsSync(credentialsPath))
@@ -184,7 +183,6 @@ program
184
183
  }
185
184
  }
186
185
  }
187
- // Only print this if the engine was successfully verified or installed
188
186
  console.log(chalk_1.default.bold.green(`\n✅ Machine synchronized with Flowstride Enterprise.`));
189
187
  }
190
188
  catch (error) {
@@ -206,7 +204,6 @@ program
206
204
  }
207
205
  catch (err) { }
208
206
  }
209
- // THE FIX: Defensive Self-Healing. If login exists but engine is missing, drop to OS.
210
207
  if (!process.env.FLOWSTRIDE_API_KEY &&
211
208
  token &&
212
209
  !fs.existsSync(nodeModulesPath)) {
@@ -247,13 +244,17 @@ program
247
244
  }
248
245
  });
249
246
  program
250
- .command("run")
247
+ .command("run [targetPath]")
251
248
  .description("Execute all .flow automation journeys in the ./flows directory")
252
249
  .option("-b, --bail", "Stop execution on first failure", false)
253
250
  .option("--headed", "Run browser in headed mode", false)
254
251
  .option("--headless <boolean>", "Run browser in headless mode", (val) => val === "true", true)
255
252
  .option("--workers <number>", "Number of parallel workers to spawn (Pro feature)")
256
- .action(async (options) => {
253
+ .action(async (targetPath, options) => {
254
+ if (typeof targetPath === "object") {
255
+ options = targetPath;
256
+ targetPath = undefined;
257
+ }
257
258
  let token = process.env.FLOWSTRIDE_API_KEY || null;
258
259
  let forceOS = false;
259
260
  const credentialsPath = path.join(process.cwd(), ".flowstride", "login", "login.json");
@@ -265,7 +266,6 @@ program
265
266
  }
266
267
  catch (err) { }
267
268
  }
268
- // THE FIX: Defensive Self-Healing
269
269
  if (!process.env.FLOWSTRIDE_API_KEY &&
270
270
  token &&
271
271
  !fs.existsSync(nodeModulesPath)) {
@@ -310,10 +310,11 @@ program
310
310
  if (options.headed) {
311
311
  options.headless = false;
312
312
  }
313
- const strictUiFlowsDir = path.resolve(process.cwd(), "./flows");
313
+ const resolvedTargetPath = targetPath ? targetPath : "./flows";
314
+ const strictUiFlowsDir = path.resolve(process.cwd(), resolvedTargetPath);
314
315
  if (!fs.existsSync(strictUiFlowsDir)) {
315
316
  console.error(`\n[ERROR]: Could not find ${strictUiFlowsDir}.`);
316
- console.error(`Flowstride requires all UI tests to be inside a './flows' directory.`);
317
+ console.error(`Flowstride requires all UI tests to be inside a valid directory.`);
317
318
  process.exit(1);
318
319
  }
319
320
  // Inject our DRM state into options before passing to runner
@@ -0,0 +1,4 @@
1
+ export declare class PdfGenerator {
2
+ generateTestcasePdf(data: any, customOutputPath?: string): Promise<string>;
3
+ private generateReportHtml;
4
+ }
@@ -0,0 +1,187 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.PdfGenerator = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const playwright_1 = require("playwright");
40
+ class PdfGenerator {
41
+ async generateTestcasePdf(data, customOutputPath) {
42
+ let outputDir = "";
43
+ if (customOutputPath) {
44
+ outputDir = customOutputPath;
45
+ if (!fs.existsSync(outputDir)) {
46
+ fs.mkdirSync(outputDir, { recursive: true });
47
+ }
48
+ }
49
+ else {
50
+ outputDir = path.join(process.cwd(), ".flowstride", "artifacts");
51
+ if (!fs.existsSync(outputDir)) {
52
+ fs.mkdirSync(outputDir, { recursive: true });
53
+ }
54
+ }
55
+ const fileName = `Flowstride_Test_Log_${Date.now()}.pdf`;
56
+ const pdfPath = path.join(outputDir, fileName);
57
+ const htmlContent = this.generateReportHtml(data);
58
+ const browser = await playwright_1.chromium.launch({ headless: true });
59
+ const page = await browser.newPage();
60
+ await page.setContent(htmlContent, { waitUntil: "networkidle" });
61
+ await page.pdf({
62
+ path: pdfPath,
63
+ format: "A4",
64
+ printBackground: true,
65
+ margin: { top: "30px", right: "30px", bottom: "30px", left: "30px" },
66
+ });
67
+ await browser.close();
68
+ return pdfPath;
69
+ }
70
+ generateReportHtml(data) {
71
+ let scenariosToProcess = [];
72
+ if (data && data.files && Array.isArray(data.files)) {
73
+ data.files.forEach((fileObj) => {
74
+ if (fileObj.scenarios && Array.isArray(fileObj.scenarios)) {
75
+ scenariosToProcess.push(...fileObj.scenarios);
76
+ }
77
+ });
78
+ }
79
+ else if (Array.isArray(data)) {
80
+ scenariosToProcess = data;
81
+ }
82
+ const groupedScenarios = {};
83
+ scenariosToProcess.forEach((scenario) => {
84
+ const key = scenario.fileName || "General";
85
+ if (!groupedScenarios[key])
86
+ groupedScenarios[key] = [];
87
+ groupedScenarios[key].push(scenario);
88
+ });
89
+ let totalSteps = 0;
90
+ let passedSteps = 0;
91
+ let failedSteps = 0;
92
+ let tablesHtml = "";
93
+ for (const [fileName, fileScenarios] of Object.entries(groupedScenarios)) {
94
+ const flatSteps = fileScenarios.flatMap((s) => s.steps || []);
95
+ let fileRows = "";
96
+ flatSteps.forEach((step, idx) => {
97
+ totalSteps++;
98
+ const status = (step.status || "PENDING").toUpperCase();
99
+ const isSuccess = status === "PASS" || status === "PASSED" || status === "SUCCESS";
100
+ const isFail = status === "FAILED" || status === "FAIL";
101
+ if (isSuccess)
102
+ passedSteps++;
103
+ else if (isFail)
104
+ failedSteps++;
105
+ const color = isSuccess ? "#10b981" : isFail ? "#ef4444" : "#64748b";
106
+ const actualResultText = isFail
107
+ ? "Validation Failed"
108
+ : "Action verified successfully.";
109
+ const actualResultColor = isFail ? "#ef4444" : "#64748b";
110
+ fileRows += `
111
+ <tr>
112
+ <td style="padding: 16px 14px; border-bottom: 1px solid #e2e8f0; color: #64748b; font-weight: 500; width: 50px;">${idx + 1}</td>
113
+ <td style="padding: 16px 14px; border-bottom: 1px solid #e2e8f0; font-weight: 700; color: #1e293b;">${step.command || "action"}</td>
114
+ <td style="padding: 16px 14px; border-bottom: 1px solid #e2e8f0; color: #475569; font-style: italic;">${step.type || ""} ${step.description || "-"}</td>
115
+ <td style="padding: 16px 14px; border-bottom: 1px solid #e2e8f0; color: ${actualResultColor};">${actualResultText}</td>
116
+ <td style="padding: 16px 14px; border-bottom: 1px solid #e2e8f0; font-weight: 900; font-size: 10px; color: ${color};">${status}</td>
117
+ </tr>
118
+ `;
119
+ });
120
+ tablesHtml += `
121
+ <div class="file-separator">
122
+ <span>📄 ${fileName.toUpperCase()}</span>
123
+ </div>
124
+ <table>
125
+ <thead>
126
+ <tr>
127
+ <th>S/N</th>
128
+ <th>Action</th>
129
+ <th>Expected Result</th>
130
+ <th>Actual Result</th>
131
+ <th>Status</th>
132
+ </tr>
133
+ </thead>
134
+ <tbody>
135
+ ${fileRows || '<tr><td colspan="5" style="text-align:center;padding:20px;color:#64748b;">No test steps recorded.</td></tr>'}
136
+ </tbody>
137
+ </table>
138
+ `;
139
+ }
140
+ return `
141
+ <!DOCTYPE html>
142
+ <html>
143
+ <head>
144
+ <style>
145
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; padding: 20px; color: #1e293b; }
146
+ .header { border-bottom: 4px solid #0f172a; padding-bottom: 24px; margin-bottom: 40px; }
147
+ .title { font-size: 36px; font-weight: 900; text-transform: uppercase; margin: 0 0 8px 0; color: #0f172a; }
148
+ .subtitle { font-size: 14px; color: #64748b; font-weight: 500; margin: 0; }
149
+ .summary { display: flex; gap: 15px; margin-bottom: 20px; }
150
+ .stat-box { padding: 15px; border-radius: 8px; background: #f8fafc; flex: 1; text-align: center; border: 1px solid #e2e8f0; }
151
+ .stat-value { font-size: 22px; font-weight: bold; }
152
+
153
+ .file-separator { text-align: center; margin: 50px 0 20px 0; border-bottom: 1px solid rgba(124, 58, 237, 0.3); line-height: 0.1em; }
154
+ .file-separator span { background: #fff; padding: 0 15px; color: #7c3aed; font-weight: 900; font-size: 11px; letter-spacing: 0.1em; }
155
+
156
+ table { width: 100%; border-collapse: collapse; border: 1px solid #e2e8f0; border-radius: 8px; overflow: hidden; font-size: 13px; }
157
+ th { background: #0f172a; color: white; text-align: left; padding: 14px; font-weight: 600; font-size: 11px; }
158
+ </style>
159
+ </head>
160
+ <body>
161
+ <div class="header">
162
+ <h1 class="title">Test Case Log</h1>
163
+ <p class="subtitle">Project: Flowstride Community Edition</p>
164
+ </div>
165
+ <div class="summary">
166
+ <div class="stat-box">
167
+ <div class="stat-value" style="color: #1e293b;">${totalSteps}</div>
168
+ <div style="font-size:10px;color:#64748b;text-transform:uppercase;margin-top:4px;font-weight:700;">Total Steps</div>
169
+ </div>
170
+ <div class="stat-box">
171
+ <div class="stat-value" style="color:#10b981;">${passedSteps}</div>
172
+ <div style="font-size:10px;color:#64748b;text-transform:uppercase;margin-top:4px;font-weight:700;">Passed</div>
173
+ </div>
174
+ <div class="stat-box">
175
+ <div class="stat-value" style="color:#ef4444;">${failedSteps}</div>
176
+ <div style="font-size:10px;color:#64748b;text-transform:uppercase;margin-top:4px;font-weight:700;">Failed</div>
177
+ </div>
178
+ </div>
179
+
180
+ ${tablesHtml}
181
+
182
+ </body>
183
+ </html>
184
+ `;
185
+ }
186
+ }
187
+ exports.PdfGenerator = PdfGenerator;
@@ -8,8 +8,10 @@ export declare class FlowOrchestrator {
8
8
  private httpServer;
9
9
  private globalTestScenarios;
10
10
  private stepStatusRegistry;
11
+ private pdfGenerator;
11
12
  constructor(worker: FlowWorker, workerCount?: number, config?: FlowstrideConfig | undefined);
12
13
  private forwardToDashboard;
13
14
  private executeTestRun;
14
15
  start(targetPath: string): Promise<void>;
16
+ private handleExportPdfRequest;
15
17
  }
@@ -41,7 +41,9 @@ const fs = __importStar(require("fs"));
41
41
  const path = __importStar(require("path"));
42
42
  const http = __importStar(require("http"));
43
43
  const chalk_1 = __importDefault(require("chalk"));
44
+ const ora_1 = __importDefault(require("ora"));
44
45
  const ws_1 = require("ws");
46
+ const pdf_generator_1 = require("../reporting/pdf_generator");
45
47
  class FlowOrchestrator {
46
48
  worker;
47
49
  workerCount;
@@ -50,10 +52,12 @@ class FlowOrchestrator {
50
52
  httpServer = null;
51
53
  globalTestScenarios = [];
52
54
  stepStatusRegistry = new Map();
55
+ pdfGenerator;
53
56
  constructor(worker, workerCount = 1, config) {
54
57
  this.worker = worker;
55
58
  this.workerCount = workerCount;
56
59
  this.config = config;
60
+ this.pdfGenerator = new pdf_generator_1.PdfGenerator();
57
61
  this.worker.on("worker_event", (data) => {
58
62
  this.forwardToDashboard(data.type, data.payload);
59
63
  });
@@ -221,14 +225,28 @@ class FlowOrchestrator {
221
225
  ws.on("message", async (message) => {
222
226
  const data = JSON.parse(message.toString());
223
227
  if (data.type === "EXPORT_PDF_REQ") {
224
- console.log(chalk_1.default.magenta(`\n[Notice]: Exporting high-fidelity PDF reports is a Flowstride Enterprise feature.`));
225
- console.log(chalk_1.default.magenta(`[Notice]: Visit flowstride.io to upgrade and unlock advanced reporting.\n`));
228
+ let scenariosToExport = data.payload || [];
229
+ if (scenariosToExport.length === 0) {
230
+ scenariosToExport = this.globalTestScenarios;
231
+ }
232
+ await this.handleExportPdfRequest(scenariosToExport);
226
233
  }
227
234
  else if (data.type === "REPLAY_REQ") {
228
235
  await this.worker.handleReplayRequest(data.payload);
229
236
  }
230
237
  else if (data.type === "RERUN_REQ") {
231
- await this.executeTestRun(targetPath);
238
+ const requestedFile = data.payload?.filePath;
239
+ let specificTarget = targetPath;
240
+ if (requestedFile) {
241
+ const targetStat = fs.statSync(targetPath);
242
+ if (targetStat.isDirectory()) {
243
+ specificTarget = path.join(targetPath, requestedFile);
244
+ }
245
+ else {
246
+ specificTarget = path.join(path.dirname(targetPath), requestedFile);
247
+ }
248
+ }
249
+ await this.executeTestRun(specificTarget);
232
250
  }
233
251
  else if (data.type === "STOP_REQ") {
234
252
  console.log(chalk_1.default.yellow("\nReceived STOP signal from Dashboard. Halting execution..."));
@@ -265,5 +283,21 @@ class FlowOrchestrator {
265
283
  throw e;
266
284
  }
267
285
  }
286
+ async handleExportPdfRequest(testScenariosFromUI) {
287
+ try {
288
+ const spinner = (0, ora_1.default)(chalk_1.default.blue("Compiling high-fidelity PDF report...")).start();
289
+ const pdfPath = await this.pdfGenerator.generateTestcasePdf(testScenariosFromUI);
290
+ // Convert the saved PDF to base64 to trigger the native browser download UI
291
+ const pdfBuffer = fs.readFileSync(pdfPath);
292
+ const base64Data = pdfBuffer.toString("base64");
293
+ this.forwardToDashboard("EXPORT_PDF_RES", { base64: base64Data });
294
+ this.forwardToDashboard("EXPORT_PDF_SUCCESS", { path: pdfPath });
295
+ spinner.succeed(chalk_1.default.green(`PDF Exported Successfully: ${pdfPath}`));
296
+ }
297
+ catch (e) {
298
+ console.error(chalk_1.default.red(`\n[Report Error]: Failed to generate PDF - ${e.message}`));
299
+ this.forwardToDashboard("EXPORT_PDF_ERROR", { error: e.message });
300
+ }
301
+ }
268
302
  }
269
303
  exports.FlowOrchestrator = FlowOrchestrator;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowstride",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "description": "An opensource flowfirst test automation framework",
5
5
  "workspaces": [
6
6
  "packages/flowstride_report"
@@ -33,6 +33,7 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "@inquirer/prompts": "^7.10.1",
36
+ "axios": "^1.18.0",
36
37
  "chalk": "^4.1.2",
37
38
  "commander": "^11.0.0",
38
39
  "dotenv": "^17.4.2",