@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.
- package/dist/cli/index.js +1 -1
- package/dist/core/live-scanner/element-finder.d.ts +2 -2
- package/dist/core/live-scanner/element-finder.d.ts.map +1 -1
- package/dist/core/live-scanner/element-finder.js +253 -56
- package/dist/core/live-scanner/element-finder.js.map +1 -1
- package/dist/core/live-scanner/matrix-reader.d.ts.map +1 -1
- package/dist/core/live-scanner/matrix-reader.js +1 -0
- package/dist/core/live-scanner/matrix-reader.js.map +1 -1
- package/dist/core/live-scanner/matrix-writer.d.ts.map +1 -1
- package/dist/core/live-scanner/matrix-writer.js +15 -4
- package/dist/core/live-scanner/matrix-writer.js.map +1 -1
- package/dist/core/live-scanner/step-replayer.d.ts.map +1 -1
- package/dist/core/live-scanner/step-replayer.js +3 -2
- package/dist/core/live-scanner/step-replayer.js.map +1 -1
- package/dist/core/live-scanner/types.d.ts +3 -2
- package/dist/core/live-scanner/types.d.ts.map +1 -1
- package/dist/generators/scaffold-generator/index.d.ts +1 -0
- package/dist/generators/scaffold-generator/index.d.ts.map +1 -1
- package/dist/generators/scaffold-generator/index.js +19 -0
- package/dist/generators/scaffold-generator/index.js.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/fill-editor-action.hbs +3 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/locator.hbs +1 -1
- package/dist/generators/test-generator/patterns/form-patterns.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/form-patterns.js +8 -2
- package/dist/generators/test-generator/patterns/form-patterns.js.map +1 -1
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +8 -5
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts +1 -0
- package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.js +21 -17
- package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/index.ts +1 -1
- package/src/core/live-scanner/element-finder.ts +263 -55
- package/src/core/live-scanner/matrix-reader.ts +1 -0
- package/src/core/live-scanner/matrix-writer.ts +15 -4
- package/src/core/live-scanner/step-replayer.ts +3 -2
- package/src/core/live-scanner/types.ts +3 -2
- package/src/generators/scaffold-generator/index.ts +20 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/fill-editor-action.hbs +3 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/locator.hbs +1 -1
- package/src/generators/test-generator/patterns/form-patterns.ts +9 -2
- package/src/generators/test-generator/step-mapper.ts +8 -5
- 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
|
-
|
|
40
|
-
|
|
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++;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
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
|
|
61
|
+
templateName,
|
|
57
62
|
data: { ...resolved, selectorRef: step.selectorRef, fillValue: value },
|
|
58
|
-
comment:
|
|
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}}
|
|
108
|
-
|
|
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
|
-
|
|
112
|
+
const resolved = this.selectorResolver.resolveSelector(step.selectorRef, this.featureName, 'dialog', 0);
|
|
113
|
+
roleName = resolved.name || step.selectorRef;
|
|
111
114
|
} catch {
|
|
112
|
-
|
|
115
|
+
roleName = step.selectorRef;
|
|
113
116
|
}
|
|
114
|
-
contextVars.
|
|
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
|
|