flowstride 1.0.14 → 1.0.15

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
@@ -4,6 +4,8 @@
4
4
 
5
5
  Flowstride OS combines the human-readable structure of Behavior-Driven Development (BDD) with the raw execution power of Playwright. It allows QA engineers and developers to write robust, maintainable UI and API tests using an intuitive domain-specific language (DSL), while providing a rich, interactive React dashboard to monitor, debug, and replay test runs in real time.
6
6
 
7
+ [Documentation](https://docs.flowstride.io/)
8
+
7
9
  ---
8
10
 
9
11
  ## Why Flowstride?
@@ -97,7 +97,11 @@ async function runAction(targetPath, options) {
97
97
  }
98
98
  }
99
99
  const config = {
100
- headless: isCI ? true : (options.headless ?? localConfig.headless ?? true),
100
+ headless: isCI
101
+ ? true
102
+ : options.headed
103
+ ? false
104
+ : (localConfig.headless ?? true),
101
105
  timeout: localConfig.timeout ?? 30000,
102
106
  bail: options.bail ?? localConfig.bail ?? false,
103
107
  baseUrl: process.env.BASE_URL ?? localConfig.baseUrl,
@@ -129,15 +133,11 @@ async function runAction(targetPath, options) {
129
133
  const pluginManager = new plugin_manager_1.PluginManager(config, web, store, apiProxy);
130
134
  const vault = new persistent_vault_1.PersistentVault(root);
131
135
  const sessions = new manager_1.SessionManager(web, api, vault);
132
- // ==========================================
133
- // DYNAMIC ENGINE ROUTER (The Plugin Handoff)
134
- // ==========================================
135
136
  let ActiveFlowWorker = flow_worker_1.FlowWorker;
136
137
  let ActiveFlowOrchestrator = flow_orchestrator_1.FlowOrchestrator;
137
138
  let engineMode = "Open Source";
138
139
  const proWorkerPath = path.join(process.cwd(), "node_modules", "@flowstride", "pro", "dist", "runner", "flow_worker.js");
139
140
  const proOrchPath = path.join(process.cwd(), "node_modules", "@flowstride", "pro", "dist", "runner", "flow_orchestrator.js");
140
- // We only attempt to load the Pro Engine if the DRM Bouncer (index.ts) did NOT force OS Mode
141
141
  if (!options.forceOS &&
142
142
  fs.existsSync(proWorkerPath) &&
143
143
  fs.existsSync(proOrchPath)) {
@@ -153,7 +153,6 @@ async function runAction(targetPath, options) {
153
153
  }
154
154
  }
155
155
  console.log(`\n[Engine]: Booting Flowstride in ${engineMode} mode...`);
156
- // ==========================================
157
156
  const worker = new ActiveFlowWorker(config, web, api, store, sessions, pluginManager);
158
157
  const orchestrator = new ActiveFlowOrchestrator(worker, workerCount, config);
159
158
  try {
package/dist/cli/index.js CHANGED
@@ -54,7 +54,7 @@ const API_BASE = process.env.FLOWSTRIDE_API_URL || "https://cloud.flowstride.io"
54
54
  program
55
55
  .name("flowstride")
56
56
  .description("Flowstride: The Enterprise Flow-First Automation CLI")
57
- .version("1.0.14");
57
+ .version("1.0.15");
58
58
  program
59
59
  .command("init")
60
60
  .description("Initialise a new Flowstride workspace in the current directory")
@@ -253,8 +253,8 @@ program
253
253
  .command("run [targetPath]")
254
254
  .description("Execute all .flow automation journeys in the ./flows directory")
255
255
  .option("-b, --bail", "Stop execution on first failure", false)
256
- .option("--headed", "Run browser in headed mode", false)
257
- .option("--headless <boolean>", "Run browser in headless mode", (val) => val === "true", true)
256
+ .option("--headed", "Run browser in headed mode")
257
+ .option("--headless", "Run browser in headless mode")
258
258
  .option("--workers <number>", "Number of parallel workers to spawn (Pro feature)")
259
259
  .action(async (targetPath, options) => {
260
260
  if (typeof targetPath === "object") {
@@ -357,6 +357,7 @@ class Parser {
357
357
  throw new Error(`Line ${viaToken.line}: Expected 'via'`);
358
358
  payload.verb = this.consume(lexer_1.TokenType.COMMAND, "Expected API verb (e.g. POST)").value.toLowerCase();
359
359
  payload.url = this.consume(lexer_1.TokenType.STRING, "Endpoint URL expected").value;
360
+ // --- ENSURE ARRAY FORMAT FOR API CLIENT ---
360
361
  payload.headers = [];
361
362
  this.parseApiModifiers(payload);
362
363
  break;
@@ -367,6 +368,7 @@ class Parser {
367
368
  case "delete":
368
369
  case "graphql":
369
370
  payload.url = this.consume(lexer_1.TokenType.STRING, "Endpoint URL expected").value;
371
+ // --- ENSURE ARRAY FORMAT FOR API CLIENT ---
370
372
  payload.headers = [];
371
373
  this.parseApiModifiers(payload);
372
374
  break;
@@ -615,9 +617,33 @@ class Parser {
615
617
  throw new Error(`Syntax Error on line ${targetToken.line}: Invalid keyword '${target}'. Flowstride uses strict camelCase. Did you mean 'reqHeader'?`);
616
618
  }
617
619
  if (target === "reqHeader") {
618
- const hKey = this.consume(lexer_1.TokenType.STRING, "Header key expected").value;
619
- const hVal = this.consume(lexer_1.TokenType.STRING, "Header value expected").value;
620
- payload.headers.push({ key: hKey, value: hVal });
620
+ // --- THE NEW MULTI-FORMAT HEADER PARSER ---
621
+ const firstToken = this.consume(lexer_1.TokenType.STRING, "Header key or JSON block expected");
622
+ // Lookahead: If the next token is ALSO a string, use the legacy Key-Value parsing
623
+ if (!this.isAtEnd() && this.peek().type === lexer_1.TokenType.STRING) {
624
+ const hKey = firstToken.value;
625
+ const hVal = this.advance().value;
626
+ payload.headers.push({ key: hKey, value: hVal });
627
+ }
628
+ else {
629
+ // Otherwise, it must be the new multi-line JSON Block format
630
+ try {
631
+ const headerObj = JSON.parse(firstToken.value);
632
+ if (typeof headerObj !== "object" ||
633
+ headerObj === null ||
634
+ Array.isArray(headerObj)) {
635
+ throw new Error("Parsed JSON is not a Key-Value object.");
636
+ }
637
+ // Loop through the parsed block and push them into the array for the API client
638
+ for (const [k, v] of Object.entries(headerObj)) {
639
+ payload.headers.push({ key: k, value: String(v) });
640
+ }
641
+ }
642
+ catch (e) {
643
+ throw new Error(`Line ${firstToken.line}: Invalid JSON format for reqHeader block. Ensure it is a valid JSON object, or use the legacy format with two strings (Key and Value).`);
644
+ }
645
+ }
646
+ // ------------------------------------------
621
647
  }
622
648
  else if (target === "reqBody") {
623
649
  payload.body = this.consume(lexer_1.TokenType.STRING, "Body content expected").value;
@@ -276,22 +276,41 @@ class FlowOrchestrator {
276
276
  }
277
277
  });
278
278
  });
279
- this.httpServer.listen(0, async () => {
280
- const address = this.httpServer?.address();
281
- const port = typeof address === "object" && address !== null ? address.port : null;
282
- if (!port) {
283
- console.error(chalk_1.default.red("\n[Error]: Could not bind to an available port."));
284
- process.exit(1);
285
- }
286
- const localUrl = `http://localhost:${port}`;
287
- const { default: open } = await import("open");
288
- await open(localUrl);
289
- console.log(chalk_1.default.yellow(`\n[Dashboard]: Local server running at ${localUrl}`));
290
- console.log(chalk_1.default.yellow("[Dashboard]: Waiting for UI connection..."));
291
- while (this.wss.clients.size === 0)
292
- await new Promise((r) => setTimeout(r, 250));
293
- console.log(chalk_1.default.green("[Dashboard]: Connected to UI!"));
294
- await this.executeTestRun(targetPath);
279
+ return new Promise((resolve, reject) => {
280
+ this.httpServer.listen(0, async () => {
281
+ try {
282
+ const address = this.httpServer?.address();
283
+ const port = typeof address === "object" && address !== null
284
+ ? address.port
285
+ : null;
286
+ if (!port) {
287
+ console.error(chalk_1.default.red("\n[Error]: Could not bind to an available port."));
288
+ return reject(new Error("No port available"));
289
+ }
290
+ const localUrl = `http://localhost:${port}`;
291
+ console.log(chalk_1.default.yellow(`\n[Dashboard]: Local server running at ${localUrl}`));
292
+ const isStrictHeadless = process.argv.includes("--headless") ||
293
+ !!process.env.CI ||
294
+ !!process.env.GITHUB_ACTIONS ||
295
+ !!process.env.FLOWSTRIDE_API_KEY;
296
+ if (!isStrictHeadless) {
297
+ const { default: open } = await import("open");
298
+ await open(localUrl);
299
+ console.log(chalk_1.default.yellow("[Dashboard]: Waiting for UI connection..."));
300
+ while (this.wss.clients.size === 0)
301
+ await new Promise((r) => setTimeout(r, 250));
302
+ console.log(chalk_1.default.green("[Dashboard]: Connected to UI!"));
303
+ }
304
+ else {
305
+ console.log(chalk_1.default.gray("[Dashboard]: Headless mode active. Bypassing UI auto-launch..."));
306
+ }
307
+ await this.executeTestRun(targetPath);
308
+ resolve(undefined);
309
+ }
310
+ catch (err) {
311
+ reject(err);
312
+ }
313
+ });
295
314
  });
296
315
  }
297
316
  catch (e) {
@@ -131,6 +131,9 @@ class FlowWorker extends events_1.EventEmitter {
131
131
  return context;
132
132
  }
133
133
  resolveTextVariables(text) {
134
+ if (typeof text !== "string") {
135
+ return text;
136
+ }
134
137
  let resolved = text;
135
138
  resolved = resolved.replace(/\{\{env\.([a-zA-Z0-9_]+)\}\}/g, (_, varName) => {
136
139
  const val = process.env[varName];
@@ -259,11 +262,21 @@ class FlowWorker extends events_1.EventEmitter {
259
262
  catch {
260
263
  rawBodyString = typeof body === "string" ? body : null;
261
264
  }
262
- const curlString = this.web.synthesizeCurl(method, cleanUrl, headers || {}, rawBodyString);
265
+ let formattedHeaders = {};
266
+ if (Array.isArray(headers)) {
267
+ headers.forEach((h) => {
268
+ if (h && h.key)
269
+ formattedHeaders[h.key] = h.value;
270
+ });
271
+ }
272
+ else if (headers && typeof headers === "object") {
273
+ formattedHeaders = headers;
274
+ }
275
+ const curlString = this.web.synthesizeCurl(method, cleanUrl, formattedHeaders, rawBodyString);
263
276
  this.emitLog("REST", (state.status || 500) < 400 ? "success" : "error", `${method.toUpperCase()} ${endpointName}`, {
264
277
  method: method.toUpperCase(),
265
278
  url: cleanUrl,
266
- requestHeaders: headers,
279
+ requestHeaders: formattedHeaders,
267
280
  requestBody: parsedReqBody,
268
281
  responseStatus: state.status,
269
282
  responseHeaders: state.headers,
@@ -340,7 +353,14 @@ class FlowWorker extends events_1.EventEmitter {
340
353
  this.isAborted = true;
341
354
  }
342
355
  async runBatch(filesToProcess) {
343
- this.config.headless = true;
356
+ // this.config.headless = true;
357
+ const isCliHeadless = process.argv.includes("--headless");
358
+ if (isCliHeadless) {
359
+ this.config.headless = true;
360
+ }
361
+ else if (this.config.headless === undefined) {
362
+ this.config.headless = false;
363
+ }
344
364
  this.activeAudioPath = undefined;
345
365
  this.isAborted = false;
346
366
  this.isDebugHalted = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowstride",
3
- "version": "1.0.14",
3
+ "version": "1.0.15",
4
4
  "description": "An opensource flowfirst test automation framework",
5
5
  "workspaces": [
6
6
  "packages/flowstride_report"