@sun-asterisk/sungen 1.0.21 → 1.0.23

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 (45) hide show
  1. package/dist/cli/index.js +1 -1
  2. package/dist/core/live-scanner/element-finder.d.ts +2 -2
  3. package/dist/core/live-scanner/element-finder.d.ts.map +1 -1
  4. package/dist/core/live-scanner/element-finder.js +253 -56
  5. package/dist/core/live-scanner/element-finder.js.map +1 -1
  6. package/dist/core/live-scanner/matrix-reader.d.ts.map +1 -1
  7. package/dist/core/live-scanner/matrix-reader.js +1 -0
  8. package/dist/core/live-scanner/matrix-reader.js.map +1 -1
  9. package/dist/core/live-scanner/matrix-writer.d.ts.map +1 -1
  10. package/dist/core/live-scanner/matrix-writer.js +15 -4
  11. package/dist/core/live-scanner/matrix-writer.js.map +1 -1
  12. package/dist/core/live-scanner/step-replayer.d.ts.map +1 -1
  13. package/dist/core/live-scanner/step-replayer.js +3 -2
  14. package/dist/core/live-scanner/step-replayer.js.map +1 -1
  15. package/dist/core/live-scanner/types.d.ts +3 -2
  16. package/dist/core/live-scanner/types.d.ts.map +1 -1
  17. package/dist/generators/scaffold-generator/index.d.ts +1 -0
  18. package/dist/generators/scaffold-generator/index.d.ts.map +1 -1
  19. package/dist/generators/scaffold-generator/index.js +19 -0
  20. package/dist/generators/scaffold-generator/index.js.map +1 -1
  21. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/fill-editor-action.hbs +3 -0
  22. package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/locator.hbs +1 -1
  23. package/dist/generators/test-generator/patterns/form-patterns.d.ts.map +1 -1
  24. package/dist/generators/test-generator/patterns/form-patterns.js +8 -2
  25. package/dist/generators/test-generator/patterns/form-patterns.js.map +1 -1
  26. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  27. package/dist/generators/test-generator/step-mapper.js +8 -5
  28. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  29. package/dist/generators/test-generator/utils/selector-resolver.d.ts +1 -0
  30. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  31. package/dist/generators/test-generator/utils/selector-resolver.js +21 -17
  32. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/cli/index.ts +1 -1
  35. package/src/core/live-scanner/element-finder.ts +263 -55
  36. package/src/core/live-scanner/matrix-reader.ts +1 -0
  37. package/src/core/live-scanner/matrix-writer.ts +15 -4
  38. package/src/core/live-scanner/step-replayer.ts +3 -2
  39. package/src/core/live-scanner/types.ts +3 -2
  40. package/src/generators/scaffold-generator/index.ts +20 -0
  41. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/fill-editor-action.hbs +3 -0
  42. package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/locator.hbs +1 -1
  43. package/src/generators/test-generator/patterns/form-patterns.ts +9 -2
  44. package/src/generators/test-generator/step-mapper.ts +8 -5
  45. package/src/generators/test-generator/utils/selector-resolver.ts +25 -17
@@ -7,6 +7,16 @@ import * as fs from 'fs';
7
7
  import * as yaml from 'yaml';
8
8
  import { LiveScanResult } from './types';
9
9
 
10
+ /**
11
+ * Sanitize a string value before YAML serialization:
12
+ * collapse newlines/tabs/carriage-returns into spaces to prevent
13
+ * broken escape sequences (e.g. \\n) in QUOTE_DOUBLE output.
14
+ */
15
+ function sanitizeForYaml(value: string | null): string | null {
16
+ if (!value) return value;
17
+ return value.replace(/[\n\r\t]/g, ' ').replace(/\s+/g, ' ').trim() || null;
18
+ }
19
+
10
20
  export function writeMatrix(result: LiveScanResult, outputPath: string): void {
11
21
  const output: any = {
12
22
  screen: result.screen,
@@ -31,13 +41,14 @@ export function writeMatrix(result: LiveScanResult, outputPath: string): void {
31
41
  scenarioData.elements[elementKey] = {
32
42
  gherkin_type: element.gherkinType,
33
43
  selector_type: element.selectorType,
34
- selector_value: element.selectorValue,
44
+ selector_value: sanitizeForYaml(element.selectorValue),
35
45
  role: element.role,
36
- name: element.name,
46
+ name: sanitizeForYaml(element.name),
37
47
  testid: element.testid,
38
48
  tag: element.tag,
39
- placeholder: element.placeholder,
40
- label: element.label,
49
+ contenteditable: element.contenteditable,
50
+ placeholder: sanitizeForYaml(element.placeholder),
51
+ label: sanitizeForYaml(element.label),
41
52
  context: element.context,
42
53
  match_method: element.matchMethod,
43
54
  exact: element.exact,
@@ -80,6 +80,7 @@ export async function replaySteps(
80
80
  name: step.selectorRef,
81
81
  testid: null,
82
82
  tag: '',
83
+ contenteditable: false,
83
84
  placeholder: null,
84
85
  label: null,
85
86
  context: 'page',
@@ -125,8 +126,8 @@ export async function replaySteps(
125
126
  continue;
126
127
  }
127
128
 
128
- // Find the element on the page
129
- const element = await findElement(page, step.selectorRef, elementType, nth);
129
+ // Find the element on the page, scoped to current context (dialog/page)
130
+ const element = await findElement(page, step.selectorRef, elementType, nth, currentContext);
130
131
  element.context = currentContext;
131
132
 
132
133
  // Record in matrix (first match wins for duplicate keys)
@@ -10,9 +10,9 @@
10
10
  * text → page.getByText(value)
11
11
  */
12
12
 
13
- export type MatchMethod = 'testid' | 'exact_role' | 'role_fallback' | 'label' | 'placeholder' | 'text_only' | 'unresolved';
13
+ export type MatchMethod = 'testid' | 'exact_role' | 'role_fallback' | 'label' | 'placeholder' | 'text_only' | 'contenteditable' | 'unresolved';
14
14
 
15
- export type SelectorType = 'testid' | 'role' | 'label' | 'placeholder' | 'text';
15
+ export type SelectorType = 'testid' | 'role' | 'label' | 'placeholder' | 'text' | 'locator';
16
16
 
17
17
  export type ElementContext = 'page' | 'dialog' | 'menu' | 'modal' | 'popup';
18
18
 
@@ -25,6 +25,7 @@ export interface LiveElement {
25
25
  name: string;
26
26
  testid: string | null;
27
27
  tag: string;
28
+ contenteditable: boolean;
28
29
  placeholder: string | null;
29
30
  label: string | null;
30
31
  context: ElementContext;
@@ -17,6 +17,7 @@ export interface ScaffoldElement {
17
17
  name?: string; // For role type (accessible name), or label text
18
18
  nth: number;
19
19
  exact?: boolean; // Whether to use exact matching (default: false)
20
+ inputMethod?: string; // 'pressSequentially' for contenteditable/rich-text elements
20
21
  }
21
22
 
22
23
  export interface ScaffoldResult {
@@ -165,6 +166,9 @@ export class ScaffoldGenerator {
165
166
  if (/^textarea\b/.test(lowerText) || /^\d*\s*textarea\b/.test(lowerText)) {
166
167
  return 'textarea';
167
168
  }
169
+ if (/^editor\b/.test(lowerText) || /^\d*\s*editor\b/.test(lowerText)) {
170
+ return 'editor';
171
+ }
168
172
  if (/^text\b/.test(lowerText) || /^\d*\s*text\b/.test(lowerText)) {
169
173
  return 'text';
170
174
  }
@@ -730,10 +734,26 @@ export class ScaffoldGenerator {
730
734
  value: liveElement.name || liveElement.gherkinRef,
731
735
  ...exactField,
732
736
  };
737
+ } else if (selectorType === 'locator' && liveElement.selectorValue) {
738
+ // page.locator('[contenteditable="true"]') — CSS locator
739
+ // Reset nth to 0: the original nth was for text matches, not this locator type
740
+ enriched[key] = {
741
+ ...scaffoldElement,
742
+ type: 'locator' as any,
743
+ value: liveElement.selectorValue,
744
+ nth: 0,
745
+ };
733
746
  } else {
734
747
  continue;
735
748
  }
736
749
 
750
+ // Detect contenteditable div-based editors (rich text editors like TipTap, Quill, ProseMirror)
751
+ // When the DOM element is contenteditable but NOT a native input/textarea,
752
+ // mark it so the generator uses click + pressSequentially instead of .fill()
753
+ if (liveElement.contenteditable && liveElement.tag !== 'textarea' && liveElement.tag !== 'input') {
754
+ enriched[key] = { ...enriched[key], inputMethod: 'pressSequentially' };
755
+ }
756
+
737
757
  enrichedCount++;
738
758
  if (liveElement.warning) {
739
759
  warningCount++;
@@ -0,0 +1,3 @@
1
+ const editorLocator = {{> locator}};
2
+ await editorLocator.click();
3
+ await editorLocator.pressSequentially('{{fillValue}}');
@@ -1 +1 @@
1
- page.locator('{{escapeQuotes value}}')
1
+ {{pageRoot}}.locator('{{escapeQuotes value}}')
@@ -52,10 +52,17 @@ export const formPatterns: StepPattern[] = [
52
52
  value = step.value!;
53
53
  }
54
54
 
55
+ // Use pressSequentially template for contenteditable/rich-text editors (auto-detected by live-scan)
56
+ // or when Gherkin explicitly uses 'editor' element type
57
+ const isEditor = resolved.inputMethod === 'pressSequentially' || step.elementType === 'editor';
58
+ const templateName = isEditor ? 'fill-editor-action' : 'fill-action';
59
+
55
60
  return {
56
- templateName: 'fill-action',
61
+ templateName,
57
62
  data: { ...resolved, selectorRef: step.selectorRef, fillValue: value },
58
- comment: `Fill ${step.selectorRef} with ${step.dataRef || step.value}`,
63
+ comment: isEditor
64
+ ? `Fill rich text editor ${step.selectorRef} with ${step.dataRef || step.value}`
65
+ : `Fill ${step.selectorRef} with ${step.dataRef || step.value}`,
59
66
  };
60
67
  },
61
68
  priority: 10,
@@ -104,14 +104,17 @@ export class StepMapper {
104
104
  const contextVars: Record<string, any> = { inDialog: true };
105
105
 
106
106
  if (step.selectorRef && step.dataRef) {
107
- // Case 3: [panel] dialog with {{value}} → filter by resolved data value
108
- let filterText = '';
107
+ // Case 3: [panel] dialog with {{value}}
108
+ // Use dialog role name from selector (more reliable than filter by data text,
109
+ // since the data value may not be present in the dialog when it first opens)
110
+ let roleName = step.selectorRef;
109
111
  try {
110
- filterText = this.dataResolver.resolveData(step.dataRef, this.featureName);
112
+ const resolved = this.selectorResolver.resolveSelector(step.selectorRef, this.featureName, 'dialog', 0);
113
+ roleName = resolved.name || step.selectorRef;
111
114
  } catch {
112
- filterText = '';
115
+ roleName = step.selectorRef;
113
116
  }
114
- contextVars.dialogFilterText = filterText;
117
+ contextVars.dialogRoleName = roleName;
115
118
  } else if (step.selectorRef) {
116
119
  // Case 2: [Title] dialog → named dialog role
117
120
  let roleName = step.selectorRef;
@@ -198,6 +198,7 @@ interface SelectorEntry {
198
198
  name?: string; // Accessible name for role-based selectors (e.g., 'Login', 'Submit')
199
199
  nth?: number; // Element index (0 = no index, 1+ = append .nth())
200
200
  exact?: boolean; // Whether to use exact matching (default: false)
201
+ inputMethod?: string; // 'pressSequentially' for contenteditable/rich-text elements
201
202
  }
202
203
 
203
204
  // New selector file structure: flat key-value pairs
@@ -224,6 +225,7 @@ export interface ResolvedSelector {
224
225
  locator?: string; // CSS locator for fallback
225
226
  nth?: number; // Index for .nth() if multiple matches exist
226
227
  exact?: boolean; // Whether to use exact matching (default: false)
228
+ inputMethod?: string; // 'pressSequentially' for contenteditable/rich-text elements
227
229
  }
228
230
 
229
231
  /**
@@ -288,7 +290,7 @@ export class SelectorResolver {
288
290
  * NOTE: textbox/textarea must precede "text" to prevent partial matching.
289
291
  */
290
292
  static extractNthFromStep(afterElement: string): number {
291
- const nthRegex = /^\s*(?:field|button|textbox|textarea|text|link|input|element|item|logo|image|img|icon|raw|table|column|columnheader|list|listitem|row|checkbox|radio|dropdown|select|uploader|heading|header)?\s*(\d+)\b/i;
293
+ const nthRegex = /^\s*(?:field|button|textbox|textarea|editor|text|link|input|element|item|logo|image|img|icon|raw|table|column|columnheader|list|listitem|row|checkbox|radio|dropdown|select|uploader|heading|header|label|caption|title|message)?\s*(\d+)\b/i;
292
294
  const match = nthRegex.exec(afterElement);
293
295
  return match ? parseInt(match[1], 10) : 0;
294
296
  }
@@ -391,6 +393,7 @@ export class SelectorResolver {
391
393
  'input': 'field',
392
394
  'textbox': 'field',
393
395
  'textarea': 'field',
396
+ 'editor': 'field',
394
397
  };
395
398
  return aliasMap[elementType] || elementType;
396
399
  }
@@ -518,79 +521,84 @@ export class SelectorResolver {
518
521
  const name = entry.name !== undefined && entry.name !== null ? entry.name : originalLabel;
519
522
  const nth = entry.nth || 0;
520
523
  const exact = entry.exact === true;
524
+ const inputMethod = entry.inputMethod;
525
+
526
+ // Helper to attach inputMethod only when present
527
+ const withInputMethod = (resolved: ResolvedSelector): ResolvedSelector =>
528
+ inputMethod ? { ...resolved, inputMethod } : resolved;
521
529
 
522
530
  // If type is 'locator', use locator field (or value as fallback) as the CSS locator directly
523
531
  if (type === 'locator') {
524
532
  const locatorValue = locator && locator.trim() ? locator : value;
525
- return {
533
+ return withInputMethod({
526
534
  strategy: 'locator',
527
535
  value: locatorValue,
528
536
  locator: locatorValue,
529
537
  nth,
530
- };
538
+ });
531
539
  }
532
540
 
533
541
  // If custom locator is provided and type is not 'locator', use it as CSS selector
534
542
  if (locator && locator.trim()) {
535
- return {
543
+ return withInputMethod({
536
544
  strategy: 'css',
537
545
  value: locator,
538
546
  locator: locator,
539
547
  nth,
540
- };
548
+ });
541
549
  }
542
550
 
543
551
  // Use type-based strategy
544
552
  switch (type) {
545
553
  case 'placeholder':
546
- return {
554
+ return withInputMethod({
547
555
  strategy: 'placeholder',
548
556
  value,
549
557
  nth,
550
558
  exact,
551
- };
559
+ });
552
560
 
553
561
  case 'role':
554
- return {
562
+ return withInputMethod({
555
563
  strategy: 'role',
556
564
  role: value,
557
565
  name: name, // Use the name field for accessible name
558
566
  value,
559
567
  nth,
560
568
  exact,
561
- };
569
+ });
562
570
 
563
571
  case 'testid':
564
- return {
572
+ return withInputMethod({
565
573
  strategy: 'testid',
566
574
  value,
567
575
  nth,
568
- };
576
+ });
569
577
 
570
578
  case 'label':
571
- return {
579
+ return withInputMethod({
572
580
  strategy: 'label',
573
581
  value,
574
582
  nth,
575
583
  exact,
576
- };
584
+ });
577
585
 
578
586
  case 'text':
579
- return {
587
+ return withInputMethod({
580
588
  strategy: 'text',
581
589
  value,
582
590
  nth,
583
591
  exact,
584
- };
592
+ });
585
593
 
586
594
  default:
587
595
  // Fallback to placeholder
588
- return {
596
+ return withInputMethod({
589
597
  strategy: 'placeholder',
590
598
  value,
591
599
  nth,
592
600
  exact,
593
- };
601
+ });
594
602
  }
595
603
  }
596
604