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 +2 -0
- package/dist/cli/commands/run.js +5 -6
- package/dist/cli/index.js +3 -3
- package/dist/parser/parser.js +29 -3
- package/dist/runner/flow_orchestrator.js +35 -16
- package/dist/runner/flow_worker.js +23 -3
- package/package.json +1 -1
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?
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -97,7 +97,11 @@ async function runAction(targetPath, options) {
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
const config = {
|
|
100
|
-
headless: isCI
|
|
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.
|
|
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"
|
|
257
|
-
.option("--headless
|
|
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") {
|
package/dist/parser/parser.js
CHANGED
|
@@ -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
|
-
|
|
619
|
-
const
|
|
620
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
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:
|
|
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;
|