@sun-asterisk/sungen 2.3.1 → 2.4.0
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/README.md +10 -11
- package/dist/cli/index.js +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/table-action-in-row.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-column-exists.hbs +5 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-exists.hbs +7 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-not-exists.hbs +5 -1
- package/dist/generators/test-generator/patterns/table-patterns.d.ts +9 -0
- package/dist/generators/test-generator/patterns/table-patterns.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/table-patterns.js +94 -98
- package/dist/generators/test-generator/patterns/table-patterns.js.map +1 -1
- package/dist/generators/test-generator/patterns/types.d.ts +2 -0
- package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.d.ts +13 -0
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +80 -0
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/orchestrator/ai-rules-updater.d.ts.map +1 -1
- package/dist/orchestrator/ai-rules-updater.js +8 -6
- package/dist/orchestrator/ai-rules-updater.js.map +1 -1
- package/dist/orchestrator/project-initializer.d.ts.map +1 -1
- package/dist/orchestrator/project-initializer.js +33 -2
- package/dist/orchestrator/project-initializer.js.map +1 -1
- package/dist/orchestrator/screen-manager.js +1 -1
- package/dist/orchestrator/screen-manager.js.map +1 -1
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +5 -5
- package/{src/orchestrator/templates/ai-instructions/claude-cmd-make-tc.md → dist/orchestrator/templates/ai-instructions/claude-cmd-create-test.md} +7 -7
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-review.md +21 -0
- package/{src/orchestrator/templates/ai-instructions/claude-cmd-make-test.md → dist/orchestrator/templates/ai-instructions/claude-cmd-run-test.md} +2 -2
- package/dist/orchestrator/templates/ai-instructions/claude-config.md +65 -12
- package/dist/orchestrator/templates/ai-instructions/claude-skill-error-mapping.md +128 -52
- package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +9 -40
- package/dist/orchestrator/templates/ai-instructions/claude-skill-selector-fix.md +67 -239
- package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +40 -208
- package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-review.md +104 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-viewpoint.md +186 -0
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +4 -4
- package/dist/orchestrator/templates/ai-instructions/{copilot-cmd-make-tc.md → copilot-cmd-create-test.md} +8 -8
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-review.md +24 -0
- package/{src/orchestrator/templates/ai-instructions/copilot-cmd-make-test.md → dist/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md} +3 -3
- package/dist/orchestrator/templates/ai-instructions/copilot-config.md +66 -13
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-error-mapping.md +128 -52
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +9 -40
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md +67 -214
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +40 -208
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-review.md +104 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-viewpoint.md +186 -0
- package/dist/orchestrator/templates/readme.md +4 -4
- package/package.json +1 -1
- package/src/cli/index.ts +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/table-action-in-row.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-column-exists.hbs +5 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-exists.hbs +7 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-not-exists.hbs +5 -1
- package/src/generators/test-generator/patterns/table-patterns.ts +98 -111
- package/src/generators/test-generator/patterns/types.ts +2 -0
- package/src/generators/test-generator/step-mapper.ts +87 -1
- package/src/orchestrator/ai-rules-updater.ts +8 -6
- package/src/orchestrator/project-initializer.ts +38 -2
- package/src/orchestrator/screen-manager.ts +1 -1
- package/src/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +5 -5
- package/{dist/orchestrator/templates/ai-instructions/claude-cmd-make-tc.md → src/orchestrator/templates/ai-instructions/claude-cmd-create-test.md} +7 -7
- package/src/orchestrator/templates/ai-instructions/claude-cmd-review.md +21 -0
- package/{dist/orchestrator/templates/ai-instructions/claude-cmd-make-test.md → src/orchestrator/templates/ai-instructions/claude-cmd-run-test.md} +2 -2
- package/src/orchestrator/templates/ai-instructions/claude-config.md +65 -12
- package/src/orchestrator/templates/ai-instructions/claude-skill-error-mapping.md +128 -52
- package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +9 -40
- package/src/orchestrator/templates/ai-instructions/claude-skill-selector-fix.md +67 -239
- package/src/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +40 -208
- package/src/orchestrator/templates/ai-instructions/claude-skill-tc-review.md +104 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-viewpoint.md +186 -0
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +4 -4
- package/src/orchestrator/templates/ai-instructions/{copilot-cmd-make-tc.md → copilot-cmd-create-test.md} +8 -8
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-review.md +24 -0
- package/{dist/orchestrator/templates/ai-instructions/copilot-cmd-make-test.md → src/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md} +3 -3
- package/src/orchestrator/templates/ai-instructions/copilot-config.md +66 -13
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-error-mapping.md +128 -52
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +9 -40
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md +67 -214
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +40 -208
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-review.md +104 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-viewpoint.md +186 -0
- package/src/orchestrator/templates/readme.md +4 -4
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-cell-by-filter.hbs +0 -2
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-cell-by-index.hbs +0 -2
- package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-review.md +0 -228
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-review.md +0 -228
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-cell-by-filter.hbs +0 -2
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-cell-by-index.hbs +0 -2
- package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-review.md +0 -228
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-review.md +0 -228
|
@@ -1 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
{{~#if isGiven}}
|
|
2
|
+
await {{> locator}}.getByRole('columnheader', { name: '{{escapeQuotes columnName}}' }).waitFor();
|
|
3
|
+
{{~else}}
|
|
4
|
+
await expect({{> locator}}.getByRole('columnheader', { name: '{{escapeQuotes columnName}}' })).toBeVisible();
|
|
5
|
+
{{~/if}}
|
|
@@ -1 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
{{~#if isGiven}}
|
|
2
|
+
const tableRow = {{> locator}}.getByRole('row').filter({ hasText: '{{escapeQuotes filterValue}}' });
|
|
3
|
+
await tableRow.waitFor();
|
|
4
|
+
{{~else}}
|
|
5
|
+
const tableRow = {{> locator}}.getByRole('row').filter({ hasText: '{{escapeQuotes filterValue}}' });
|
|
6
|
+
await expect(tableRow).toBeVisible();
|
|
7
|
+
{{~/if}}
|
|
@@ -1 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
{{~#if isGiven}}
|
|
2
|
+
await {{> locator}}.getByRole('row').filter({ hasText: '{{escapeQuotes filterValue}}' }).waitFor({ state: 'hidden' });
|
|
3
|
+
{{~else}}
|
|
4
|
+
await expect({{> locator}}.getByRole('row').filter({ hasText: '{{escapeQuotes filterValue}}' })).toHaveCount(0);
|
|
5
|
+
{{~/if}}
|
|
@@ -1,19 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Table Patterns
|
|
3
3
|
* Handles: table row assertions, table cell lookups, actions in table rows
|
|
4
|
+
*
|
|
5
|
+
* Syntax (v2.3 final):
|
|
6
|
+
* - User see [Col] column in [Table] table # column exists
|
|
7
|
+
* - User see [Ref] row in [Table] table with {{value}} # row exists (enters row scope)
|
|
8
|
+
* - User see [Ref] row in [Table] table with {{value}} is hidden # row hidden
|
|
9
|
+
* - User see [Table] table with {{count}} # row count
|
|
10
|
+
* - User see [Table] table is empty # empty table
|
|
11
|
+
* - User see [Col] column with {{value}} # cell value (row scoped — handled in step-mapper)
|
|
12
|
+
* - User click [Act] button in [Table] table with {{filter}} # action in row
|
|
4
13
|
*/
|
|
5
14
|
|
|
6
|
-
import { StepPattern } from './types';
|
|
15
|
+
import { StepPattern, PatternContext } from './types';
|
|
16
|
+
|
|
17
|
+
/** Helper: check if current step is in a Given context */
|
|
18
|
+
function isGivenContext(context: PatternContext): boolean {
|
|
19
|
+
return context.effectiveKeyword === 'Given';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Map Gherkin element type to Playwright ARIA role */
|
|
23
|
+
const typeToRole: Record<string, string> = {
|
|
24
|
+
button: 'button',
|
|
25
|
+
link: 'link',
|
|
26
|
+
checkbox: 'checkbox',
|
|
27
|
+
icon: 'img',
|
|
28
|
+
image: 'img',
|
|
29
|
+
radio: 'radio',
|
|
30
|
+
switch: 'switch',
|
|
31
|
+
};
|
|
7
32
|
|
|
8
33
|
export const tablePatterns: StepPattern[] = [
|
|
9
|
-
// "User see [
|
|
10
|
-
// Also accepts: "User see [Users] table has {{count}} rows" (backward compat)
|
|
34
|
+
// "User see [Table] table with {{count}}" — row count (no "rows" keyword needed)
|
|
11
35
|
{
|
|
12
36
|
name: 'table-row-count',
|
|
13
37
|
matcher: (step) => {
|
|
14
|
-
return
|
|
15
|
-
/\
|
|
16
|
-
!!step.dataRef
|
|
38
|
+
return step.elementType === 'table' &&
|
|
39
|
+
/\btable\s+with\b/i.test(step.text) &&
|
|
40
|
+
!!step.dataRef &&
|
|
41
|
+
!/\brow\b/i.test(step.text) &&
|
|
42
|
+
!/\bcolumn\b/i.test(step.text) &&
|
|
43
|
+
!/\bempty\b/i.test(step.text);
|
|
17
44
|
},
|
|
18
45
|
resolver: (step, context) => {
|
|
19
46
|
const resolved = context.selectorResolver.resolveSelector(
|
|
@@ -21,44 +48,47 @@ export const tablePatterns: StepPattern[] = [
|
|
|
21
48
|
);
|
|
22
49
|
const count = context.dataResolver.resolveData(step.dataRef!, context.featureName);
|
|
23
50
|
|
|
51
|
+
const isGiven = isGivenContext(context);
|
|
24
52
|
return {
|
|
25
53
|
templateName: 'table-row-count',
|
|
26
|
-
data: { ...resolved, expectedCount: count },
|
|
27
|
-
comment:
|
|
54
|
+
data: { ...resolved, expectedCount: count, isGiven },
|
|
55
|
+
comment: `${isGiven ? 'Wait' : 'Assert'} ${step.selectorRef} table has ${count} rows`,
|
|
28
56
|
};
|
|
29
57
|
},
|
|
30
58
|
priority: 16,
|
|
31
59
|
},
|
|
32
60
|
|
|
33
|
-
// "User see [
|
|
34
|
-
// Preferred: "User see [Email] column in [Users] table" (parent scoping)
|
|
61
|
+
// "User see [Col] column in [Table] table"
|
|
35
62
|
{
|
|
36
63
|
name: 'table-column-exists',
|
|
37
64
|
matcher: (step) => {
|
|
38
|
-
return /\
|
|
39
|
-
/\
|
|
65
|
+
return /\bcolumn\s+in\b/i.test(step.text) &&
|
|
66
|
+
/\btable\b/i.test(step.text) &&
|
|
67
|
+
!step.dataRef;
|
|
40
68
|
},
|
|
41
69
|
resolver: (step, context) => {
|
|
42
|
-
//
|
|
70
|
+
// brackets[0] = Col, brackets[1] = Table
|
|
43
71
|
const brackets = step.text.match(/\[([^\]]+)\]/g) || [];
|
|
44
|
-
const columnName = brackets
|
|
72
|
+
const columnName = brackets[0]?.replace(/[\[\]]/g, '') || 'Unknown';
|
|
73
|
+
const tableName = brackets.length >= 2
|
|
45
74
|
? brackets[1].replace(/[\[\]]/g, '')
|
|
46
|
-
: '
|
|
75
|
+
: brackets[0]?.replace(/[\[\]]/g, '') || '';
|
|
47
76
|
|
|
48
77
|
const resolved = context.selectorResolver.resolveSelector(
|
|
49
|
-
|
|
78
|
+
tableName, context.featureName, 'table', step.nth
|
|
50
79
|
);
|
|
51
80
|
|
|
81
|
+
const isGiven = isGivenContext(context);
|
|
52
82
|
return {
|
|
53
83
|
templateName: 'table-column-exists',
|
|
54
|
-
data: { ...resolved, columnName },
|
|
55
|
-
comment:
|
|
84
|
+
data: { ...resolved, columnName, isGiven },
|
|
85
|
+
comment: `${isGiven ? 'Wait' : 'Assert'} ${columnName} column in ${tableName} table`,
|
|
56
86
|
};
|
|
57
87
|
},
|
|
58
88
|
priority: 16,
|
|
59
89
|
},
|
|
60
90
|
|
|
61
|
-
// "User see [
|
|
91
|
+
// "User see [Table] table is empty"
|
|
62
92
|
{
|
|
63
93
|
name: 'table-is-empty',
|
|
64
94
|
matcher: (step) => {
|
|
@@ -69,165 +99,122 @@ export const tablePatterns: StepPattern[] = [
|
|
|
69
99
|
step.selectorRef!, context.featureName, 'table', step.nth
|
|
70
100
|
);
|
|
71
101
|
|
|
102
|
+
const isGiven = isGivenContext(context);
|
|
72
103
|
return {
|
|
73
104
|
templateName: 'table-empty',
|
|
74
|
-
data: { ...resolved },
|
|
75
|
-
comment:
|
|
105
|
+
data: { ...resolved, isGiven },
|
|
106
|
+
comment: `${isGiven ? 'Wait' : 'Assert'} ${step.selectorRef} table is empty`,
|
|
76
107
|
};
|
|
77
108
|
},
|
|
78
109
|
priority: 16,
|
|
79
110
|
},
|
|
80
111
|
|
|
81
|
-
// "User see [
|
|
112
|
+
// "User see [Ref] row in [Table] table with {{value}}" — row exists (enters row scope)
|
|
82
113
|
{
|
|
83
|
-
name: 'table-
|
|
114
|
+
name: 'table-row-exists',
|
|
84
115
|
matcher: (step) => {
|
|
85
|
-
return
|
|
86
|
-
/\
|
|
116
|
+
return step.elementType === 'row' &&
|
|
117
|
+
/\bin\b.*\btable\b/i.test(step.text) &&
|
|
118
|
+
/\bwith\b/i.test(step.text) &&
|
|
119
|
+
!!step.dataRef &&
|
|
120
|
+
!/\bis\s+hidden\b/i.test(step.text);
|
|
87
121
|
},
|
|
88
122
|
resolver: (step, context) => {
|
|
89
|
-
//
|
|
123
|
+
// brackets[0] = Ref (row label), brackets[1] = Table
|
|
90
124
|
const brackets = step.text.match(/\[([^\]]+)\]/g) || [];
|
|
91
|
-
const
|
|
92
|
-
const
|
|
125
|
+
const rowLabel = brackets[0]?.replace(/[\[\]]/g, '') || '';
|
|
126
|
+
const tableName = brackets.length >= 2
|
|
93
127
|
? brackets[1].replace(/[\[\]]/g, '')
|
|
94
128
|
: '';
|
|
95
129
|
|
|
96
|
-
// Extract data refs: first is filter, second is cell value
|
|
97
|
-
const dataRefs = step.text.match(/\{\{([^}]+)\}\}/g) || [];
|
|
98
|
-
const filterRef = dataRefs[0]?.replace(/\{\{|\}\}/g, '') || '';
|
|
99
|
-
const valueRef = dataRefs.length >= 2
|
|
100
|
-
? dataRefs[1].replace(/\{\{|\}\}/g, '')
|
|
101
|
-
: '';
|
|
102
|
-
|
|
103
|
-
const filterValue = filterRef
|
|
104
|
-
? context.dataResolver.resolveData(filterRef, context.featureName)
|
|
105
|
-
: '';
|
|
106
|
-
const cellValue = valueRef
|
|
107
|
-
? context.dataResolver.resolveData(valueRef, context.featureName)
|
|
108
|
-
: '';
|
|
109
|
-
|
|
110
130
|
const resolved = context.selectorResolver.resolveSelector(
|
|
111
131
|
tableName, context.featureName, 'table', step.nth
|
|
112
132
|
);
|
|
133
|
+
const filterValue = context.dataResolver.resolveData(step.dataRef!, context.featureName);
|
|
113
134
|
|
|
135
|
+
const isGiven = isGivenContext(context);
|
|
114
136
|
return {
|
|
115
|
-
templateName: 'table-
|
|
116
|
-
data: { ...resolved, filterValue,
|
|
117
|
-
comment:
|
|
137
|
+
templateName: 'table-row-exists',
|
|
138
|
+
data: { ...resolved, filterValue, isGiven },
|
|
139
|
+
comment: `${isGiven ? 'Wait' : 'Assert'} ${rowLabel} row in ${tableName} table with ${step.dataRef}`,
|
|
118
140
|
};
|
|
119
141
|
},
|
|
120
142
|
priority: 17,
|
|
121
143
|
},
|
|
122
144
|
|
|
123
|
-
// "User see [
|
|
145
|
+
// "User see [Ref] row in [Table] table with {{value}} is hidden"
|
|
124
146
|
{
|
|
125
|
-
name: 'table-
|
|
147
|
+
name: 'table-row-hidden',
|
|
126
148
|
matcher: (step) => {
|
|
127
|
-
return
|
|
128
|
-
/\
|
|
149
|
+
return step.elementType === 'row' &&
|
|
150
|
+
/\bin\b.*\btable\b/i.test(step.text) &&
|
|
151
|
+
/\bwith\b/i.test(step.text) &&
|
|
152
|
+
!!step.dataRef &&
|
|
153
|
+
/\bis\s+hidden\b/i.test(step.text);
|
|
129
154
|
},
|
|
130
155
|
resolver: (step, context) => {
|
|
156
|
+
// brackets[0] = Ref (row label), brackets[1] = Table
|
|
131
157
|
const brackets = step.text.match(/\[([^\]]+)\]/g) || [];
|
|
132
|
-
const
|
|
133
|
-
const
|
|
158
|
+
const rowLabel = brackets[0]?.replace(/[\[\]]/g, '') || '';
|
|
159
|
+
const tableName = brackets.length >= 2
|
|
134
160
|
? brackets[1].replace(/[\[\]]/g, '')
|
|
135
161
|
: '';
|
|
136
162
|
|
|
137
|
-
const rowMatch = step.text.match(/\brow\s+(\d+)\b/i);
|
|
138
|
-
const rowIndex = rowMatch ? parseInt(rowMatch[1], 10) : 1;
|
|
139
|
-
|
|
140
|
-
const cellValue = step.dataRef
|
|
141
|
-
? context.dataResolver.resolveData(step.dataRef, context.featureName)
|
|
142
|
-
: '';
|
|
143
|
-
|
|
144
163
|
const resolved = context.selectorResolver.resolveSelector(
|
|
145
164
|
tableName, context.featureName, 'table', step.nth
|
|
146
165
|
);
|
|
147
|
-
|
|
148
|
-
return {
|
|
149
|
-
templateName: 'table-cell-by-index',
|
|
150
|
-
data: { ...resolved, rowIndex, columnName, cellValue },
|
|
151
|
-
comment: `Assert ${tableName} table row ${rowIndex} ${columnName} cell = ${step.dataRef}`,
|
|
152
|
-
};
|
|
153
|
-
},
|
|
154
|
-
priority: 17,
|
|
155
|
-
},
|
|
156
|
-
|
|
157
|
-
// "User see [Users] table row with {{name}}" (preferred)
|
|
158
|
-
// Also accepts: "User see [Users] table has row with {{name}}" (backward compat)
|
|
159
|
-
{
|
|
160
|
-
name: 'table-row-exists',
|
|
161
|
-
matcher: (step) => {
|
|
162
|
-
return /\btable\s+(?:has\s+)?row\s+with\b/i.test(step.text) && !!step.dataRef;
|
|
163
|
-
},
|
|
164
|
-
resolver: (step, context) => {
|
|
165
|
-
const resolved = context.selectorResolver.resolveSelector(
|
|
166
|
-
step.selectorRef!, context.featureName, 'table', step.nth
|
|
167
|
-
);
|
|
168
|
-
const filterValue = context.dataResolver.resolveData(step.dataRef!, context.featureName);
|
|
169
|
-
|
|
170
|
-
return {
|
|
171
|
-
templateName: 'table-row-exists',
|
|
172
|
-
data: { ...resolved, filterValue },
|
|
173
|
-
comment: `Assert ${step.selectorRef} table has row with ${step.dataRef}`,
|
|
174
|
-
};
|
|
175
|
-
},
|
|
176
|
-
priority: 18,
|
|
177
|
-
},
|
|
178
|
-
|
|
179
|
-
// "User see [Users] table has no row with {{name}}"
|
|
180
|
-
{
|
|
181
|
-
name: 'table-row-not-exists',
|
|
182
|
-
matcher: (step) => {
|
|
183
|
-
return /\btable\s+has\s+no\s+row\s+with\b/i.test(step.text) && !!step.dataRef;
|
|
184
|
-
},
|
|
185
|
-
resolver: (step, context) => {
|
|
186
|
-
const resolved = context.selectorResolver.resolveSelector(
|
|
187
|
-
step.selectorRef!, context.featureName, 'table', step.nth
|
|
188
|
-
);
|
|
189
166
|
const filterValue = context.dataResolver.resolveData(step.dataRef!, context.featureName);
|
|
190
167
|
|
|
168
|
+
const isGiven = isGivenContext(context);
|
|
191
169
|
return {
|
|
192
170
|
templateName: 'table-row-not-exists',
|
|
193
|
-
data: { ...resolved, filterValue },
|
|
194
|
-
comment:
|
|
171
|
+
data: { ...resolved, filterValue, isGiven },
|
|
172
|
+
comment: `${isGiven ? 'Wait' : 'Assert'} ${rowLabel} row in ${tableName} table is hidden`,
|
|
195
173
|
};
|
|
196
174
|
},
|
|
197
175
|
priority: 19,
|
|
198
176
|
},
|
|
199
177
|
|
|
200
|
-
// "User click [
|
|
178
|
+
// "User click [Act] button in [Table] table with {{filter}}" — action in row
|
|
201
179
|
{
|
|
202
180
|
name: 'table-action-in-row',
|
|
203
181
|
matcher: (step) => {
|
|
204
|
-
return /\bin\b.*\btable\
|
|
182
|
+
return /\bin\b.*\btable\b/i.test(step.text) &&
|
|
183
|
+
/\bwith\b/i.test(step.text) &&
|
|
184
|
+
!!step.dataRef &&
|
|
185
|
+
step.elementType !== 'column' &&
|
|
186
|
+
step.elementType !== 'row' &&
|
|
187
|
+
step.elementType !== 'table';
|
|
205
188
|
},
|
|
206
189
|
resolver: (step, context) => {
|
|
190
|
+
// brackets[0] = Act (element name), brackets[1] = Table
|
|
207
191
|
const brackets = step.text.match(/\[([^\]]+)\]/g) || [];
|
|
208
192
|
const elementName = brackets[0]?.replace(/[\[\]]/g, '') || '';
|
|
209
193
|
const tableName = brackets.length >= 2
|
|
210
194
|
? brackets[1].replace(/[\[\]]/g, '')
|
|
211
195
|
: '';
|
|
212
196
|
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
const filterValue = filterRef
|
|
216
|
-
? context.dataResolver.resolveData(filterRef, context.featureName)
|
|
197
|
+
const filterValue = step.dataRef
|
|
198
|
+
? context.dataResolver.resolveData(step.dataRef, context.featureName)
|
|
217
199
|
: '';
|
|
218
200
|
|
|
219
201
|
const tableResolved = context.selectorResolver.resolveSelector(
|
|
220
202
|
tableName, context.featureName, 'table', 0
|
|
221
203
|
);
|
|
222
204
|
|
|
223
|
-
// Determine action
|
|
205
|
+
// Determine action from step text
|
|
224
206
|
const actionMatch = step.text.match(/\b(click|check|uncheck)\b/i);
|
|
225
207
|
const action = actionMatch ? actionMatch[1].toLowerCase() : 'click';
|
|
226
208
|
|
|
209
|
+
// Extract element type (word after [Act]) and map to ARIA role
|
|
210
|
+
const elementTypeMatch = step.text.match(/\]\s+([\w-]+)\s+in\b/i);
|
|
211
|
+
const elementType = elementTypeMatch ? elementTypeMatch[1].toLowerCase() : 'button';
|
|
212
|
+
const elementRole = typeToRole[elementType] || elementType;
|
|
213
|
+
|
|
227
214
|
return {
|
|
228
215
|
templateName: 'table-action-in-row',
|
|
229
|
-
data: { ...tableResolved, elementName, filterValue, action },
|
|
230
|
-
comment: `${action} ${elementName} in ${tableName} table row with ${
|
|
216
|
+
data: { ...tableResolved, elementName, filterValue, action, elementRole },
|
|
217
|
+
comment: `${action} ${elementName} in ${tableName} table row with ${step.dataRef}`,
|
|
231
218
|
};
|
|
232
219
|
},
|
|
233
220
|
priority: 17,
|
|
@@ -29,6 +29,11 @@ export class StepMapper {
|
|
|
29
29
|
private currentScenarioSteps?: ParsedStep[];
|
|
30
30
|
private inDialogScope: boolean = false;
|
|
31
31
|
private inFrameScope: boolean = false;
|
|
32
|
+
/** Tracks the last primary keyword (Given/When/Then) for resolving And/But */
|
|
33
|
+
private lastPrimaryKeyword: string = 'Given';
|
|
34
|
+
/** Row scope: when active, column assertions are scoped to this row */
|
|
35
|
+
private inRowScope: boolean = false;
|
|
36
|
+
private rowScopeTable: string = '';
|
|
32
37
|
|
|
33
38
|
constructor(options: { verbose?: boolean; baseURL?: string; featureName?: string; screenName?: string; featurePath?: string } = {}) {
|
|
34
39
|
this.verbose = options.verbose ?? false;
|
|
@@ -74,6 +79,9 @@ export class StepMapper {
|
|
|
74
79
|
// Reset dialog and frame scope at the start of each new scenario
|
|
75
80
|
this.inDialogScope = false;
|
|
76
81
|
this.inFrameScope = false;
|
|
82
|
+
this.inRowScope = false;
|
|
83
|
+
this.rowScopeTable = '';
|
|
84
|
+
this.lastPrimaryKeyword = 'Given';
|
|
77
85
|
this.templateEngine.resetBaseContext();
|
|
78
86
|
}
|
|
79
87
|
|
|
@@ -84,6 +92,12 @@ export class StepMapper {
|
|
|
84
92
|
mapStep(step: ParsedStep): MappedStep | Promise<MappedStep> {
|
|
85
93
|
this.stepCounter++;
|
|
86
94
|
|
|
95
|
+
// Track effective keyword: And/But inherit from previous Given/When/Then
|
|
96
|
+
const keyword = step.keyword.trim();
|
|
97
|
+
if (keyword === 'Given' || keyword === 'When' || keyword === 'Then') {
|
|
98
|
+
this.lastPrimaryKeyword = keyword;
|
|
99
|
+
}
|
|
100
|
+
|
|
87
101
|
// Frame scope directives — intercept before pattern matching
|
|
88
102
|
if (/\bswitch(?:es)?\s+to\b/i.test(step.text) &&
|
|
89
103
|
(step.elementType === 'frame' || step.elementType === 'iframe')) {
|
|
@@ -154,11 +168,29 @@ export class StepMapper {
|
|
|
154
168
|
return { code: '', comment: `Enter dialog scope${step.selectorRef ? ': ' + step.selectorRef : ''}` };
|
|
155
169
|
}
|
|
156
170
|
|
|
171
|
+
// Row-scoped column assertion: "User see [Col] column with {{value}}"
|
|
172
|
+
// Must be checked BEFORE pattern matching (like dialog/frame scope)
|
|
173
|
+
if (this.inRowScope && step.elementType === 'column' &&
|
|
174
|
+
!!step.dataRef && /\bwith\b/i.test(step.text) &&
|
|
175
|
+
!/\btable\b/i.test(step.text)) {
|
|
176
|
+
return this.generateRowScopedColumnAssertion(step);
|
|
177
|
+
}
|
|
178
|
+
|
|
157
179
|
// Try pattern registry first
|
|
158
180
|
const context = this.createPatternContext();
|
|
159
181
|
const mappedStep = this.patternRegistry.generateStep(step, context);
|
|
160
|
-
|
|
182
|
+
|
|
161
183
|
if (mappedStep) {
|
|
184
|
+
// Enter row scope when a row-in-table step is matched
|
|
185
|
+
if (step.elementType === 'row' && /\bin\b.*\btable\b/i.test(step.text) &&
|
|
186
|
+
/\bwith\b/i.test(step.text) && !/\bis\s+hidden\b/i.test(step.text)) {
|
|
187
|
+
const brackets = step.text.match(/\[([^\]]+)\]/g) || [];
|
|
188
|
+
this.inRowScope = true;
|
|
189
|
+
this.rowScopeTable = brackets.length >= 2
|
|
190
|
+
? brackets[1].replace(/[\[\]]/g, '')
|
|
191
|
+
: '';
|
|
192
|
+
}
|
|
193
|
+
|
|
162
194
|
if (this.verbose) {
|
|
163
195
|
console.log(` ✓ Pattern matched: ${step.text.substring(0, 50)}...`);
|
|
164
196
|
}
|
|
@@ -172,6 +204,59 @@ export class StepMapper {
|
|
|
172
204
|
};
|
|
173
205
|
}
|
|
174
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Generate column assertion scoped to the current row.
|
|
209
|
+
* Uses columns config for exact cell targeting (nth), or filter fallback.
|
|
210
|
+
*
|
|
211
|
+
* With columns: await expect(tableRow.getByRole('cell').nth(index)).toHaveText('value');
|
|
212
|
+
* Without: await expect(tableRow.getByRole('cell').filter({ hasText: 'value' })).toBeVisible();
|
|
213
|
+
*/
|
|
214
|
+
private generateRowScopedColumnAssertion(step: ParsedStep): MappedStep {
|
|
215
|
+
const columnRef = step.selectorRef || '';
|
|
216
|
+
const cellValue = step.dataRef
|
|
217
|
+
? this.dataResolver.resolveData(step.dataRef, this.featureName)
|
|
218
|
+
: '';
|
|
219
|
+
|
|
220
|
+
// Try to find column index from table's columns config
|
|
221
|
+
let columnIndex: number | undefined;
|
|
222
|
+
if (this.rowScopeTable) {
|
|
223
|
+
try {
|
|
224
|
+
const tableResolved = this.selectorResolver.resolveSelector(
|
|
225
|
+
this.rowScopeTable, this.featureName, 'table', 0
|
|
226
|
+
);
|
|
227
|
+
const columnKey = columnRef.toLowerCase().normalize('NFC');
|
|
228
|
+
columnIndex = tableResolved.columns?.[columnKey]?.index;
|
|
229
|
+
} catch {
|
|
230
|
+
// Table not found — fallback to filter
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const isGiven = this.lastPrimaryKeyword === 'Given';
|
|
235
|
+
const escapedValue = cellValue.replace(/'/g, "\\'");
|
|
236
|
+
|
|
237
|
+
let code: string;
|
|
238
|
+
if (columnIndex !== undefined) {
|
|
239
|
+
// Exact cell: nth(index) + toHaveText
|
|
240
|
+
if (isGiven) {
|
|
241
|
+
code = `await expect(tableRow.getByRole('cell').nth(${columnIndex})).toHaveText('${escapedValue}');`;
|
|
242
|
+
} else {
|
|
243
|
+
code = `await expect(tableRow.getByRole('cell').nth(${columnIndex})).toHaveText('${escapedValue}');`;
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
// Fallback: filter by text
|
|
247
|
+
if (isGiven) {
|
|
248
|
+
code = `await tableRow.getByRole('cell').filter({ hasText: '${escapedValue}' }).waitFor();`;
|
|
249
|
+
} else {
|
|
250
|
+
code = `await expect(tableRow.getByRole('cell').filter({ hasText: '${escapedValue}' })).toBeVisible();`;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
code,
|
|
256
|
+
comment: `${isGiven ? 'Wait' : 'Assert'} ${columnRef} column = ${step.dataRef}`,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
175
260
|
/**
|
|
176
261
|
* Create pattern context for generators
|
|
177
262
|
*/
|
|
@@ -191,6 +276,7 @@ export class StepMapper {
|
|
|
191
276
|
screenName: this.screenName,
|
|
192
277
|
featurePath: this.featurePath, // Pass feature path to patterns
|
|
193
278
|
scenarioSteps: this.currentScenarioSteps || [], // Pass scenario steps for path variables
|
|
279
|
+
effectiveKeyword: this.lastPrimaryKeyword,
|
|
194
280
|
};
|
|
195
281
|
}
|
|
196
282
|
|
|
@@ -16,13 +16,13 @@ export const AI_RULES_FILE_MAPPING: [string, string][] = [
|
|
|
16
16
|
|
|
17
17
|
// Commands — Claude Code
|
|
18
18
|
['claude-cmd-add-screen.md', '.claude/commands/sungen/add-screen.md'],
|
|
19
|
-
['claude-cmd-
|
|
20
|
-
['claude-cmd-
|
|
19
|
+
['claude-cmd-create-test.md', '.claude/commands/sungen/create-test.md'],
|
|
20
|
+
['claude-cmd-run-test.md', '.claude/commands/sungen/run-test.md'],
|
|
21
21
|
|
|
22
22
|
// Commands — GitHub Copilot
|
|
23
23
|
['copilot-cmd-add-screen.md', '.github/prompts/sungen-add-screen.prompt.md'],
|
|
24
|
-
['copilot-cmd-
|
|
25
|
-
['copilot-cmd-
|
|
24
|
+
['copilot-cmd-create-test.md', '.github/prompts/sungen-create-test.prompt.md'],
|
|
25
|
+
['copilot-cmd-run-test.md', '.github/prompts/sungen-run-test.prompt.md'],
|
|
26
26
|
|
|
27
27
|
// Skills — Claude Code
|
|
28
28
|
['claude-skill-gherkin-syntax.md', '.claude/skills/sungen-gherkin-syntax/SKILL.md'],
|
|
@@ -30,7 +30,8 @@ export const AI_RULES_FILE_MAPPING: [string, string][] = [
|
|
|
30
30
|
['claude-skill-error-mapping.md', '.claude/skills/sungen-error-mapping/SKILL.md'],
|
|
31
31
|
['claude-skill-tc-generation.md', '.claude/skills/sungen-tc-generation/SKILL.md'],
|
|
32
32
|
['claude-skill-selector-fix.md', '.claude/skills/sungen-selector-fix/SKILL.md'],
|
|
33
|
-
['claude-skill-
|
|
33
|
+
['claude-skill-tc-review.md', '.claude/skills/sungen-tc-review/SKILL.md'],
|
|
34
|
+
['claude-skill-viewpoint.md', '.claude/skills/sungen-viewpoint/SKILL.md'],
|
|
34
35
|
|
|
35
36
|
// Skills — GitHub Copilot
|
|
36
37
|
['github-skill-sungen-gherkin-syntax.md', '.github/skills/sungen-gherkin-syntax/SKILL.md'],
|
|
@@ -38,7 +39,8 @@ export const AI_RULES_FILE_MAPPING: [string, string][] = [
|
|
|
38
39
|
['github-skill-sungen-error-mapping.md', '.github/skills/sungen-error-mapping/SKILL.md'],
|
|
39
40
|
['github-skill-sungen-tc-generation.md', '.github/skills/sungen-tc-generation/SKILL.md'],
|
|
40
41
|
['github-skill-sungen-selector-fix.md', '.github/skills/sungen-selector-fix/SKILL.md'],
|
|
41
|
-
['github-skill-sungen-
|
|
42
|
+
['github-skill-sungen-tc-review.md', '.github/skills/sungen-tc-review/SKILL.md'],
|
|
43
|
+
['github-skill-sungen-viewpoint.md', '.github/skills/sungen-viewpoint/SKILL.md'],
|
|
42
44
|
];
|
|
43
45
|
|
|
44
46
|
export class AIRulesUpdater {
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
import * as fs from 'fs';
|
|
7
7
|
import * as path from 'path';
|
|
8
8
|
import { execSync } from 'child_process';
|
|
9
|
-
import { AI_RULES_FILE_MAPPING } from './ai-rules-updater';
|
|
10
9
|
|
|
11
10
|
export class ProjectInitializer {
|
|
12
11
|
private baseCwd: string;
|
|
@@ -251,7 +250,44 @@ export class ProjectInitializer {
|
|
|
251
250
|
private createAIRules(): void {
|
|
252
251
|
const aiTemplateDir = path.join(__dirname, 'templates', 'ai-instructions');
|
|
253
252
|
|
|
254
|
-
|
|
253
|
+
// File mapping: [templateFile, outputPath]
|
|
254
|
+
const fileMapping: [string, string][] = [
|
|
255
|
+
// Config
|
|
256
|
+
['claude-config.md', 'CLAUDE.md'],
|
|
257
|
+
['copilot-config.md', '.github/copilot-instructions.md'],
|
|
258
|
+
|
|
259
|
+
// Commands — Claude Code
|
|
260
|
+
['claude-cmd-add-screen.md', '.claude/commands/sungen/add-screen.md'],
|
|
261
|
+
['claude-cmd-create-test.md', '.claude/commands/sungen/create-test.md'],
|
|
262
|
+
['claude-cmd-run-test.md', '.claude/commands/sungen/run-test.md'],
|
|
263
|
+
['claude-cmd-review.md', '.claude/commands/sungen/review.md'],
|
|
264
|
+
|
|
265
|
+
// Commands — GitHub Copilot
|
|
266
|
+
['copilot-cmd-add-screen.md', '.github/prompts/sungen-add-screen.prompt.md'],
|
|
267
|
+
['copilot-cmd-create-test.md', '.github/prompts/sungen-create-test.prompt.md'],
|
|
268
|
+
['copilot-cmd-run-test.md', '.github/prompts/sungen-run-test.prompt.md'],
|
|
269
|
+
['copilot-cmd-review.md', '.github/prompts/sungen-review.prompt.md'],
|
|
270
|
+
|
|
271
|
+
// Skills — Claude Code
|
|
272
|
+
['claude-skill-gherkin-syntax.md', '.claude/skills/sungen-gherkin-syntax/SKILL.md'],
|
|
273
|
+
['claude-skill-selector-keys.md', '.claude/skills/sungen-selector-keys/SKILL.md'],
|
|
274
|
+
['claude-skill-error-mapping.md', '.claude/skills/sungen-error-mapping/SKILL.md'],
|
|
275
|
+
['claude-skill-tc-generation.md', '.claude/skills/sungen-tc-generation/SKILL.md'],
|
|
276
|
+
['claude-skill-selector-fix.md', '.claude/skills/sungen-selector-fix/SKILL.md'],
|
|
277
|
+
['claude-skill-tc-review.md', '.claude/skills/sungen-tc-review/SKILL.md'],
|
|
278
|
+
['claude-skill-viewpoint.md', '.claude/skills/sungen-viewpoint/SKILL.md'],
|
|
279
|
+
|
|
280
|
+
// Skills — GitHub Copilot (separate copies with Copilot-friendly descriptions)
|
|
281
|
+
['github-skill-sungen-gherkin-syntax.md', '.github/skills/sungen-gherkin-syntax/SKILL.md'],
|
|
282
|
+
['github-skill-sungen-selector-keys.md', '.github/skills/sungen-selector-keys/SKILL.md'],
|
|
283
|
+
['github-skill-sungen-error-mapping.md', '.github/skills/sungen-error-mapping/SKILL.md'],
|
|
284
|
+
['github-skill-sungen-tc-generation.md', '.github/skills/sungen-tc-generation/SKILL.md'],
|
|
285
|
+
['github-skill-sungen-selector-fix.md', '.github/skills/sungen-selector-fix/SKILL.md'],
|
|
286
|
+
['github-skill-sungen-tc-review.md', '.github/skills/sungen-tc-review/SKILL.md'],
|
|
287
|
+
['github-skill-sungen-viewpoint.md', '.github/skills/sungen-viewpoint/SKILL.md'],
|
|
288
|
+
];
|
|
289
|
+
|
|
290
|
+
for (const [templateFile, outputRelPath] of fileMapping) {
|
|
255
291
|
const outputPath = path.join(this.cwd, outputRelPath);
|
|
256
292
|
|
|
257
293
|
if (fs.existsSync(outputPath)) {
|
|
@@ -134,7 +134,7 @@ export class ScreenManager {
|
|
|
134
134
|
console.log('Next steps:');
|
|
135
135
|
console.log(` 1. Fill requirements/spec.md with screen spec (fields, validation, business rules)`);
|
|
136
136
|
console.log(` Optionally add UI designs to requirements/ui/ (screenshots, mockups)`);
|
|
137
|
-
console.log(` 2. Generate test cases: /sungen:
|
|
137
|
+
console.log(` 2. Generate test cases: /sungen:create-test ${screenName} (or /sungen-create-test)`);
|
|
138
138
|
console.log(` 3. Compile: sungen generate --screen ${screenName}`);
|
|
139
139
|
console.log(` 4. Run: npx playwright test\n`);
|
|
140
140
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: add-screen
|
|
3
|
-
description: 'Add a new Sungen screen — scaffolds directories and delegates to
|
|
3
|
+
description: 'Add a new Sungen screen — scaffolds directories and delegates to create-test for test case creation'
|
|
4
4
|
argument-hint: [screen-name] [url-path]
|
|
5
5
|
allowed-tools: Read, Grep, Bash, Glob, AskUserQuestion
|
|
6
6
|
---
|
|
@@ -37,10 +37,10 @@ Ask the user: "Would you like to fill in `requirements/spec.md` now? This helps
|
|
|
37
37
|
|
|
38
38
|
Ask the user: "Would you like to create test cases now?"
|
|
39
39
|
|
|
40
|
-
If yes → **you MUST use the Skill tool** to invoke `/sungen:
|
|
40
|
+
If yes → **you MUST use the Skill tool** to invoke `/sungen:create-test <screen>`. This is critical because `create-test` auto-loads the `sungen-gherkin-syntax` and `sungen-tc-generation` skills which contain the full Gherkin syntax rules, pattern shapes, viewpoint checklists, and output format. **Do NOT attempt to generate test cases yourself** — always invoke the Skill tool so these skills are properly loaded.
|
|
41
41
|
|
|
42
42
|
```
|
|
43
|
-
Skill:
|
|
43
|
+
Skill: create-test
|
|
44
44
|
Args: <screen>
|
|
45
45
|
```
|
|
46
46
|
|
|
@@ -48,5 +48,5 @@ Args: <screen>
|
|
|
48
48
|
|
|
49
49
|
If the user declined test case creation, tell them next steps:
|
|
50
50
|
- Fill `requirements/spec.md` with screen specs (if not done)
|
|
51
|
-
- Run `/sungen:
|
|
52
|
-
- Run `/sungen:
|
|
51
|
+
- Run `/sungen:create-test <screen>` to create test cases
|
|
52
|
+
- Run `/sungen:run-test <screen>` to generate selectors, compile, and run tests
|