@dev-blinq/cucumber_client 1.0.1173-dev → 1.0.1173-stage

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.
Files changed (47) hide show
  1. package/bin/assets/bundled_scripts/recorder.js +220 -0
  2. package/bin/assets/preload/accessibility.js +1 -1
  3. package/bin/assets/preload/find_context.js +1 -1
  4. package/bin/assets/preload/generateSelector.js +24 -0
  5. package/bin/assets/preload/locators.js +18 -0
  6. package/bin/assets/preload/recorderv3.js +85 -11
  7. package/bin/assets/preload/unique_locators.js +24 -3
  8. package/bin/assets/scripts/aria_snapshot.js +235 -0
  9. package/bin/assets/scripts/dom_attr.js +372 -0
  10. package/bin/assets/scripts/dom_element.js +0 -0
  11. package/bin/assets/scripts/dom_parent.js +185 -0
  12. package/bin/assets/scripts/event_utils.js +105 -0
  13. package/bin/assets/scripts/pw.js +7886 -0
  14. package/bin/assets/scripts/recorder.js +1147 -0
  15. package/bin/assets/scripts/snapshot_capturer.js +155 -0
  16. package/bin/assets/scripts/unique_locators.js +844 -0
  17. package/bin/assets/scripts/yaml.js +4770 -0
  18. package/bin/assets/templates/page_template.txt +2 -16
  19. package/bin/assets/templates/utils_template.txt +65 -12
  20. package/bin/client/cli_helpers.js +0 -1
  21. package/bin/client/code_cleanup/utils.js +43 -14
  22. package/bin/client/code_gen/code_inversion.js +112 -18
  23. package/bin/client/code_gen/index.js +3 -0
  24. package/bin/client/code_gen/page_reflection.js +37 -20
  25. package/bin/client/code_gen/playwright_codeget.js +152 -48
  26. package/bin/client/cucumber/feature.js +96 -42
  27. package/bin/client/cucumber/project_to_document.js +8 -7
  28. package/bin/client/cucumber/steps_definitions.js +59 -16
  29. package/bin/client/local_agent.js +9 -7
  30. package/bin/client/operations/dump_tree.js +159 -8
  31. package/bin/client/playground/playground.js +1 -1
  32. package/bin/client/project.js +6 -2
  33. package/bin/client/recorderv3/bvt_recorder.js +236 -79
  34. package/bin/client/recorderv3/cli.js +1 -0
  35. package/bin/client/recorderv3/implemented_steps.js +111 -11
  36. package/bin/client/recorderv3/index.js +45 -4
  37. package/bin/client/recorderv3/network.js +299 -0
  38. package/bin/client/recorderv3/step_runner.js +179 -13
  39. package/bin/client/recorderv3/step_utils.js +159 -14
  40. package/bin/client/recorderv3/update_feature.js +55 -30
  41. package/bin/client/recording.js +8 -0
  42. package/bin/client/run_cucumber.js +116 -4
  43. package/bin/client/scenario_report.js +112 -50
  44. package/bin/client/test_scenario.js +0 -1
  45. package/bin/index.js +1 -0
  46. package/package.json +15 -8
  47. package/bin/client/code_gen/get_implemented_steps.js +0 -27
@@ -2,7 +2,7 @@ function getElementAccessibleDescription(element, includeHidden) {
2
2
  let accessibleDescription = null;
3
3
 
4
4
  // https://w3c.github.io/accname/#mapping_additional_nd_description
5
- // https://www.w3.org/TR/html-aam-1.0/#accdesc-computation
5
+ // https://www.w3.org/TR/html-aam-1.0/#accdesc-computation
6
6
  accessibleDescription = "";
7
7
 
8
8
  if (element.hasAttribute("aria-describedby")) {
@@ -511,7 +511,7 @@ function findContext(element) {
511
511
  const textsInnerText = [];
512
512
  for (let i = 0; i < texts.length; i++) {
513
513
  const text = texts[i];
514
- textsInnerText.push(text.textContent);
514
+ textsInnerText.push(text.parentElement.textContent.trim());
515
515
  // set attribute to the text: blinq-text-${key}-${i}
516
516
  text.parentElement.setAttribute(`blinq-text-${key}-${i}`, "");
517
517
  }
@@ -196,6 +196,30 @@ function escapeRegExp(s) {
196
196
  // From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
197
197
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
198
198
  }
199
+ function enclosingShadowRootOrDocument(element) {
200
+ let node = element;
201
+ while (node.parentNode) node = node.parentNode;
202
+ if (node.nodeType === 11 /* Node.DOCUMENT_FRAGMENT_NODE */ || node.nodeType === 9 /* Node.DOCUMENT_NODE */)
203
+ return node;
204
+ }
205
+ function getIdRefs(element, ref) {
206
+ if (!ref) return [];
207
+ const root = enclosingShadowRootOrDocument(element);
208
+ if (!root) return [];
209
+ try {
210
+ const ids = ref.split(" ").filter((id) => !!id);
211
+ const set = new Set();
212
+ for (const id of ids) {
213
+ // https://www.w3.org/TR/wai-aria-1.2/#mapping_additional_relations_error_processing
214
+ // "If more than one element has the same ID, the user agent SHOULD use the first element found with the given ID"
215
+ const firstElement = root.querySelector("#" + CSS.escape(id));
216
+ if (firstElement) set.add(firstElement);
217
+ }
218
+ return [...set];
219
+ } catch (e) {
220
+ return [];
221
+ }
222
+ }
199
223
  function getAriaLabelledByElements(element) {
200
224
  const ref = element.getAttribute("aria-labelledby");
201
225
  if (ref === null) return null;
@@ -697,6 +697,24 @@ function querySelector_shadow_aware(element, selector) {
697
697
  }
698
698
  window.querySelector_shadow_aware = querySelector_shadow_aware;
699
699
 
700
+ function findElement(css, scope = document) {
701
+ // return null if no css provided
702
+ if (!css) return null;
703
+ const selector = window.__injectedScript.parseSelector(css);
704
+ try {
705
+ const elements = window.__injectedScript.querySelectorAll(selector, scope, true);
706
+ if (!elements) return null;
707
+ if (elements.length === 1) {
708
+ return elements[0];
709
+ } else {
710
+ return elements;
711
+ }
712
+ } catch (error) {
713
+ return null;
714
+ }
715
+ }
716
+ window.findElement = findElement;
717
+
700
718
  // export {
701
719
  // getUniqueLocators,
702
720
  // getBasicInfo,
@@ -304,13 +304,16 @@ function findMatchingElements(inputSnapshot, objectSet) {
304
304
 
305
305
  // For each subtree, check for matches in the object set
306
306
  subtrees.forEach((subtree) => {
307
- const subtreeText = subtree.join("\n");
307
+ const subtreeText = subtree.map((t) => t.trim()).join("\n");
308
308
 
309
309
  // Look for matching snapshots in the object set
310
310
  for (const obj of objectSet) {
311
311
  // Normalize snapshots for comparison (trim whitespace, etc.)
312
312
  const normalizedSubtree = subtreeText.trim();
313
- const normalizedSnapshot = obj.snapshot.trim();
313
+ const normalizedSnapshot = obj.snapshot
314
+ .split("\n")
315
+ .map((s) => s.trim())
316
+ .join("\n");
314
317
 
315
318
  // Check if the current object's snapshot matches our subtree
316
319
  if (normalizedSnapshot === normalizedSubtree) {
@@ -456,7 +459,7 @@ class BVTRecorder {
456
459
  getAction: (e) => {
457
460
  consumeEvent(e);
458
461
  },
459
- getInterestedElement: () => {},
462
+ getInterestedElement: () => { },
460
463
  hoverOutlineStyle: "",
461
464
  });
462
465
 
@@ -471,7 +474,7 @@ class BVTRecorder {
471
474
  };
472
475
  },
473
476
  getAction: () => null,
474
- getInterestedElement: () => {},
477
+ getInterestedElement: () => { },
475
478
  hoverOutlineStyle: "",
476
479
  });
477
480
 
@@ -486,7 +489,7 @@ class BVTRecorder {
486
489
  };
487
490
  },
488
491
  getAction: () => null,
489
- getInterestedElement: () => {},
492
+ getInterestedElement: () => { },
490
493
  hoverOutlineStyle: "",
491
494
  });
492
495
 
@@ -916,6 +919,65 @@ class BVTRecorder {
916
919
  }
917
920
  this.contextElement = null;
918
921
  }
922
+ getElementProperties(element) {
923
+ if (!element || !(element instanceof HTMLElement || element instanceof SVGElement)) {
924
+ throw new Error("Please provide a valid HTML element");
925
+ }
926
+
927
+ const result = {
928
+ properties: {},
929
+ attributes: {},
930
+ dataset: {},
931
+ };
932
+
933
+ const unsortedProperties = {};
934
+ const unsortedAttributes = {};
935
+ const unsortedDataset = {};
936
+
937
+ // Get enumerable properties
938
+ for (const prop in element) {
939
+ try {
940
+ const value = element[prop];
941
+ if (
942
+ typeof value !== "function" &&
943
+ typeof value !== "object" &&
944
+ value !== null &&
945
+ value !== undefined &&
946
+ !prop.includes("_")
947
+ ) {
948
+ unsortedProperties[prop] = value;
949
+ }
950
+ } catch (error) {
951
+ unsortedProperties[prop] = "[Error accessing property]";
952
+ }
953
+ }
954
+
955
+ // Get all attributes
956
+ if (element.attributes) {
957
+ for (const attr of element.attributes) {
958
+ if (attr.name === "data-blinq-id" || attr.name === "data-input-id") continue;
959
+ unsortedAttributes[attr.name] = attr.value;
960
+ }
961
+ }
962
+
963
+ // Get dataset properties (data-* attributes)
964
+ if (element.dataset) {
965
+ for (const [key, value] of Object.entries(element.dataset)) {
966
+ if (key === "blinqId" || key === "inputId") continue;
967
+ unsortedDataset[key] = value;
968
+ }
969
+ }
970
+
971
+ // Sort each object by key
972
+ const sortByKey = (obj) => Object.fromEntries(Object.entries(obj).sort(([a], [b]) => a.localeCompare(b)));
973
+
974
+ result.properties = sortByKey(unsortedProperties);
975
+ result.attributes = sortByKey(unsortedAttributes);
976
+ result.dataset = sortByKey(unsortedDataset);
977
+
978
+ return result;
979
+ }
980
+
919
981
  getElementDetails(el, type) {
920
982
  if (lastInputId !== el.dataset.inputId || type !== "input") {
921
983
  el.dataset.inputId = getNextInputId();
@@ -925,9 +987,12 @@ class BVTRecorder {
925
987
  // }
926
988
 
927
989
  lastInputId = el.dataset.inputId;
990
+
991
+ el.__locators = this.getLocatorsObject(el);
928
992
  }
929
993
  const role = window.getAriaRole(el);
930
- const label = window.getElementAccessibleName(el, false) || window.getElementAccessibleName(el, true) || role || "";
994
+ const label = window.getElementAccessibleName(el, false) || window.getElementAccessibleName(el, true) || "";
995
+ const result = this.getElementProperties(el);
931
996
  return {
932
997
  role,
933
998
  label,
@@ -944,13 +1009,16 @@ class BVTRecorder {
944
1009
  value: el.value,
945
1010
  tagName: el.tagName,
946
1011
  text: el.textContent,
947
- type: el.getAttribute("type"),
1012
+ type: el.type, // el.getAttribute("type"),
948
1013
  disabled: el.disabled,
949
1014
  readOnly: el.readOnly,
950
1015
  required: el.required,
951
1016
  checked: el.checked,
952
1017
  innerText: el.innerText,
953
1018
  },
1019
+ attributes: result.attributes,
1020
+ properties: result.properties,
1021
+ dataset: result.dataset,
954
1022
  };
955
1023
  }
956
1024
  handleEvent(e) {
@@ -1058,7 +1126,10 @@ class BVTRecorder {
1058
1126
  mode: "IGNORE_DIGIT",
1059
1127
  });
1060
1128
  allStrategyLocators["ignore_digit"].push(locator);
1061
- } else if (locator.css.includes("text=") || locator.css.includes("[name=") || locator.css.includes("label=")) {
1129
+ } else if (
1130
+ locator.css &&
1131
+ (locator.css.includes("text=") || locator.css.includes("[name=") || locator.css.includes("label="))
1132
+ ) {
1062
1133
  locs.push(locator);
1063
1134
  allStrategyLocators["basic"].push(locator);
1064
1135
  } else {
@@ -1092,6 +1163,7 @@ class BVTRecorder {
1092
1163
  improviseLocators: false,
1093
1164
  mustIncludeCSSChain: true,
1094
1165
  root: commonParent,
1166
+ noCSSId: true,
1095
1167
  });
1096
1168
  locators.forEach((locator) => {
1097
1169
  locator.text = text;
@@ -1118,7 +1190,7 @@ class BVTRecorder {
1118
1190
  cssLocators.push(origenCss);
1119
1191
  }
1120
1192
  const noClasses = CssSelectorGenerator.getCssSelector(el, {
1121
- blacklist: [/^(?!.*h\d).*?\d.*/, /\[style/, /\[data-input-id/, /\[blinq-container/],
1193
+ blacklist: [/^(?!.*h\d).*?\d.*/, /\[style/, /\[data-input-id/],
1122
1194
  combineWithinSelector: true,
1123
1195
  combineBetweenSelectors: true,
1124
1196
  selectors: ["id", "attribute", "tag", "nthchild", "nthoftype"],
@@ -1212,7 +1284,8 @@ class BVTRecorder {
1212
1284
  action: action.details,
1213
1285
  element: this.getElementDetails(actionElement, eventName),
1214
1286
  isPopupCloseClick: this.isPopupCloseEvent(e),
1215
- ...this.getLocatorsObject(actionElement),
1287
+ // ...this.getLocatorsObject(actionElement),
1288
+ ...(actionElement.__locators ?? this.getLocatorsObject(actionElement)),
1216
1289
  frame: this.getFrameDetails(),
1217
1290
  statistics: {
1218
1291
  time: `${performance.measure("command-received", "command-send").duration.toFixed(2)} ms`,
@@ -1352,7 +1425,8 @@ function countAccurances(css, scope = document) {
1352
1425
  }
1353
1426
  window.countAccurances = countAccurances;
1354
1427
  function findElement(css, scope = document) {
1355
- // return null if no css provided
1428
+ // Note: If you change anything iin this function
1429
+ // please change it in locators.js
1356
1430
  if (!css) return null;
1357
1431
  const selector = window.__injectedScript.parseSelector(css);
1358
1432
  try {
@@ -24,11 +24,13 @@ const getMatchingElements = (selector, options = {}) => {
24
24
  const root = options?.root || window.document;
25
25
  const prefix = options?.prefix;
26
26
  if (prefix) {
27
- selector = `${prefix} >> ${selector}`;
27
+ selector = `${prefix} >> ${selector} >> visible=true`;
28
28
  }
29
29
  return window.__injectedScript.querySelectorAll(window.__injectedScript.parseSelector(selector), root);
30
30
  };
31
31
 
32
+ window.getMatchingElements = getMatchingElements;
33
+
32
34
  const getPWSelectors = (element, options = {}) => {
33
35
  const selectors = [];
34
36
  const exludeText = options?.excludeText || false;
@@ -147,7 +149,25 @@ const getElementLocators = (element, options) => {
147
149
  .map((item) => {
148
150
  return item;
149
151
  });
150
-
152
+ if (result.nonUnique.length === 0) {
153
+ // find slector with min score
154
+ let minScore = Infinity;
155
+ let minSelector = null;
156
+ for (const locator of pw_locators) {
157
+ if (locator.score < minScore) {
158
+ minScore = locator.score;
159
+ minSelector = locator.selector;
160
+ }
161
+ }
162
+ if (minSelector) {
163
+ result.nonUnique.push({
164
+ css: minSelector,
165
+ priority: 2,
166
+ elements: getMatchingElements(minSelector, options),
167
+ score: minScore,
168
+ });
169
+ }
170
+ }
151
171
  return result;
152
172
  };
153
173
 
@@ -337,6 +357,7 @@ function generateUniqueCSSSelector(element, options) {
337
357
  const root = options?.root || window.document;
338
358
  const separator = options?.separator || " > ";
339
359
  const isUnique = options?.isunique || ((selector) => getMatchingElements(selector, options).length === 1);
360
+ const noCSSId = options?.noCSSId || false;
340
361
 
341
362
  if (!(element instanceof Element)) return null;
342
363
 
@@ -344,7 +365,7 @@ function generateUniqueCSSSelector(element, options) {
344
365
 
345
366
  let selector = "";
346
367
  const id = element.getAttribute("id");
347
- if (id && !/\d/.test(id)) {
368
+ if (id && !/\d/.test(id) && (!noCSSId)) {
348
369
  selector = "#" + cssEscape(id);
349
370
  if (isUnique(selector)) return selector;
350
371
  }
@@ -0,0 +1,235 @@
1
+ class AriaSnapshotUtils {
2
+ isLeafNode(node) {
3
+ if (node.kind === "text") {
4
+ return true;
5
+ } else {
6
+ return !node.children || node.children.length === 0;
7
+ }
8
+ }
9
+
10
+ deepClone(node) {
11
+ if (node.kind === "text") {
12
+ return node.text;
13
+ } else {
14
+ const result = {
15
+ kind: "role",
16
+ role: node.role,
17
+ };
18
+ if ("checked" in node) result.checked = node.checked;
19
+ if ("disabled" in node) result.disabled = node.disabled;
20
+ if ("expanded" in node) result.expanded = node.expanded;
21
+ if ("level" in node) result.level = node.level;
22
+ if ("pressed" in node) result.pressed = node.pressed;
23
+ if ("selected" in node) result.selected = node.selected;
24
+ if (node.name !== undefined) {
25
+ result.name = node.name;
26
+ }
27
+ if (node.props) {
28
+ result.props = Object.assign({}, node.props);
29
+ }
30
+ if (node.containerMode) {
31
+ result.containerMode = node.containerMode;
32
+ }
33
+ if (node.children) {
34
+ result.children = node.children.map(child => this.deepClone(child));
35
+ }
36
+ return result;
37
+ }
38
+ }
39
+
40
+ yamlEscapeKeyIfNeeded(str) {
41
+ if (!this.yamlStringNeedsQuotes(str)) return str;
42
+ return `'` + str.replace(/'/g, `''`) + `'`;
43
+ }
44
+
45
+ yamlEscapeValueIfNeeded(str) {
46
+ if (!this.yamlStringNeedsQuotes(str)) return str;
47
+ return (
48
+ '"' +
49
+ str.replace(/[\\"\x00-\x1f\x7f-\x9f]/g, (c) => {
50
+ switch (c) {
51
+ case "\\":
52
+ return "\\\\";
53
+ case '"':
54
+ return '\\"';
55
+ case "\b":
56
+ return "\\b";
57
+ case "\f":
58
+ return "\\f";
59
+ case "\n":
60
+ return "\\n";
61
+ case "\r":
62
+ return "\\r";
63
+ case "\t":
64
+ return "\\t";
65
+ default:
66
+ const code = c.charCodeAt(0);
67
+ return "\\x" + code.toString(16).padStart(2, "0");
68
+ }
69
+ }) +
70
+ '"'
71
+ );
72
+ }
73
+
74
+ yamlStringNeedsQuotes(str) {
75
+ if (!str) return false;
76
+ if (str.length === 0) return true;
77
+ if (/^\s|\s$/.test(str)) return true;
78
+ if (/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/.test(str)) return true;
79
+ if (/^-/.test(str)) return true;
80
+ if (/[\n:](\s|$)/.test(str)) return true;
81
+ if (/\s#/.test(str)) return true;
82
+ if (/[\n\r]/.test(str)) return true;
83
+ if (/^[&*\],?!>|@"'#%]/.test(str)) return true;
84
+ if (/[{}`]/.test(str)) return true;
85
+ if (/^\[/.test(str)) return true;
86
+ if (!isNaN(Number(str)) || ["y", "n", "yes", "no", "true", "false", "on", "off", "null"].includes(str.toLowerCase()))
87
+ return true;
88
+ return false;
89
+ }
90
+
91
+ filterParentToChildren(parent, targetChildren) {
92
+ const isDirectMatch = targetChildren.some((child) => JSON.stringify(child) === JSON.stringify(parent));
93
+ if (isDirectMatch || this.isLeafNode(parent)) {
94
+ return isDirectMatch ? this.deepClone(parent) : null;
95
+ }
96
+ if (parent.kind === "role" && parent.children && parent.children.length > 0) {
97
+ const filteredChildren = parent.children
98
+ .map((child) => this.filterParentToChildren(child, targetChildren))
99
+ .filter((child) => child !== null);
100
+ if (filteredChildren.length > 0) {
101
+ const result = this.deepClone(parent);
102
+ result.children = filteredChildren;
103
+ return result;
104
+ }
105
+ }
106
+ return null;
107
+ }
108
+
109
+ serializeAriaSnapshot(ariaSnapshot, indent = 0) {
110
+ const lines = [];
111
+ const visit = (node, parentNode, indent) => {
112
+ if (typeof node === "string") {
113
+ const text = this.yamlEscapeValueIfNeeded(node);
114
+ if (text) lines.push(indent + "- text: " + text);
115
+ return;
116
+ }
117
+ let key = node.role;
118
+
119
+ if (node.name && node.name.length <= 900) {
120
+ const name = node.name;
121
+ if (name) {
122
+ const stringifiedName = name.startsWith("/") && name.endsWith("/") ? name : JSON.stringify(name);
123
+ key += " " + stringifiedName;
124
+ }
125
+ }
126
+ if (node.checked === "mixed") key += ` [checked=mixed]`;
127
+ if (node.checked === true) key += ` [checked]`;
128
+ if (node.disabled) key += ` [disabled]`;
129
+ if (node.expanded) key += ` [expanded]`;
130
+ if (node.level) key += ` [level=${node.level}]`;
131
+ if (node.pressed === "mixed") key += ` [pressed=mixed]`;
132
+ if (node.pressed === true) key += ` [pressed]`;
133
+ if (node.selected === true) key += ` [selected]`;
134
+
135
+ const escapedKey = indent + "- " + this.yamlEscapeKeyIfNeeded(key);
136
+ if (node.props === undefined) node.props = {};
137
+ if (node.children === undefined) node.children = [];
138
+ const hasProps = !!Object.keys(node.props).length;
139
+ if (!node.children.length && !hasProps) {
140
+ lines.push(escapedKey);
141
+ } else if (node.children.length === 1 && typeof node.children[0] === "string" && !hasProps) {
142
+ const text = node.children[0];
143
+ if (text) lines.push(escapedKey + ": " + this.yamlEscapeValueIfNeeded(text));
144
+ else lines.push(escapedKey);
145
+ } else {
146
+ lines.push(escapedKey + ":");
147
+ for (const [name, value] of Object.entries(node.props) || []) {
148
+ lines.push(indent + " - /" + name + ": " + this.yamlEscapeValueIfNeeded(value));
149
+ }
150
+ for (const child of node.children || []) {
151
+ visit(child, node, indent + " ");
152
+ }
153
+ }
154
+ };
155
+
156
+ const node = ariaSnapshot;
157
+ if (node.role === "fragment") {
158
+ for (const child of node.children || []) visit(child, node, "");
159
+ } else {
160
+ visit(node, null, "");
161
+ }
162
+ return lines.join("\n");
163
+ }
164
+
165
+ isSnapshotAvailable(element, elementSet) {
166
+ if (elementSet.has(element)) {
167
+ return element;
168
+ } else {
169
+ return undefined;
170
+ }
171
+ }
172
+
173
+ findMatchingElements(inputSnapshot, objectSet) {
174
+ const matches = new Map();
175
+ const lines = inputSnapshot.trim().split("\n");
176
+ const subtrees = this.extractSubtrees(lines);
177
+
178
+ subtrees.forEach((subtree) => {
179
+ const subtreeText = subtree.map((t) => t.trim()).join("\n");
180
+ for (const obj of objectSet) {
181
+ const normalizedSubtree = subtreeText.trim();
182
+ const normalizedSnapshot = obj.snapshot
183
+ .split("\n")
184
+ .map((s) => s.trim())
185
+ .join("\n");
186
+ if (normalizedSnapshot === normalizedSubtree) {
187
+ matches.set(subtreeText, obj.element);
188
+ break;
189
+ }
190
+ }
191
+ });
192
+
193
+ return matches;
194
+ }
195
+
196
+ extractSubtrees(lines) {
197
+ const subtrees = [];
198
+ const indentSize = this.getIndentSize(lines);
199
+
200
+ for (let i = 0; i < lines.length; i++) {
201
+ const currentLine = lines[i];
202
+ const currentIndent = this.getIndentLevel(currentLine);
203
+ const subtree = [currentLine];
204
+ for (let j = i + 1; j < lines.length; j++) {
205
+ const nextLine = lines[j];
206
+ const nextIndent = this.getIndentLevel(nextLine);
207
+ if (nextIndent > currentIndent) {
208
+ subtree.push(nextLine);
209
+ } else {
210
+ break;
211
+ }
212
+ }
213
+ subtrees.push(subtree);
214
+ }
215
+ subtrees.sort((a, b) => a.length - b.length);
216
+ return subtrees;
217
+ }
218
+
219
+ getIndentLevel(line) {
220
+ const match = line.match(/^(\s*)/);
221
+ return match ? match[1].length : 0;
222
+ }
223
+
224
+ getIndentSize(lines) {
225
+ for (let i = 1; i < lines.length; i++) {
226
+ const indentLevel = this.getIndentLevel(lines[i]);
227
+ if (indentLevel > 0) {
228
+ return indentLevel - this.getIndentLevel(lines[i - 1]);
229
+ }
230
+ }
231
+ return 2;
232
+ }
233
+ }
234
+
235
+ export default AriaSnapshotUtils