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.
@@ -55,6 +55,7 @@ class FlowWorker extends events_1.EventEmitter {
55
55
  pluginManager;
56
56
  isSilent = process.env.FLOWSTRIDE_PARALLEL === "true";
57
57
  isAborted = false;
58
+ isDebugHalted = false;
58
59
  currentStepContext = "Initializing...";
59
60
  currentFileName = "System";
60
61
  activeAudioPath = undefined;
@@ -171,6 +172,8 @@ class FlowWorker extends events_1.EventEmitter {
171
172
  extractFromJsonPath(obj, path) {
172
173
  if (!obj)
173
174
  return undefined;
175
+ if (!path)
176
+ return obj;
174
177
  const parts = path.replace(/\[(\d+)\]/g, ".$1").split(".");
175
178
  let current = obj;
176
179
  for (const part of parts) {
@@ -186,11 +189,11 @@ class FlowWorker extends events_1.EventEmitter {
186
189
  const state = this.api.getState();
187
190
  for (const ext of extracts) {
188
191
  let val;
189
- if (ext.target === "resbody") {
192
+ if (ext.target === "resBody") {
190
193
  val = this.extractFromJsonPath(state.body, ext.jsonPath);
191
194
  }
192
- else if (ext.target === "resheader") {
193
- val = state.headers?.[ext.jsonPath.toLowerCase()];
195
+ else if (ext.target === "resHeader") {
196
+ val = state.headers?.[(ext.jsonPath || "").toLowerCase()];
194
197
  }
195
198
  else if (ext.target === "cookie") {
196
199
  const cookies = state.headers?.["set-cookie"];
@@ -340,136 +343,174 @@ class FlowWorker extends events_1.EventEmitter {
340
343
  this.config.headless = true;
341
344
  this.activeAudioPath = undefined;
342
345
  this.isAborted = false;
343
- for (const filePath of filesToProcess) {
344
- const content = fs.readFileSync(filePath, "utf8");
345
- const audioMatch = content.match(/(?:flow\.)?injectAudio\s+["']([^"']+)["']/i);
346
- if (audioMatch && audioMatch[1]) {
347
- const rawPath = audioMatch[1];
348
- const absolutePath = path.resolve(process.cwd(), rawPath);
349
- if (!fs.existsSync(absolutePath)) {
350
- throw new Error(`Pre-launch initialization failed: Audio file not found at ${absolutePath}`);
351
- }
352
- this.activeAudioPath = absolutePath;
353
- break;
346
+ this.isDebugHalted = false;
347
+ const sigintHandler = async () => {
348
+ if (!this.isSilent) {
349
+ console.log(chalk_1.default.yellow("\n[Flowstride]: Process interrupted. Gracefully shutting down..."));
354
350
  }
355
- }
356
- for (const filePath of filesToProcess) {
357
- if (this.isAborted)
358
- break;
359
- const fileStartTime = Date.now();
351
+ this.abort();
352
+ this.broadcast("SERVER_SHUTDOWN", { message: "Terminal closed." });
360
353
  try {
361
- const fileName = path.basename(filePath);
362
- this.currentFileName = fileName;
363
- this.localVariables = {};
364
- this.localBrowserSessions = {};
365
- this.api.resetState();
366
- this.dataGen = new data_generators_1.DataGenerators();
367
- await this.web.initialise(this.activeAudioPath);
368
- await this.startStreaming();
369
- await this.setupNetworkInterception();
354
+ await this.web.shutdown();
355
+ }
356
+ catch (e) { }
357
+ process.exit(0);
358
+ };
359
+ process.once("SIGINT", sigintHandler);
360
+ try {
361
+ for (const filePath of filesToProcess) {
370
362
  const content = fs.readFileSync(filePath, "utf8");
371
- const hasSessionIntent = content.includes("use session") ||
372
- content.includes("use global session");
373
- if (!hasSessionIntent) {
374
- try {
375
- const page = this.web.getPage();
376
- if (page) {
377
- await page
378
- .context()
379
- .clearCookies()
363
+ const audioMatch = content.match(/(?:flow\.)?injectAudio\s+["']([^"']+)["']/i);
364
+ if (audioMatch && audioMatch[1]) {
365
+ const rawPath = audioMatch[1];
366
+ const absolutePath = path.resolve(process.cwd(), rawPath);
367
+ if (!fs.existsSync(absolutePath)) {
368
+ throw new Error(`Pre-launch initialization failed: Audio file not found at ${absolutePath}`);
369
+ }
370
+ this.activeAudioPath = absolutePath;
371
+ break;
372
+ }
373
+ }
374
+ for (const filePath of filesToProcess) {
375
+ if (this.isAborted || this.isDebugHalted)
376
+ break;
377
+ const fileStartTime = Date.now();
378
+ try {
379
+ const fileName = path.basename(filePath);
380
+ this.currentFileName = fileName;
381
+ this.localVariables = {};
382
+ this.localBrowserSessions = {};
383
+ this.api.resetState();
384
+ this.dataGen = new data_generators_1.DataGenerators();
385
+ await this.web.initialise(this.activeAudioPath);
386
+ await this.startStreaming();
387
+ await this.setupNetworkInterception();
388
+ const content = fs.readFileSync(filePath, "utf8");
389
+ const hasSessionIntent = content.includes("use session") ||
390
+ content.includes("use global session");
391
+ if (!hasSessionIntent) {
392
+ try {
393
+ const page = this.web.getPage();
394
+ if (page) {
395
+ await page
396
+ .context()
397
+ .clearCookies()
398
+ .catch(() => { });
399
+ await page
400
+ .evaluate(() => {
401
+ try {
402
+ localStorage.clear();
403
+ }
404
+ catch (e) { }
405
+ try {
406
+ sessionStorage.clear();
407
+ }
408
+ catch (e) { }
409
+ })
410
+ .catch(() => { });
411
+ }
412
+ }
413
+ catch (e) { }
414
+ }
415
+ if (!this.isSilent)
416
+ console.log(chalk_1.default.blue.bold(`\n[Execution]: Processing file: ${fileName}`));
417
+ this.emitLog("FILE_HEADER", "info", fileName, { filePath });
418
+ const features = new parser_1.Parser(new lexer_1.Lexer(content).tokenise()).parse();
419
+ const hasOnlyMode = features.some((f) => f.scenarios.some((s) => s.isOnly));
420
+ if (hasOnlyMode) {
421
+ if (!this.isSilent) {
422
+ console.log(chalk_1.default.yellow(` > [Debug Mode]: 'only.Scenario' detected. Operating in Ghost Mode.`));
423
+ }
424
+ features.forEach((f) => {
425
+ f.scenarios = f.scenarios.filter((s) => s.isOnly);
426
+ });
427
+ }
428
+ for (const f of features) {
429
+ if (this.isAborted || this.isDebugHalted)
430
+ break;
431
+ if (f.scenarios.length === 0)
432
+ continue;
433
+ if (!this.isSilent)
434
+ console.log(chalk_1.default.magenta.bold(` Feature: ${f.name}`));
435
+ for (const s of f.scenarios) {
436
+ if (this.isAborted || this.isDebugHalted)
437
+ break;
438
+ if (!this.isSilent)
439
+ console.log(chalk_1.default.cyan(` Scenario: ${s.name}`));
440
+ this.broadcast("RUN_START", {
441
+ scenarioName: s.name,
442
+ fileName: this.currentFileName,
443
+ });
444
+ await this.runScenario(s, f.declarations);
445
+ if (this.isDebugHalted)
446
+ break;
447
+ await this.web
448
+ .getPage()
449
+ .waitForLoadState("load", { timeout: 4000 })
380
450
  .catch(() => { });
381
- await page
382
- .evaluate(() => {
383
- try {
384
- localStorage.clear();
385
- }
386
- catch (e) { }
387
- try {
388
- sessionStorage.clear();
389
- }
390
- catch (e) { }
391
- })
451
+ await this.web
452
+ .getPage()
453
+ .waitForLoadState("networkidle", { timeout: 4000 })
392
454
  .catch(() => { });
455
+ await new Promise((resolve) => setTimeout(resolve, 2000));
393
456
  }
394
457
  }
395
- catch (e) { }
396
- }
397
- if (!this.isSilent)
398
- console.log(chalk_1.default.blue.bold(`\n[Execution]: Processing file: ${fileName}`));
399
- this.emitLog("FILE_HEADER", "info", fileName, { filePath });
400
- const features = new parser_1.Parser(new lexer_1.Lexer(content).tokenise()).parse();
401
- for (const f of features) {
402
458
  if (this.isAborted)
403
459
  break;
404
460
  if (!this.isSilent)
405
- console.log(chalk_1.default.magenta.bold(` Feature: ${f.name}`));
406
- for (const s of f.scenarios) {
407
- if (this.isAborted)
408
- break;
461
+ console.log(chalk_1.default.gray("\n[Execution]: Finalizing browser actions..."));
462
+ await new Promise((resolve) => setTimeout(resolve, 2000));
463
+ try {
464
+ const baseName = this.currentFileName.replace(".flow", "");
465
+ const mockCloudDir = path.join(process.cwd(), ".flowstride", "mock-cloud");
466
+ if (!fs.existsSync(mockCloudDir))
467
+ fs.mkdirSync(mockCloudDir, { recursive: true });
468
+ const targetVideoPath = path.join(mockCloudDir, `${baseName}.webm`);
469
+ await this.web.shutdown(targetVideoPath);
409
470
  if (!this.isSilent)
410
- console.log(chalk_1.default.cyan(` Scenario: ${s.name}`));
411
- this.broadcast("RUN_START", {
412
- scenarioName: s.name,
413
- fileName: this.currentFileName,
414
- });
415
- await this.runScenario(s, f.declarations);
416
- await this.web
417
- .getPage()
418
- .waitForLoadState("load", { timeout: 4000 })
419
- .catch(() => { });
420
- await this.web
421
- .getPage()
422
- .waitForLoadState("networkidle", { timeout: 4000 })
423
- .catch(() => { });
424
- await new Promise((resolve) => setTimeout(resolve, 2000));
471
+ console.log(chalk_1.default.gray(` [Cloud Sync]: Video safely encoded and preserved for ${this.currentFileName}`));
425
472
  }
473
+ catch (err) { }
474
+ this.broadcast("FILE_COMPLETE", {
475
+ fileName: this.currentFileName,
476
+ duration: Date.now() - fileStartTime,
477
+ });
426
478
  }
427
- if (this.isAborted)
428
- break;
429
- if (!this.isSilent)
430
- console.log(chalk_1.default.gray("\n[Execution]: Finalizing browser actions..."));
431
- await new Promise((resolve) => setTimeout(resolve, 2000));
432
- try {
433
- const baseName = this.currentFileName.replace(".flow", "");
434
- const mockCloudDir = path.join(process.cwd(), ".flowstride", "mock-cloud");
435
- if (!fs.existsSync(mockCloudDir))
436
- fs.mkdirSync(mockCloudDir, { recursive: true });
437
- const targetVideoPath = path.join(mockCloudDir, `${baseName}.webm`);
438
- await this.web.shutdown(targetVideoPath);
479
+ catch (e) {
480
+ if (this.isAborted)
481
+ break;
439
482
  if (!this.isSilent)
440
- console.log(chalk_1.default.gray(` [Cloud Sync]: Video safely encoded and preserved for ${this.currentFileName}`));
483
+ console.error(chalk_1.default.red(`\n[Execution]: Failed processing ${this.currentFileName}: ${e.message}`));
484
+ try {
485
+ const baseName = this.currentFileName.replace(".flow", "");
486
+ const mockCloudDir = path.join(process.cwd(), ".flowstride", "mock-cloud");
487
+ if (!fs.existsSync(mockCloudDir))
488
+ fs.mkdirSync(mockCloudDir, { recursive: true });
489
+ const targetVideoPath = path.join(mockCloudDir, `${baseName}.webm`);
490
+ await this.web.shutdown(targetVideoPath);
491
+ if (!this.isSilent)
492
+ console.log(chalk_1.default.gray(` [Cloud Sync]: Video safely encoded and preserved for ${this.currentFileName}`));
493
+ }
494
+ catch (err) { }
495
+ this.broadcast("FILE_FAILED", {
496
+ fileName: this.currentFileName,
497
+ duration: Date.now() - fileStartTime,
498
+ error: e.message,
499
+ });
441
500
  }
442
- catch (err) { }
443
- this.broadcast("FILE_COMPLETE", {
444
- fileName: this.currentFileName,
445
- duration: Date.now() - fileStartTime,
446
- });
447
501
  }
448
- catch (e) {
449
- if (this.isAborted)
450
- break;
451
- if (!this.isSilent)
452
- console.error(chalk_1.default.red(`\n[Execution]: Failed processing ${this.currentFileName}: ${e.message}`));
453
- try {
454
- const baseName = this.currentFileName.replace(".flow", "");
455
- const mockCloudDir = path.join(process.cwd(), ".flowstride", "mock-cloud");
456
- if (!fs.existsSync(mockCloudDir))
457
- fs.mkdirSync(mockCloudDir, { recursive: true });
458
- const targetVideoPath = path.join(mockCloudDir, `${baseName}.webm`);
459
- await this.web.shutdown(targetVideoPath);
460
- if (!this.isSilent)
461
- console.log(chalk_1.default.gray(` [Cloud Sync]: Video safely encoded and preserved for ${this.currentFileName}`));
502
+ if (!this.isSilent && !this.isAborted) {
503
+ if (this.isDebugHalted) {
504
+ console.log(chalk_1.default.cyan("\n[Execution]: Test suite halted by debug flag."));
505
+ }
506
+ else {
507
+ console.log(chalk_1.default.green("\n[Execution]: All suites completed successfully."));
462
508
  }
463
- catch (err) { }
464
- this.broadcast("FILE_FAILED", {
465
- fileName: this.currentFileName,
466
- duration: Date.now() - fileStartTime,
467
- error: e.message,
468
- });
469
509
  }
470
510
  }
471
- if (!this.isSilent && !this.isAborted)
472
- console.log(chalk_1.default.green("[Execution]: All suites completed successfully."));
511
+ finally {
512
+ process.removeListener("SIGINT", sigintHandler);
513
+ }
473
514
  }
474
515
  async runScenario(scenario, globalDeclarations = []) {
475
516
  for (const step of scenario.steps) {
@@ -529,6 +570,13 @@ class FlowWorker extends events_1.EventEmitter {
529
570
  screenshot: stepContext.screenshot,
530
571
  fileName: this.currentFileName,
531
572
  });
573
+ if (step.isStop) {
574
+ if (!this.isSilent) {
575
+ console.log(chalk_1.default.cyan(` > [Execution]: Debug stop triggered. Halting global execution.`));
576
+ }
577
+ this.isDebugHalted = true;
578
+ break;
579
+ }
532
580
  }
533
581
  catch (e) {
534
582
  if (this.isAborted)
@@ -561,6 +609,7 @@ class FlowWorker extends events_1.EventEmitter {
561
609
  }
562
610
  const isUIAction = [
563
611
  "click",
612
+ "hover",
564
613
  "type",
565
614
  "forceClick",
566
615
  "forceType",
@@ -639,11 +688,23 @@ class FlowWorker extends events_1.EventEmitter {
639
688
  }
640
689
  }
641
690
  async executeStep(action, payload, localDeclarations = [], globalDeclarations = []) {
691
+ if (payload && payload.selector === "smart-burger-fallback") {
692
+ payload.selector = [
693
+ 'button[aria-label*="menu" i]',
694
+ '[aria-label*="menu" i]',
695
+ '[title*="menu" i]',
696
+ ".bm-burger-button",
697
+ '[class*="hamburger" i]',
698
+ '[class*="burger" i]',
699
+ '[id*="hamburger" i]',
700
+ '[id*="burger" i]',
701
+ ].join(", ");
702
+ }
642
703
  switch (action) {
643
704
  case "extract":
644
705
  this.processExtractions([
645
706
  {
646
- target: payload.target.toLowerCase(),
707
+ target: payload.target,
647
708
  jsonPath: payload.jsonPath,
648
709
  variableName: payload.variableName,
649
710
  isGlobal: payload.isGlobal,
@@ -791,6 +852,9 @@ class FlowWorker extends events_1.EventEmitter {
791
852
  case "click":
792
853
  await this.web.click(payload.selector, payload.elementType);
793
854
  break;
855
+ case "hover":
856
+ await this.web.hover(payload.selector, payload.elementType);
857
+ break;
794
858
  case "forceClick":
795
859
  await this.web.forceClick(payload.selector, payload.elementType);
796
860
  break;
@@ -841,23 +905,27 @@ class FlowWorker extends events_1.EventEmitter {
841
905
  await this.waitForPipeline();
842
906
  break;
843
907
  case "expect":
844
- if (["status", "responsetime", "resbody", "resheader", "cookie"].includes(payload.type)) {
908
+ const expectType = payload.type;
909
+ if (["status", "responseTime", "resBody", "resHeader", "cookie"].includes(expectType)) {
845
910
  const apiState = this.api.getState();
846
- if (payload.type === "status") {
847
- if (String(apiState.status) !== String(payload.statusCode))
848
- throw new Error(`API Assertion Failed: Expected HTTP status ${payload.statusCode}, but got ${apiState.status}`);
911
+ if (expectType === "status") {
912
+ const expectedStatus = payload.statusCode || payload.value;
913
+ if (String(apiState.status) !== String(expectedStatus))
914
+ throw new Error(`API Assertion Failed: Expected HTTP status ${expectedStatus}, but got ${apiState.status}`);
849
915
  }
850
- else if (payload.type === "responsetime") {
851
- if (apiState.responseTime > parseInt(payload.threshold, 10))
852
- throw new Error(`API Assertion Failed: Expected response time to be less than ${payload.threshold}ms, but got ${apiState.responseTime}ms`);
916
+ else if (expectType === "responseTime") {
917
+ const threshold = payload.threshold || payload.value;
918
+ if (apiState.responseTime > parseInt(threshold, 10))
919
+ throw new Error(`API Assertion Failed: Expected response time to be less than ${threshold}ms, but got ${apiState.responseTime}ms`);
853
920
  }
854
921
  else {
855
922
  let actualValue;
856
- if (payload.type === "resbody")
923
+ if (expectType === "resBody")
857
924
  actualValue = this.extractFromJsonPath(apiState.body, payload.jsonPath);
858
- else if (payload.type === "resheader")
859
- actualValue = apiState.headers?.[payload.jsonPath.toLowerCase()];
860
- else if (payload.type === "cookie") {
925
+ else if (expectType === "resHeader")
926
+ actualValue =
927
+ apiState.headers?.[(payload.jsonPath || "").toLowerCase()];
928
+ else if (expectType === "cookie") {
861
929
  const cookies = apiState.headers?.["set-cookie"];
862
930
  if (Array.isArray(cookies)) {
863
931
  const match = cookies.find((c) => c.startsWith(`${payload.jsonPath}=`));
@@ -865,7 +933,12 @@ class FlowWorker extends events_1.EventEmitter {
865
933
  actualValue = match.split("=")[1].split(";")[0];
866
934
  }
867
935
  }
868
- for (const assertion of payload.assertions) {
936
+ const assertionsArray = Array.isArray(payload.assertions)
937
+ ? payload.assertions
938
+ : payload.condition
939
+ ? [{ condition: payload.condition, value: payload.value }]
940
+ : [];
941
+ for (const assertion of assertionsArray) {
869
942
  const condition = assertion.condition;
870
943
  const rawExpected = assertion.value;
871
944
  const expectedLower = String(rawExpected || "").toLowerCase();
@@ -1014,13 +1087,13 @@ class FlowWorker extends events_1.EventEmitter {
1014
1087
  }
1015
1088
  }
1016
1089
  }
1017
- else if (payload.type === "visible")
1090
+ else if (expectType === "visible")
1018
1091
  await this.web.expectVisible(payload.selector, payload.elementType);
1019
- else if (payload.type === "text")
1092
+ else if (expectType === "text")
1020
1093
  await this.web.expectText(payload.selector, payload.fuzzy, payload.elementType);
1021
- else if (payload.type === "value")
1094
+ else if (expectType === "value")
1022
1095
  await this.web.expectValue(payload.selector, payload.text, payload.elementType);
1023
- else if (payload.type === "transcript")
1096
+ else if (expectType === "transcript")
1024
1097
  await this.web.expectTranscript(payload.text);
1025
1098
  break;
1026
1099
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowstride",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "description": "An opensource flowfirst test automation framework",
5
5
  "workspaces": [
6
6
  "packages/flowstride_report"