flowstride 1.0.9 → 1.0.11

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,29 +24,49 @@ 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.)_
37
37
 
38
- **3. Run your first test:**
38
+ **3. Install VS Code Extension (Highly Recommended):**
39
+
40
+ To get the best developer experience, including syntax highlighting, auto-completion, and native IDE support for .flow files, install our official VS Code extension directly via the VS Code CLI.
41
+
42
+ Run the command below:
43
+
44
+ ```bash
45
+ code --install-extension flowstride.flowstride-vscode
46
+ ```
47
+
48
+ _Alternatively, you can open the Extensions tab in VS Code and search for Flowstride Automation_
49
+
50
+ **4. Run your first test:**
39
51
 
40
52
  ```bash
41
- flowstride run
53
+ npx flowstride run
42
54
 
43
55
  OR
44
56
 
45
- flowstride run flows/example.flow
57
+ npx flowstride run flows/example.flow
46
58
  ```
47
59
 
48
60
  _(This command executes your test while instantly booting up the local Flowstride Dashboard.)_
49
61
 
62
+ **5. Login to Flowstride Cloud:**
63
+
64
+ Authenticate your local project with your Cloud account to unlock Cloud features after signing up at [cloud.flowstride.io](https://cloud.flowstride.io).
65
+
66
+ ```bash
67
+ npx flowstride login
68
+ ```
69
+
50
70
  ---
51
71
 
52
72
  ## Privacy & Data Security
package/dist/cli/index.js CHANGED
@@ -45,6 +45,7 @@ const fs = __importStar(require("fs"));
45
45
  const path = __importStar(require("path"));
46
46
  const os = __importStar(require("os"));
47
47
  const tar = __importStar(require("tar"));
48
+ const child_process_1 = require("child_process");
48
49
  const prompts_1 = require("@inquirer/prompts");
49
50
  const chalk_1 = __importDefault(require("chalk"));
50
51
  const ora_1 = __importDefault(require("ora"));
@@ -53,7 +54,7 @@ const API_BASE = process.env.FLOWSTRIDE_API_URL || "https://cloud.flowstride.io"
53
54
  program
54
55
  .name("flowstride")
55
56
  .description("Flowstride: The Enterprise Flow-First Automation CLI")
56
- .version("1.0.9");
57
+ .version("1.0.11");
57
58
  program
58
59
  .command("init")
59
60
  .description("Initialise a new Flowstride workspace in the current directory")
@@ -154,20 +155,20 @@ program
154
155
  cwd: nodeModulesPath,
155
156
  strip: 1,
156
157
  });
158
+ installSpinner.text = "Wiring up Enterprise dependencies...";
159
+ (0, child_process_1.execSync)("npm install --omit=dev", {
160
+ cwd: nodeModulesPath,
161
+ stdio: "ignore",
162
+ });
157
163
  installSpinner.succeed(chalk_1.default.green("Enterprise Engine installed successfully!"));
158
164
  }
159
165
  catch (installError) {
160
166
  installSpinner.stop();
161
- // ==========================================
162
- // TEMPORARY DEBUG LOG TO UNMASK THE ERROR
163
- // ==========================================
164
167
  console.error(chalk_1.default.bgRed.white("\n[RAW SYSTEM ERROR]:"), installError);
165
- // ==========================================
166
168
  console.log(chalk_1.default.red("\n✖ Failed to install the Enterprise Engine. Please check your internet connection or verify your project configuration."));
167
169
  if (fs.existsSync(nodeModulesPath)) {
168
170
  fs.rmSync(nodeModulesPath, { recursive: true, force: true });
169
171
  }
170
- // THE FIX: Strict Rollback on Failure
171
172
  console.log(chalk_1.default.yellow("⚠️ Reverting login: Enterprise Engine is required to sync."));
172
173
  if (fs.existsSync(credentialsPath))
173
174
  fs.unlinkSync(credentialsPath);
@@ -180,7 +181,6 @@ program
180
181
  }
181
182
  }
182
183
  else {
183
- // THE FIX: Strict Rollback on Decline
184
184
  console.log(chalk_1.default.yellow(`\n⚠️ Installation skipped.`));
185
185
  console.log(chalk_1.default.red(`✖ Reverting login: You must install the Enterprise Engine to sync this workspace.`));
186
186
  if (fs.existsSync(credentialsPath))
@@ -189,7 +189,6 @@ program
189
189
  }
190
190
  }
191
191
  }
192
- // Only print this if the engine was successfully verified or installed
193
192
  console.log(chalk_1.default.bold.green(`\n✅ Machine synchronized with Flowstride Enterprise.`));
194
193
  }
195
194
  catch (error) {
@@ -211,7 +210,6 @@ program
211
210
  }
212
211
  catch (err) { }
213
212
  }
214
- // THE FIX: Defensive Self-Healing. If login exists but engine is missing, drop to OS.
215
213
  if (!process.env.FLOWSTRIDE_API_KEY &&
216
214
  token &&
217
215
  !fs.existsSync(nodeModulesPath)) {
@@ -252,13 +250,17 @@ program
252
250
  }
253
251
  });
254
252
  program
255
- .command("run")
253
+ .command("run [targetPath]")
256
254
  .description("Execute all .flow automation journeys in the ./flows directory")
257
255
  .option("-b, --bail", "Stop execution on first failure", false)
258
256
  .option("--headed", "Run browser in headed mode", false)
259
257
  .option("--headless <boolean>", "Run browser in headless mode", (val) => val === "true", true)
260
258
  .option("--workers <number>", "Number of parallel workers to spawn (Pro feature)")
261
- .action(async (options) => {
259
+ .action(async (targetPath, options) => {
260
+ if (typeof targetPath === "object") {
261
+ options = targetPath;
262
+ targetPath = undefined;
263
+ }
262
264
  let token = process.env.FLOWSTRIDE_API_KEY || null;
263
265
  let forceOS = false;
264
266
  const credentialsPath = path.join(process.cwd(), ".flowstride", "login", "login.json");
@@ -270,7 +272,6 @@ program
270
272
  }
271
273
  catch (err) { }
272
274
  }
273
- // THE FIX: Defensive Self-Healing
274
275
  if (!process.env.FLOWSTRIDE_API_KEY &&
275
276
  token &&
276
277
  !fs.existsSync(nodeModulesPath)) {
@@ -315,13 +316,13 @@ program
315
316
  if (options.headed) {
316
317
  options.headless = false;
317
318
  }
318
- const strictUiFlowsDir = path.resolve(process.cwd(), "./flows");
319
+ const resolvedTargetPath = targetPath ? targetPath : "./flows";
320
+ const strictUiFlowsDir = path.resolve(process.cwd(), resolvedTargetPath);
319
321
  if (!fs.existsSync(strictUiFlowsDir)) {
320
322
  console.error(`\n[ERROR]: Could not find ${strictUiFlowsDir}.`);
321
- console.error(`Flowstride requires all UI tests to be inside a './flows' directory.`);
323
+ console.error(`Flowstride requires all UI tests to be inside a valid directory.`);
322
324
  process.exit(1);
323
325
  }
324
- // Inject our DRM state into options before passing to runner
325
326
  options.forceOS = forceOS;
326
327
  await (0, run_1.runAction)(strictUiFlowsDir, options);
327
328
  });
@@ -329,7 +330,7 @@ async function handleOfflinePrompt() {
329
330
  console.log(chalk_1.default.yellow("🛜 OFFLINE: Unable to reach Flowstride Cloud. Enterprise Sync paused."));
330
331
  if (process.env.CI || process.env.FLOWSTRIDE_API_KEY) {
331
332
  console.log(chalk_1.default.yellow("⚠️ CI/CD Environment: Automatically falling back to OS Engine."));
332
- return true; // Force OS
333
+ return true;
333
334
  }
334
335
  const runAnyway = await (0, prompts_1.confirm)({
335
336
  message: "⚠️ Execute a local OS run anyway? (Pro features are disabled on OS)",
@@ -338,7 +339,7 @@ async function handleOfflinePrompt() {
338
339
  if (!runAnyway) {
339
340
  process.exit(0);
340
341
  }
341
- return true; // User said yes, Force OS
342
+ return true;
342
343
  }
343
344
  program
344
345
  .command("report")
@@ -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.9",
3
+ "version": "1.0.11",
4
4
  "description": "An opensource flowfirst test automation framework",
5
5
  "workspaces": [
6
6
  "packages/flowstride_report"