flowstride 1.0.11 → 1.0.13
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/dist/adapters/web/playwright.d.ts +1 -0
- package/dist/adapters/web/playwright.js +60 -0
- package/dist/cli/index.js +1 -1
- package/dist/parser/lexer.js +25 -8
- package/dist/parser/parser.d.ts +6 -0
- package/dist/parser/parser.js +106 -30
- package/dist/runner/flow_orchestrator.d.ts +1 -0
- package/dist/runner/flow_orchestrator.js +20 -5
- package/dist/runner/flow_worker.d.ts +1 -0
- package/dist/runner/flow_worker.js +204 -131
- package/package.json +1 -1
- package/src/ui/dist/assets/flow_logo-CjiL4Lvy.png +0 -0
- package/src/ui/dist/assets/index-BCdexgt5.js +0 -270
- package/src/ui/dist/assets/index-DXTn_ThB.css +0 -1
- package/src/ui/dist/favicon.png +0 -0
- package/src/ui/dist/favicon.svg +0 -1
- package/src/ui/dist/icons.svg +0 -24
- package/src/ui/dist/index.html +0 -14
|
@@ -26,6 +26,7 @@ export declare class WebAdapter {
|
|
|
26
26
|
private checkAmbiguity;
|
|
27
27
|
close(targetText: string): Promise<void>;
|
|
28
28
|
click(selector: string, elementType?: string): Promise<void>;
|
|
29
|
+
hover(selector: string, elementType?: string): Promise<void>;
|
|
29
30
|
type(selector: string, value: string, elementType?: string): Promise<void>;
|
|
30
31
|
passcode(selector: string, value: string, elementType?: string): Promise<void>;
|
|
31
32
|
check(selector: string, elementType?: string): Promise<void>;
|
|
@@ -397,6 +397,66 @@ class WebAdapter {
|
|
|
397
397
|
await targetLoc.scrollIntoViewIfNeeded();
|
|
398
398
|
await targetLoc.click();
|
|
399
399
|
}
|
|
400
|
+
async hover(selector, elementType) {
|
|
401
|
+
const loc = await this.getLocator(selector, elementType, false, false);
|
|
402
|
+
const targetLoc = await this.checkAmbiguity(loc, selector);
|
|
403
|
+
await targetLoc.waitFor({ state: "visible" });
|
|
404
|
+
await targetLoc.scrollIntoViewIfNeeded();
|
|
405
|
+
// 1. Accessibility WAI-ARIA Hack (Focus)
|
|
406
|
+
await targetLoc.focus({ timeout: 3000 }).catch(() => { });
|
|
407
|
+
await targetLoc
|
|
408
|
+
.evaluate((node) => {
|
|
409
|
+
if (typeof node.focus === "function")
|
|
410
|
+
node.focus();
|
|
411
|
+
})
|
|
412
|
+
.catch(() => { });
|
|
413
|
+
// 2. Get Exact Center Coordinates for Physical Move
|
|
414
|
+
let box = null;
|
|
415
|
+
try {
|
|
416
|
+
box = await targetLoc.boundingBox({ timeout: 3000 });
|
|
417
|
+
}
|
|
418
|
+
catch (e) { } // Catch in case it mutated instantly during focus
|
|
419
|
+
if (box) {
|
|
420
|
+
const centerX = box.x + box.width / 2;
|
|
421
|
+
const centerY = box.y + box.height / 2;
|
|
422
|
+
// Physically drag the mouse to the element to satisfy Hover Intent APIs
|
|
423
|
+
await this.page.mouse.move(centerX, centerY, { steps: 5 });
|
|
424
|
+
}
|
|
425
|
+
// 3. Playwright Native Hover Backup (Caught to prevent mutation crashes)
|
|
426
|
+
await targetLoc.hover({ force: true, timeout: 3000 }).catch(() => { });
|
|
427
|
+
// 4. The Ultimate Wakeup Call: Synthesize Full Coordinate-Loaded Events
|
|
428
|
+
await targetLoc
|
|
429
|
+
.evaluate((node) => {
|
|
430
|
+
const rect = node.getBoundingClientRect();
|
|
431
|
+
const x = rect.left + rect.width / 2;
|
|
432
|
+
const y = rect.top + rect.height / 2;
|
|
433
|
+
const events = [
|
|
434
|
+
"pointerover",
|
|
435
|
+
"pointerenter",
|
|
436
|
+
"mouseover",
|
|
437
|
+
"mouseenter",
|
|
438
|
+
"mousemove",
|
|
439
|
+
];
|
|
440
|
+
events.forEach((type) => {
|
|
441
|
+
node.dispatchEvent(new MouseEvent(type, {
|
|
442
|
+
view: window,
|
|
443
|
+
bubbles: true,
|
|
444
|
+
cancelable: true,
|
|
445
|
+
clientX: x,
|
|
446
|
+
clientY: y,
|
|
447
|
+
screenX: x,
|
|
448
|
+
screenY: y,
|
|
449
|
+
}));
|
|
450
|
+
});
|
|
451
|
+
})
|
|
452
|
+
.catch(() => { });
|
|
453
|
+
// 5. We perform a programmatic click that won't trigger W3C navigation if handled by React toggles
|
|
454
|
+
await targetLoc
|
|
455
|
+
.click({ delay: 50, force: true, timeout: 3000 })
|
|
456
|
+
.catch(() => { });
|
|
457
|
+
// 6. The Paint Buffer
|
|
458
|
+
await this.page.waitForTimeout(600);
|
|
459
|
+
}
|
|
400
460
|
async type(selector, value, elementType) {
|
|
401
461
|
const loc = await this.getLocator(selector, elementType, false, false);
|
|
402
462
|
const targetLoc = await this.checkAmbiguity(loc, selector);
|
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.13");
|
|
58
58
|
program
|
|
59
59
|
.command("init")
|
|
60
60
|
.description("Initialise a new Flowstride workspace in the current directory")
|
package/dist/parser/lexer.js
CHANGED
|
@@ -129,7 +129,9 @@ class Lexer {
|
|
|
129
129
|
tokens.push({ type: TokenType.LET, value: ident, line: this.line });
|
|
130
130
|
continue;
|
|
131
131
|
}
|
|
132
|
-
if (lowerIdent === "feature" ||
|
|
132
|
+
if (lowerIdent === "feature" ||
|
|
133
|
+
lowerIdent === "scenario" ||
|
|
134
|
+
lowerIdent === "only.scenario") {
|
|
133
135
|
let tempPos = this.position;
|
|
134
136
|
while (tempPos < this.input.length &&
|
|
135
137
|
(this.input[tempPos] === " " || this.input[tempPos] === "\t"))
|
|
@@ -205,6 +207,7 @@ class Lexer {
|
|
|
205
207
|
"autoheal",
|
|
206
208
|
"mail.getotp",
|
|
207
209
|
"extract",
|
|
210
|
+
"hover",
|
|
208
211
|
];
|
|
209
212
|
if (validNakedCommands.map((c) => c.toLowerCase()).includes(lowerIdent)) {
|
|
210
213
|
tokens.push({
|
|
@@ -226,7 +229,16 @@ class Lexer {
|
|
|
226
229
|
throw new SyntaxError(`Syntax Error on line ${this.line}: Expected an action command after 'flow.try', but found "${ident}".`);
|
|
227
230
|
}
|
|
228
231
|
}
|
|
229
|
-
if ([
|
|
232
|
+
if ([
|
|
233
|
+
"given",
|
|
234
|
+
"when",
|
|
235
|
+
"then",
|
|
236
|
+
"and",
|
|
237
|
+
"stop.given",
|
|
238
|
+
"stop.when",
|
|
239
|
+
"stop.then",
|
|
240
|
+
"stop.and",
|
|
241
|
+
].includes(lowerIdent)) {
|
|
230
242
|
let tempPos = this.position;
|
|
231
243
|
while (tempPos < this.input.length &&
|
|
232
244
|
(this.input[tempPos] === " " || this.input[tempPos] === "\t"))
|
|
@@ -268,6 +280,7 @@ class Lexer {
|
|
|
268
280
|
"flow.autoheal",
|
|
269
281
|
"flow.mail.getotp",
|
|
270
282
|
"flow.extract",
|
|
283
|
+
"flow.hover",
|
|
271
284
|
]
|
|
272
285
|
.map((c) => c.toLowerCase())
|
|
273
286
|
.includes(lowerIdent)) {
|
|
@@ -290,14 +303,16 @@ class Lexer {
|
|
|
290
303
|
"contains",
|
|
291
304
|
"placeholder",
|
|
292
305
|
"status",
|
|
293
|
-
"
|
|
294
|
-
"
|
|
295
|
-
"
|
|
296
|
-
"
|
|
297
|
-
"
|
|
306
|
+
"responseTime",
|
|
307
|
+
"reqBody",
|
|
308
|
+
"resBody",
|
|
309
|
+
"reqHeader",
|
|
310
|
+
"resHeader",
|
|
298
311
|
"cookie",
|
|
299
312
|
"global",
|
|
300
|
-
]
|
|
313
|
+
]
|
|
314
|
+
.map((c) => c.toLowerCase())
|
|
315
|
+
.includes(lowerIdent)) {
|
|
301
316
|
tokens.push({
|
|
302
317
|
type: TokenType.MODIFIER,
|
|
303
318
|
value: ident,
|
|
@@ -314,6 +329,8 @@ class Lexer {
|
|
|
314
329
|
"span",
|
|
315
330
|
"image",
|
|
316
331
|
"img",
|
|
332
|
+
"burger",
|
|
333
|
+
"hamburger",
|
|
317
334
|
].includes(lowerIdent)) {
|
|
318
335
|
tokens.push({
|
|
319
336
|
type: TokenType.ELEMENT,
|
package/dist/parser/parser.d.ts
CHANGED
|
@@ -10,11 +10,13 @@ export interface StepNode {
|
|
|
10
10
|
action: string;
|
|
11
11
|
payload: any;
|
|
12
12
|
line: number;
|
|
13
|
+
isStop?: boolean;
|
|
13
14
|
}
|
|
14
15
|
export interface ScenarioNode {
|
|
15
16
|
name: string;
|
|
16
17
|
declarations: DeclarationNode[];
|
|
17
18
|
steps: StepNode[];
|
|
19
|
+
isOnly?: boolean;
|
|
18
20
|
}
|
|
19
21
|
export interface FeatureNode {
|
|
20
22
|
name: string;
|
|
@@ -26,6 +28,10 @@ export declare class Parser {
|
|
|
26
28
|
private position;
|
|
27
29
|
private currentBlockDescription;
|
|
28
30
|
private stepCounter;
|
|
31
|
+
private scenarioCounter;
|
|
32
|
+
private hasOnlyScenario;
|
|
33
|
+
private hasStopInCurrentScenario;
|
|
34
|
+
private currentBlockIsStop;
|
|
29
35
|
constructor(tokens: Token[]);
|
|
30
36
|
parse(): FeatureNode[];
|
|
31
37
|
private parseDeclaration;
|
package/dist/parser/parser.js
CHANGED
|
@@ -7,6 +7,10 @@ class Parser {
|
|
|
7
7
|
position = 0;
|
|
8
8
|
currentBlockDescription = "";
|
|
9
9
|
stepCounter = 1;
|
|
10
|
+
scenarioCounter = 0;
|
|
11
|
+
hasOnlyScenario = false;
|
|
12
|
+
hasStopInCurrentScenario = false;
|
|
13
|
+
currentBlockIsStop = false;
|
|
10
14
|
constructor(tokens) {
|
|
11
15
|
this.tokens = tokens;
|
|
12
16
|
}
|
|
@@ -68,15 +72,25 @@ class Parser {
|
|
|
68
72
|
};
|
|
69
73
|
}
|
|
70
74
|
parseScenario() {
|
|
71
|
-
this.advance();
|
|
75
|
+
const scenarioToken = this.advance();
|
|
76
|
+
const isOnly = scenarioToken.value.toLowerCase() === "only.scenario";
|
|
77
|
+
if (isOnly) {
|
|
78
|
+
if (this.hasOnlyScenario) {
|
|
79
|
+
throw new Error(`Line ${scenarioToken.line}: You cannot have more than one 'only.Scenario' in a single file.`);
|
|
80
|
+
}
|
|
81
|
+
this.hasOnlyScenario = true;
|
|
82
|
+
}
|
|
83
|
+
this.hasStopInCurrentScenario = false;
|
|
72
84
|
const nameToken = this.consume(lexer_1.TokenType.STRING, "Scenario must have a description");
|
|
73
85
|
const steps = [];
|
|
74
86
|
const declarations = [];
|
|
87
|
+
this.scenarioCounter++;
|
|
75
88
|
this.stepCounter = 1;
|
|
76
89
|
let state = "START";
|
|
77
90
|
let hasGiven = false, hasWhen = false, hasAnd = false, hasThen = false;
|
|
78
91
|
let isLocked = false;
|
|
79
92
|
let currentBlockType = "";
|
|
93
|
+
this.currentBlockIsStop = false;
|
|
80
94
|
while (!this.isAtEnd() &&
|
|
81
95
|
this.peek().type !== lexer_1.TokenType.SCENARIO &&
|
|
82
96
|
this.peek().type !== lexer_1.TokenType.FEATURE) {
|
|
@@ -88,7 +102,19 @@ class Parser {
|
|
|
88
102
|
if (isLocked)
|
|
89
103
|
throw new Error(`Line ${token.line}: Terminal Lock Violation. No steps allowed after Then.`);
|
|
90
104
|
const blockToken = this.advance();
|
|
91
|
-
|
|
105
|
+
let rawBlockType = blockToken.value.toLowerCase();
|
|
106
|
+
if (rawBlockType.startsWith("stop.")) {
|
|
107
|
+
if (this.hasStopInCurrentScenario) {
|
|
108
|
+
throw new Error(`Line ${blockToken.line}: You cannot have more than one 'stop.' block in a single scenario.`);
|
|
109
|
+
}
|
|
110
|
+
this.hasStopInCurrentScenario = true;
|
|
111
|
+
this.currentBlockIsStop = true;
|
|
112
|
+
rawBlockType = rawBlockType.replace("stop.", "");
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
this.currentBlockIsStop = false;
|
|
116
|
+
}
|
|
117
|
+
const blockType = rawBlockType;
|
|
92
118
|
let description = this.peek().type === lexer_1.TokenType.STRING ? this.advance().value : "";
|
|
93
119
|
if (blockType === "given") {
|
|
94
120
|
if (state !== "START")
|
|
@@ -116,9 +142,10 @@ class Parser {
|
|
|
116
142
|
hasAnd = true;
|
|
117
143
|
}
|
|
118
144
|
currentBlockType = blockType;
|
|
145
|
+
const displayBlockName = blockType.charAt(0).toUpperCase() + blockType.slice(1);
|
|
119
146
|
this.currentBlockDescription = description
|
|
120
|
-
? `${
|
|
121
|
-
:
|
|
147
|
+
? `${displayBlockName} "${description}"`
|
|
148
|
+
: displayBlockName;
|
|
122
149
|
}
|
|
123
150
|
else if (token.type === lexer_1.TokenType.COMMAND ||
|
|
124
151
|
token.type === lexer_1.TokenType.ALIAS_COMMAND) {
|
|
@@ -126,6 +153,9 @@ class Parser {
|
|
|
126
153
|
throw new Error(`Line ${token.line}: Terminal Lock Violation.`);
|
|
127
154
|
}
|
|
128
155
|
const step = this.parseCommand();
|
|
156
|
+
if (this.currentBlockIsStop) {
|
|
157
|
+
step.isStop = true;
|
|
158
|
+
}
|
|
129
159
|
if (currentBlockType === "and" &&
|
|
130
160
|
step.action !== "expect" &&
|
|
131
161
|
step.action !== "plugin") {
|
|
@@ -140,7 +170,15 @@ class Parser {
|
|
|
140
170
|
if (!hasGiven || !hasWhen || !hasThen) {
|
|
141
171
|
throw new Error(`Scenario "${nameToken.value}" is missing mandatory blocks.`);
|
|
142
172
|
}
|
|
143
|
-
|
|
173
|
+
const scenarioNode = {
|
|
174
|
+
name: nameToken.value,
|
|
175
|
+
declarations,
|
|
176
|
+
steps,
|
|
177
|
+
};
|
|
178
|
+
if (isOnly) {
|
|
179
|
+
scenarioNode.isOnly = true;
|
|
180
|
+
}
|
|
181
|
+
return scenarioNode;
|
|
144
182
|
}
|
|
145
183
|
parseSmartSelectorPayload() {
|
|
146
184
|
let selector = "";
|
|
@@ -202,7 +240,7 @@ class Parser {
|
|
|
202
240
|
dataPayload = this.advance().value;
|
|
203
241
|
}
|
|
204
242
|
return {
|
|
205
|
-
id: `
|
|
243
|
+
id: `scenario_${this.scenarioCounter}_S${this.stepCounter++}`,
|
|
206
244
|
blockDescription: this.currentBlockDescription,
|
|
207
245
|
action: "plugin",
|
|
208
246
|
payload: {
|
|
@@ -259,26 +297,32 @@ class Parser {
|
|
|
259
297
|
autoheal: "autoHeal",
|
|
260
298
|
"mail.getotp": "getOtp",
|
|
261
299
|
extract: "extract",
|
|
300
|
+
hover: "hover",
|
|
262
301
|
};
|
|
263
302
|
const action = actionMap[actionBase] || actionBase;
|
|
264
303
|
let payload = { optional: isOptional };
|
|
265
304
|
switch (action) {
|
|
266
305
|
case "extract":
|
|
267
306
|
const extTargetToken = this.consume(lexer_1.TokenType.MODIFIER, "Expected target after extract");
|
|
268
|
-
const extTarget = extTargetToken.value
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
307
|
+
const extTarget = extTargetToken.value;
|
|
308
|
+
if (extTarget.toLowerCase() === "resbody" && extTarget !== "resBody") {
|
|
309
|
+
throw new Error(`Syntax Error on line ${extTargetToken.line}: Invalid keyword '${extTarget}'. Flowstride uses strict camelCase. Did you mean 'resBody'?`);
|
|
310
|
+
}
|
|
311
|
+
if (extTarget.toLowerCase() === "reqbody" && extTarget !== "reqBody") {
|
|
312
|
+
throw new Error(`Syntax Error on line ${extTargetToken.line}: Invalid keyword '${extTarget}'. Flowstride uses strict camelCase. Did you mean 'reqBody'?`);
|
|
313
|
+
}
|
|
314
|
+
if (extTarget.toLowerCase() === "resheader" &&
|
|
315
|
+
extTarget !== "resHeader") {
|
|
316
|
+
throw new Error(`Syntax Error on line ${extTargetToken.line}: Invalid keyword '${extTarget}'. Flowstride uses strict camelCase. Did you mean 'resHeader'?`);
|
|
317
|
+
}
|
|
318
|
+
if (extTarget.toLowerCase() === "reqheader" &&
|
|
319
|
+
extTarget !== "reqHeader") {
|
|
320
|
+
throw new Error(`Syntax Error on line ${extTargetToken.line}: Invalid keyword '${extTarget}'. Flowstride uses strict camelCase. Did you mean 'reqHeader'?`);
|
|
321
|
+
}
|
|
322
|
+
if (!["resBody", "resHeader", "cookie"].includes(extTarget)) {
|
|
279
323
|
throw new Error(`Line ${extTargetToken.line}: Expected 'resBody', 'resHeader', or 'cookie' after extract`);
|
|
280
324
|
}
|
|
281
|
-
payload.target =
|
|
325
|
+
payload.target = extTarget;
|
|
282
326
|
payload.jsonPath = this.consume(lexer_1.TokenType.STRING, "JSONPath or Key expected").value;
|
|
283
327
|
const asToken = this.advance();
|
|
284
328
|
if (asToken.value.toLowerCase() !== "as") {
|
|
@@ -333,8 +377,19 @@ class Parser {
|
|
|
333
377
|
case "forceClick":
|
|
334
378
|
case "check":
|
|
335
379
|
case "uncheck":
|
|
380
|
+
case "hover":
|
|
336
381
|
this.consumeOptionalElement(payload);
|
|
337
|
-
|
|
382
|
+
if (["burger", "hamburger"].includes(payload.elementType)) {
|
|
383
|
+
if (!this.isAtEnd() && this.peek().type === lexer_1.TokenType.STRING) {
|
|
384
|
+
payload.selector = this.parseSmartSelectorPayload();
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
payload.selector = "smart-burger-fallback";
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
payload.selector = this.parseSmartSelectorPayload();
|
|
392
|
+
}
|
|
338
393
|
break;
|
|
339
394
|
case "type":
|
|
340
395
|
case "forceType":
|
|
@@ -426,16 +481,21 @@ class Parser {
|
|
|
426
481
|
break;
|
|
427
482
|
case "expect":
|
|
428
483
|
const modToken = this.consume(lexer_1.TokenType.MODIFIER, "Modifier expected");
|
|
429
|
-
|
|
430
|
-
if (mod === "resbody")
|
|
431
|
-
mod
|
|
432
|
-
|
|
433
|
-
|
|
484
|
+
const mod = modToken.value;
|
|
485
|
+
if (mod.toLowerCase() === "resbody" && mod !== "resBody") {
|
|
486
|
+
throw new Error(`Syntax Error on line ${modToken.line}: Invalid keyword '${mod}'. Flowstride uses strict camelCase. Did you mean 'resBody'?`);
|
|
487
|
+
}
|
|
488
|
+
if (mod.toLowerCase() === "resheader" && mod !== "resHeader") {
|
|
489
|
+
throw new Error(`Syntax Error on line ${modToken.line}: Invalid keyword '${mod}'. Flowstride uses strict camelCase. Did you mean 'resHeader'?`);
|
|
490
|
+
}
|
|
491
|
+
if (mod.toLowerCase() === "responsetime" && mod !== "responseTime") {
|
|
492
|
+
throw new Error(`Syntax Error on line ${modToken.line}: Invalid keyword '${mod}'. Flowstride uses strict camelCase. Did you mean 'responseTime'?`);
|
|
493
|
+
}
|
|
434
494
|
payload.type = mod;
|
|
435
495
|
if (mod === "status") {
|
|
436
496
|
payload.statusCode = this.consume(lexer_1.TokenType.STRING, 'Status code expected (e.g., "200")').value;
|
|
437
497
|
}
|
|
438
|
-
else if (mod === "
|
|
498
|
+
else if (mod === "responseTime") {
|
|
439
499
|
const conditionToken = this.advance();
|
|
440
500
|
if (conditionToken.value.toLowerCase() !== "lessthan") {
|
|
441
501
|
throw new Error(`Line ${modToken.line}: Expected 'lessThan' after responseTime`);
|
|
@@ -502,7 +562,17 @@ class Parser {
|
|
|
502
562
|
payload.fuzzy = true;
|
|
503
563
|
}
|
|
504
564
|
this.consumeOptionalElement(payload);
|
|
505
|
-
|
|
565
|
+
if (["burger", "hamburger"].includes(payload.elementType)) {
|
|
566
|
+
if (!this.isAtEnd() && this.peek().type === lexer_1.TokenType.STRING) {
|
|
567
|
+
payload.selector = this.parseSmartSelectorPayload();
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
payload.selector = "smart-burger-fallback";
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
payload.selector = this.parseSmartSelectorPayload();
|
|
575
|
+
}
|
|
506
576
|
}
|
|
507
577
|
else if (mod === "value" || mod === "attribute") {
|
|
508
578
|
this.consumeOptionalElement(payload);
|
|
@@ -523,7 +593,7 @@ class Parser {
|
|
|
523
593
|
throw new Error(`Unknown command: ${action}`);
|
|
524
594
|
}
|
|
525
595
|
return {
|
|
526
|
-
id: `
|
|
596
|
+
id: `scenario_${this.scenarioCounter}_S${this.stepCounter++}`,
|
|
527
597
|
blockDescription: this.currentBlockDescription,
|
|
528
598
|
action,
|
|
529
599
|
payload,
|
|
@@ -537,13 +607,19 @@ class Parser {
|
|
|
537
607
|
if (nextToken.type === lexer_1.TokenType.PREPOSITION && nextVal === "with") {
|
|
538
608
|
this.advance();
|
|
539
609
|
const targetToken = this.advance();
|
|
540
|
-
const target = targetToken.value
|
|
541
|
-
if (target === "
|
|
610
|
+
const target = targetToken.value;
|
|
611
|
+
if (target.toLowerCase() === "reqbody" && target !== "reqBody") {
|
|
612
|
+
throw new Error(`Syntax Error on line ${targetToken.line}: Invalid keyword '${target}'. Flowstride uses strict camelCase. Did you mean 'reqBody'?`);
|
|
613
|
+
}
|
|
614
|
+
if (target.toLowerCase() === "reqheader" && target !== "reqHeader") {
|
|
615
|
+
throw new Error(`Syntax Error on line ${targetToken.line}: Invalid keyword '${target}'. Flowstride uses strict camelCase. Did you mean 'reqHeader'?`);
|
|
616
|
+
}
|
|
617
|
+
if (target === "reqHeader") {
|
|
542
618
|
const hKey = this.consume(lexer_1.TokenType.STRING, "Header key expected").value;
|
|
543
619
|
const hVal = this.consume(lexer_1.TokenType.STRING, "Header value expected").value;
|
|
544
620
|
payload.headers.push({ key: hKey, value: hVal });
|
|
545
621
|
}
|
|
546
|
-
else if (target === "
|
|
622
|
+
else if (target === "reqBody") {
|
|
547
623
|
payload.body = this.consume(lexer_1.TokenType.STRING, "Body content expected").value;
|
|
548
624
|
}
|
|
549
625
|
else {
|
|
@@ -9,6 +9,7 @@ export declare class FlowOrchestrator {
|
|
|
9
9
|
private globalTestScenarios;
|
|
10
10
|
private stepStatusRegistry;
|
|
11
11
|
private pdfGenerator;
|
|
12
|
+
private disconnectTimer;
|
|
12
13
|
constructor(worker: FlowWorker, workerCount?: number, config?: FlowstrideConfig | undefined);
|
|
13
14
|
private forwardToDashboard;
|
|
14
15
|
private executeTestRun;
|
|
@@ -53,6 +53,7 @@ class FlowOrchestrator {
|
|
|
53
53
|
globalTestScenarios = [];
|
|
54
54
|
stepStatusRegistry = new Map();
|
|
55
55
|
pdfGenerator;
|
|
56
|
+
disconnectTimer = null;
|
|
56
57
|
constructor(worker, workerCount = 1, config) {
|
|
57
58
|
this.worker = worker;
|
|
58
59
|
this.workerCount = workerCount;
|
|
@@ -222,6 +223,10 @@ class FlowOrchestrator {
|
|
|
222
223
|
});
|
|
223
224
|
this.wss = new ws_1.WebSocketServer({ server: this.httpServer });
|
|
224
225
|
this.wss.on("connection", (ws) => {
|
|
226
|
+
if (this.disconnectTimer) {
|
|
227
|
+
clearTimeout(this.disconnectTimer);
|
|
228
|
+
this.disconnectTimer = null;
|
|
229
|
+
}
|
|
225
230
|
ws.on("message", async (message) => {
|
|
226
231
|
const data = JSON.parse(message.toString());
|
|
227
232
|
if (data.type === "EXPORT_PDF_REQ") {
|
|
@@ -255,6 +260,21 @@ class FlowOrchestrator {
|
|
|
255
260
|
this.forwardToDashboard("RUN_COMPLETE", { status: "aborted" });
|
|
256
261
|
}
|
|
257
262
|
});
|
|
263
|
+
ws.on("close", () => {
|
|
264
|
+
if (this.wss && this.wss.clients.size === 0) {
|
|
265
|
+
this.disconnectTimer = setTimeout(async () => {
|
|
266
|
+
console.log(chalk_1.default.yellow("\n[Flowstride]: Dashboard disconnected. Shutting down framework..."));
|
|
267
|
+
this.worker.abort();
|
|
268
|
+
try {
|
|
269
|
+
await this.worker.shutdown();
|
|
270
|
+
}
|
|
271
|
+
catch (e) { }
|
|
272
|
+
this.wss?.close();
|
|
273
|
+
this.httpServer?.close();
|
|
274
|
+
process.exit(0);
|
|
275
|
+
}, 3000);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
258
278
|
});
|
|
259
279
|
this.httpServer.listen(0, async () => {
|
|
260
280
|
const address = this.httpServer?.address();
|
|
@@ -273,11 +293,6 @@ class FlowOrchestrator {
|
|
|
273
293
|
console.log(chalk_1.default.green("[Dashboard]: Connected to UI!"));
|
|
274
294
|
await this.executeTestRun(targetPath);
|
|
275
295
|
});
|
|
276
|
-
process.on("SIGINT", () => {
|
|
277
|
-
this.wss?.close();
|
|
278
|
-
this.httpServer?.close();
|
|
279
|
-
process.exit();
|
|
280
|
-
});
|
|
281
296
|
}
|
|
282
297
|
catch (e) {
|
|
283
298
|
throw e;
|