flowstride 1.0.17 → 1.0.19

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.
@@ -25,6 +25,8 @@ export declare class WebAdapter {
25
25
  synthesizeCurl(method: string, url: string, headers: Record<string, string>, postData: string | null): string;
26
26
  private checkAmbiguity;
27
27
  close(targetText: string): Promise<void>;
28
+ extractText(selector: string, elementType?: string): Promise<string>;
29
+ extractValue(selector: string, elementType?: string): Promise<string>;
28
30
  click(selector: string, elementType?: string): Promise<void>;
29
31
  hover(selector: string, elementType?: string): Promise<void>;
30
32
  type(selector: string, value: string, elementType?: string): Promise<void>;
@@ -43,6 +45,7 @@ export declare class WebAdapter {
43
45
  expectValue(selector: string, val: string, elementType?: string): Promise<void>;
44
46
  expectTranscript(expectedText: string): Promise<void>;
45
47
  getAttribute(selector: string, attr: string, elementType?: string): Promise<string | null>;
48
+ private translateCoordinateSelector;
46
49
  private isRawCssSelector;
47
50
  private buildHeuristicSelector;
48
51
  private getLocator;
@@ -62,13 +62,13 @@ class WebAdapter {
62
62
  ? [
63
63
  "--use-fake-ui-for-media-stream",
64
64
  "--use-fake-device-for-media-stream",
65
- "--disable-blink-features=AutomationControlled", // Cloudflare Mask
65
+ "--disable-blink-features=AutomationControlled",
66
66
  ]
67
67
  : [
68
68
  "--window-size=1280,800",
69
69
  "--use-fake-ui-for-media-stream",
70
70
  "--use-fake-device-for-media-stream",
71
- "--disable-blink-features=AutomationControlled", // Cloudflare Mask
71
+ "--disable-blink-features=AutomationControlled",
72
72
  ];
73
73
  if (audioInjectionPath) {
74
74
  launchArgs.push(`--use-file-for-fake-audio-capture=${audioInjectionPath}`);
@@ -390,6 +390,18 @@ class WebAdapter {
390
390
  await this.page.mouse.up();
391
391
  await this.page.waitForTimeout(300);
392
392
  }
393
+ async extractText(selector, elementType) {
394
+ const loc = await this.getLocator(selector, elementType, false, false);
395
+ const targetLoc = await this.checkAmbiguity(loc, selector);
396
+ await targetLoc.waitFor({ state: "visible" });
397
+ return (await targetLoc.innerText()) || "";
398
+ }
399
+ async extractValue(selector, elementType) {
400
+ const loc = await this.getLocator(selector, elementType, false, false);
401
+ const targetLoc = await this.checkAmbiguity(loc, selector);
402
+ await targetLoc.waitFor({ state: "visible" });
403
+ return (await targetLoc.inputValue()) || "";
404
+ }
393
405
  async click(selector, elementType) {
394
406
  const loc = await this.getLocator(selector, elementType, false, false);
395
407
  const targetLoc = await this.checkAmbiguity(loc, selector);
@@ -402,7 +414,6 @@ class WebAdapter {
402
414
  const targetLoc = await this.checkAmbiguity(loc, selector);
403
415
  await targetLoc.waitFor({ state: "visible" });
404
416
  await targetLoc.scrollIntoViewIfNeeded();
405
- // 1. Accessibility WAI-ARIA Hack (Focus)
406
417
  await targetLoc.focus({ timeout: 3000 }).catch(() => { });
407
418
  await targetLoc
408
419
  .evaluate((node) => {
@@ -410,21 +421,17 @@ class WebAdapter {
410
421
  node.focus();
411
422
  })
412
423
  .catch(() => { });
413
- // 2. Get Exact Center Coordinates for Physical Move
414
424
  let box = null;
415
425
  try {
416
426
  box = await targetLoc.boundingBox({ timeout: 3000 });
417
427
  }
418
- catch (e) { } // Catch in case it mutated instantly during focus
428
+ catch (e) { }
419
429
  if (box) {
420
430
  const centerX = box.x + box.width / 2;
421
431
  const centerY = box.y + box.height / 2;
422
- // Physically drag the mouse to the element to satisfy Hover Intent APIs
423
432
  await this.page.mouse.move(centerX, centerY, { steps: 5 });
424
433
  }
425
- // 3. Playwright Native Hover Backup (Caught to prevent mutation crashes)
426
434
  await targetLoc.hover({ force: true, timeout: 3000 }).catch(() => { });
427
- // 4. The Ultimate Wakeup Call: Synthesize Full Coordinate-Loaded Events
428
435
  await targetLoc
429
436
  .evaluate((node) => {
430
437
  const rect = node.getBoundingClientRect();
@@ -450,11 +457,9 @@ class WebAdapter {
450
457
  });
451
458
  })
452
459
  .catch(() => { });
453
- // 5. We perform a programmatic click that won't trigger W3C navigation if handled by React toggles
454
460
  await targetLoc
455
461
  .click({ delay: 50, force: true, timeout: 3000 })
456
462
  .catch(() => { });
457
- // 6. The Paint Buffer
458
463
  await this.page.waitForTimeout(600);
459
464
  }
460
465
  async type(selector, value, elementType) {
@@ -902,7 +907,7 @@ class WebAdapter {
902
907
  .replace(/[^\w\s]/g, "")
903
908
  .replace(/\s+/g, " ");
904
909
  if (cleanBody.includes(cleanExpected)) {
905
- return; // Assertion passed!
910
+ return;
906
911
  }
907
912
  await this.page.waitForTimeout(500);
908
913
  }
@@ -912,9 +917,32 @@ class WebAdapter {
912
917
  const loc = await this.getLocator(selector, elementType, true, true);
913
918
  return await loc.first().getAttribute(attr);
914
919
  }
920
+ translateCoordinateSelector(selector) {
921
+ const cleanSelector = selector.replace(/^['"]|['"]$/g, "").trim();
922
+ if (!cleanSelector)
923
+ return null;
924
+ const parts = cleanSelector.split(/\s+/);
925
+ const validTags = ["table", "tr", "th", "td"];
926
+ const isCoordinatePath = parts.every((part) => validTags.some((tag) => part.toLowerCase().startsWith(tag)));
927
+ if (!isCoordinatePath)
928
+ return null;
929
+ let translated = "";
930
+ for (let i = 0; i < parts.length; i++) {
931
+ const part = parts[i];
932
+ const match = part.match(/^([a-z]+)(?:\[(\d+)\])?$/i);
933
+ if (!match)
934
+ return null;
935
+ const tag = match[1];
936
+ const index = match[2] ? ` >> nth=${match[2]}` : "";
937
+ translated += (i > 0 ? " >> " : "") + tag + index;
938
+ }
939
+ return translated;
940
+ }
915
941
  isRawCssSelector(selector) {
916
942
  if (/\b(near|above|under|below|leftOf|rightOf|inside|in)\s+['"]/i.test(selector))
917
943
  return false;
944
+ if (selector.includes(">> nth="))
945
+ return true;
918
946
  if (/^[#\.\[]/.test(selector))
919
947
  return true;
920
948
  if (/^[a-zA-Z0-9]+\s*[\.\#:][a-zA-Z0-9_\-]+/.test(selector))
@@ -954,6 +982,9 @@ class WebAdapter {
954
982
  "iframe",
955
983
  "img",
956
984
  "svg",
985
+ "tr",
986
+ "th",
987
+ "td",
957
988
  ];
958
989
  if (htmlTags.includes(selector.toLowerCase()))
959
990
  return true;
@@ -995,27 +1026,35 @@ class WebAdapter {
995
1026
  return `:is(${attributes}, ${partialActionable})`;
996
1027
  }
997
1028
  async getLocator(rawSelector, elementType, isForceAction = false, isAssertionOrAnchor = false, fuzzy = false) {
998
- if (this.isRawCssSelector(rawSelector.trim())) {
999
- let loc = this.page.locator(rawSelector);
1029
+ const translatedRaw = this.translateCoordinateSelector(rawSelector);
1030
+ const processingSelector = translatedRaw || rawSelector;
1031
+ if (this.isRawCssSelector(processingSelector.trim())) {
1032
+ let loc = this.page.locator(processingSelector);
1000
1033
  if (!isForceAction)
1001
1034
  loc = loc.filter({ visible: true });
1002
1035
  return loc;
1003
1036
  }
1004
- let target = rawSelector;
1037
+ let target = processingSelector;
1005
1038
  let relation = "";
1006
1039
  let anchor = "";
1007
1040
  const spatialRegex = /\b(near|above|under|below|leftOf|rightOf|inside|in)\s+(['"].*?['"])/i;
1008
- const match = rawSelector.match(spatialRegex);
1041
+ const match = processingSelector.match(spatialRegex);
1009
1042
  if (match) {
1010
- target = rawSelector.substring(0, match.index).trim();
1043
+ target = processingSelector.substring(0, match.index).trim();
1011
1044
  relation = match[1].toLowerCase();
1012
1045
  anchor = match[2];
1013
1046
  }
1047
+ const translatedTarget = this.translateCoordinateSelector(target);
1048
+ if (translatedTarget) {
1049
+ target = translatedTarget;
1050
+ }
1014
1051
  let arrayIndex = null;
1015
- const indexMatch = target.match(/\[(\d+)\]/);
1016
- if (indexMatch) {
1017
- arrayIndex = parseInt(indexMatch[1], 10);
1018
- target = target.replace(indexMatch[0], "").trim();
1052
+ if (!translatedTarget) {
1053
+ const indexMatch = target.match(/\[(\d+)\]/);
1054
+ if (indexMatch) {
1055
+ arrayIndex = parseInt(indexMatch[1], 10);
1056
+ target = target.replace(indexMatch[0], "").trim();
1057
+ }
1019
1058
  }
1020
1059
  const targetClean = target.replace(/^['"]|['"]$/g, "");
1021
1060
  const targetIsRaw = this.isRawCssSelector(targetClean);
@@ -1025,10 +1064,12 @@ class WebAdapter {
1025
1064
  let alternativeSelector = "";
1026
1065
  if (relation && anchor) {
1027
1066
  const anchorClean = anchor.replace(/^['"]|['"]$/g, "");
1028
- const anchorIsRaw = this.isRawCssSelector(anchorClean);
1067
+ const translatedAnchor = this.translateCoordinateSelector(anchorClean);
1068
+ const finalAnchorClean = translatedAnchor || anchorClean;
1069
+ const anchorIsRaw = this.isRawCssSelector(finalAnchorClean);
1029
1070
  const anchorSelector = anchorIsRaw
1030
- ? anchorClean
1031
- : this.buildHeuristicSelector(anchorClean, undefined, true, fuzzy);
1071
+ ? finalAnchorClean
1072
+ : this.buildHeuristicSelector(finalAnchorClean, undefined, true, fuzzy);
1032
1073
  const anchorLoc = this.page.locator(anchorSelector).first();
1033
1074
  try {
1034
1075
  if ((await anchorLoc.count()) > 0) {
@@ -1055,7 +1096,9 @@ class WebAdapter {
1055
1096
  break;
1056
1097
  case "inside":
1057
1098
  case "in":
1058
- alternativeSelector = `${anchorSelector}:has-text("${targetClean}")`;
1099
+ if (!anchorSelector.includes("nth=")) {
1100
+ alternativeSelector = `${anchorSelector}:has-text("${targetClean}")`;
1101
+ }
1059
1102
  finalSelector = `${anchorSelector} >> ${finalSelector}`;
1060
1103
  break;
1061
1104
  }
@@ -46,17 +46,22 @@ const ora_1 = __importDefault(require("ora"));
46
46
  const execAsync = (0, util_1.promisify)(child_process_1.exec);
47
47
  async function initAction() {
48
48
  const root = process.cwd();
49
- const folders = ["flows", "reports", "plugins", "fixtures"];
49
+ // Added 'manual_flows' to strictly isolate manual test designs
50
+ const folders = ["flows", "manual_flows", "reports", "plugins", "fixtures"];
50
51
  const sampleFlowPath = path.join(root, "flows", "example.flow");
51
52
  const fixturesReadmePath = path.join(root, "fixtures", "README.md");
52
53
  const gitignorePath = path.join(root, ".gitignore");
53
- console.log("\n[Info] Initializing new workspace...");
54
+ console.log("\n[Info] Initialising new workspace...");
54
55
  // 1. Create Directories
55
56
  folders.forEach((folder) => {
56
57
  const dirPath = path.join(root, folder);
57
58
  if (!fs.existsSync(dirPath)) {
58
59
  fs.mkdirSync(dirPath);
59
60
  console.log(`[OK] Created directory: ${folder}/`);
61
+ // Securely ensure the manual_flows directory is strictly tracked by Git even when empty
62
+ if (folder === "manual_flows") {
63
+ fs.writeFileSync(path.join(dirPath, ".gitkeep"), "");
64
+ }
60
65
  }
61
66
  else {
62
67
  console.log(`[Info] Directory already exists: ${folder}/`);
@@ -0,0 +1 @@
1
+ export declare function openAction(): Promise<void>;
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.openAction = openAction;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const workspace_server_1 = require("../manual_server/workspace_server");
9
+ async function openAction() {
10
+ console.log(chalk_1.default.cyan("\nšŸŽØ Flowstride: Launching Local Manual Test Design Workspace..."));
11
+ await (0, workspace_server_1.startManualWorkspaceServer)();
12
+ }
package/dist/cli/index.js CHANGED
@@ -41,6 +41,7 @@ const commander_1 = require("commander");
41
41
  const run_1 = require("./commands/run");
42
42
  const report_1 = require("./commands/report");
43
43
  const init_1 = require("./commands/init");
44
+ const open_1 = require("./commands/open");
44
45
  const fs = __importStar(require("fs"));
45
46
  const path = __importStar(require("path"));
46
47
  const os = __importStar(require("os"));
@@ -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.17");
57
+ .version("1.0.19");
57
58
  program
58
59
  .command("init")
59
60
  .description("Initialise a new Flowstride workspace in the current directory")
@@ -374,4 +375,10 @@ program
374
375
  .action(async () => {
375
376
  await (0, report_1.reportAction)();
376
377
  });
378
+ program
379
+ .command("open")
380
+ .description("Launch the local manual QA UI Workspace")
381
+ .action(async () => {
382
+ await (0, open_1.openAction)();
383
+ });
377
384
  program.parse(process.argv);
@@ -0,0 +1 @@
1
+ export declare function startManualWorkspaceServer(): Promise<void>;