@sun-asterisk/sungen 2.1.0 → 2.2.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.
Files changed (173) hide show
  1. package/README.md +78 -51
  2. package/dist/cli/index.js +1 -1
  3. package/dist/generators/test-generator/adapters/adapter-interface.d.ts +6 -1
  4. package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
  5. package/dist/generators/test-generator/adapters/playwright/templates/scenario.hbs +0 -4
  6. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/drag-action.hbs +1 -0
  7. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/expand-action.hbs +11 -0
  8. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/fill-action.hbs +1 -1
  9. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/fill-editor-action.hbs +1 -1
  10. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/toggle-action.hbs +1 -0
  11. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/checked-assertion.hbs +0 -1
  12. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/column-cell-assertion.hbs +0 -1
  13. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/contain-text-assertion.hbs +0 -1
  14. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/count-assertion.hbs +0 -1
  15. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-assertion.hbs +0 -1
  16. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-with-filter-assertion.hbs +0 -1
  17. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +0 -1
  18. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-with-variable-assertion.hbs +0 -1
  19. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/empty-assertion.hbs +0 -1
  20. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/enabled-assertion.hbs +0 -1
  21. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/focused-assertion.hbs +0 -1
  22. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/have-text-assertion.hbs +0 -1
  23. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-dialog-heading-assertion.hbs +0 -1
  24. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-filter-assertion.hbs +0 -1
  25. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +0 -1
  26. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-variable-assertion.hbs +0 -1
  27. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/is-hidden-assertion.hbs +0 -1
  28. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/label-value-assertion.hbs +0 -1
  29. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/list-item-count-assertion.hbs +0 -1
  30. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/loading-assertion.hbs +1 -0
  31. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/not-checked-assertion.hbs +0 -1
  32. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/page-assertion.hbs +0 -1
  33. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/selected-assertion.hbs +1 -0
  34. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/sorted-assertion.hbs +1 -0
  35. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-cell-by-filter.hbs +0 -1
  36. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-cell-by-index.hbs +0 -1
  37. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-column-exists.hbs +0 -1
  38. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-empty.hbs +0 -1
  39. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-count.hbs +0 -1
  40. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-exists.hbs +0 -1
  41. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-not-exists.hbs +0 -1
  42. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-assertion.hbs +0 -1
  43. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-dialog-heading-assertion.hbs +0 -1
  44. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-locator-variable-assertion.hbs +0 -1
  45. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-role-variable-assertion.hbs +0 -1
  46. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-value-assertion.hbs +0 -1
  47. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-variable-assertion.hbs +0 -1
  48. package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/navigation.hbs +1 -1
  49. package/dist/generators/test-generator/adapters/playwright/templates/test-file.hbs +25 -0
  50. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  51. package/dist/generators/test-generator/code-generator.js +41 -3
  52. package/dist/generators/test-generator/code-generator.js.map +1 -1
  53. package/dist/generators/test-generator/patterns/assertion-patterns.d.ts.map +1 -1
  54. package/dist/generators/test-generator/patterns/assertion-patterns.js +58 -6
  55. package/dist/generators/test-generator/patterns/assertion-patterns.js.map +1 -1
  56. package/dist/generators/test-generator/patterns/form-patterns.js +3 -3
  57. package/dist/generators/test-generator/patterns/form-patterns.js.map +1 -1
  58. package/dist/generators/test-generator/patterns/interaction-patterns.d.ts.map +1 -1
  59. package/dist/generators/test-generator/patterns/interaction-patterns.js +86 -1
  60. package/dist/generators/test-generator/patterns/interaction-patterns.js.map +1 -1
  61. package/dist/generators/test-generator/patterns/navigation-patterns.js +2 -2
  62. package/dist/generators/test-generator/patterns/navigation-patterns.js.map +1 -1
  63. package/dist/generators/test-generator/template-engine.d.ts +6 -0
  64. package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
  65. package/dist/generators/test-generator/template-engine.js +1 -0
  66. package/dist/generators/test-generator/template-engine.js.map +1 -1
  67. package/dist/generators/test-generator/utils/selector-resolver.d.ts +15 -8
  68. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  69. package/dist/generators/test-generator/utils/selector-resolver.js +26 -197
  70. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  71. package/dist/orchestrator/project-initializer.d.ts +4 -0
  72. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  73. package/dist/orchestrator/project-initializer.js +49 -4
  74. package/dist/orchestrator/project-initializer.js.map +1 -1
  75. package/dist/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +4 -3
  76. package/dist/orchestrator/templates/ai-instructions/claude-cmd-make-tc.md +11 -46
  77. package/dist/orchestrator/templates/ai-instructions/claude-cmd-make-test.md +11 -46
  78. package/dist/orchestrator/templates/ai-instructions/claude-config.md +9 -8
  79. package/dist/orchestrator/templates/ai-instructions/claude-skill-error-mapping.md +11 -0
  80. package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +8 -4
  81. package/dist/orchestrator/templates/ai-instructions/claude-skill-selector-fix.md +206 -0
  82. package/dist/orchestrator/templates/ai-instructions/claude-skill-selector-keys.md +19 -21
  83. package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +256 -0
  84. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +14 -17
  85. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-make-tc.md +16 -47
  86. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-make-test.md +16 -47
  87. package/dist/orchestrator/templates/ai-instructions/copilot-config.md +8 -7
  88. package/dist/orchestrator/templates/ai-instructions/{copilot-skill-error-mapping.md → github-skill-sungen-error-mapping.md} +14 -3
  89. package/{src/orchestrator/templates/ai-instructions/copilot-skill-gherkin-syntax.md → dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md} +11 -7
  90. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md +206 -0
  91. package/dist/orchestrator/templates/ai-instructions/{copilot-skill-selector-keys.md → github-skill-sungen-selector-keys.md} +22 -24
  92. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +256 -0
  93. package/dist/orchestrator/templates/playwright.config.d.ts.map +1 -1
  94. package/dist/orchestrator/templates/playwright.config.js +3 -1
  95. package/dist/orchestrator/templates/playwright.config.js.map +1 -1
  96. package/dist/orchestrator/templates/playwright.config.ts +3 -1
  97. package/dist/orchestrator/templates/readme.md +78 -101
  98. package/package.json +1 -1
  99. package/src/cli/index.ts +1 -1
  100. package/src/generators/test-generator/adapters/adapter-interface.ts +7 -1
  101. package/src/generators/test-generator/adapters/playwright/templates/scenario.hbs +0 -4
  102. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/drag-action.hbs +1 -0
  103. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/expand-action.hbs +11 -0
  104. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/fill-action.hbs +1 -1
  105. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/fill-editor-action.hbs +1 -1
  106. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/toggle-action.hbs +1 -0
  107. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/checked-assertion.hbs +0 -1
  108. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/column-cell-assertion.hbs +0 -1
  109. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/contain-text-assertion.hbs +0 -1
  110. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/count-assertion.hbs +0 -1
  111. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-assertion.hbs +0 -1
  112. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-with-filter-assertion.hbs +0 -1
  113. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-with-role-variable-assertion.hbs +0 -1
  114. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/disabled-with-variable-assertion.hbs +0 -1
  115. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/empty-assertion.hbs +0 -1
  116. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/enabled-assertion.hbs +0 -1
  117. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/focused-assertion.hbs +0 -1
  118. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/have-text-assertion.hbs +0 -1
  119. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-dialog-heading-assertion.hbs +0 -1
  120. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-filter-assertion.hbs +0 -1
  121. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-role-variable-assertion.hbs +0 -1
  122. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/hidden-with-variable-assertion.hbs +0 -1
  123. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/is-hidden-assertion.hbs +0 -1
  124. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/label-value-assertion.hbs +0 -1
  125. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/list-item-count-assertion.hbs +0 -1
  126. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/loading-assertion.hbs +1 -0
  127. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/not-checked-assertion.hbs +0 -1
  128. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/page-assertion.hbs +0 -1
  129. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/selected-assertion.hbs +1 -0
  130. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/sorted-assertion.hbs +1 -0
  131. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-cell-by-filter.hbs +0 -1
  132. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-cell-by-index.hbs +0 -1
  133. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-column-exists.hbs +0 -1
  134. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-empty.hbs +0 -1
  135. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-count.hbs +0 -1
  136. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-exists.hbs +0 -1
  137. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-not-exists.hbs +0 -1
  138. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-assertion.hbs +0 -1
  139. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-dialog-heading-assertion.hbs +0 -1
  140. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-locator-variable-assertion.hbs +0 -1
  141. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-role-variable-assertion.hbs +0 -1
  142. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-value-assertion.hbs +0 -1
  143. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/visible-with-variable-assertion.hbs +0 -1
  144. package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/navigation.hbs +1 -1
  145. package/src/generators/test-generator/adapters/playwright/templates/test-file.hbs +25 -0
  146. package/src/generators/test-generator/code-generator.ts +50 -8
  147. package/src/generators/test-generator/patterns/assertion-patterns.ts +62 -6
  148. package/src/generators/test-generator/patterns/form-patterns.ts +3 -3
  149. package/src/generators/test-generator/patterns/interaction-patterns.ts +93 -1
  150. package/src/generators/test-generator/patterns/navigation-patterns.ts +2 -2
  151. package/src/generators/test-generator/template-engine.ts +4 -0
  152. package/src/generators/test-generator/utils/selector-resolver.ts +27 -204
  153. package/src/orchestrator/project-initializer.ts +60 -5
  154. package/src/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +4 -3
  155. package/src/orchestrator/templates/ai-instructions/claude-cmd-make-tc.md +11 -46
  156. package/src/orchestrator/templates/ai-instructions/claude-cmd-make-test.md +11 -46
  157. package/src/orchestrator/templates/ai-instructions/claude-config.md +9 -8
  158. package/src/orchestrator/templates/ai-instructions/claude-skill-error-mapping.md +11 -0
  159. package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +8 -4
  160. package/src/orchestrator/templates/ai-instructions/claude-skill-selector-fix.md +206 -0
  161. package/src/orchestrator/templates/ai-instructions/claude-skill-selector-keys.md +19 -21
  162. package/src/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +256 -0
  163. package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +14 -17
  164. package/src/orchestrator/templates/ai-instructions/copilot-cmd-make-tc.md +16 -47
  165. package/src/orchestrator/templates/ai-instructions/copilot-cmd-make-test.md +16 -47
  166. package/src/orchestrator/templates/ai-instructions/copilot-config.md +8 -7
  167. package/src/orchestrator/templates/ai-instructions/{copilot-skill-error-mapping.md → github-skill-sungen-error-mapping.md} +14 -3
  168. package/{dist/orchestrator/templates/ai-instructions/copilot-skill-gherkin-syntax.md → src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md} +11 -7
  169. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md +206 -0
  170. package/src/orchestrator/templates/ai-instructions/{copilot-skill-selector-keys.md → github-skill-sungen-selector-keys.md} +22 -24
  171. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +256 -0
  172. package/src/orchestrator/templates/playwright.config.ts +3 -1
  173. package/src/orchestrator/templates/readme.md +78 -101
@@ -17,11 +17,17 @@ sungen generate → compiles Gherkin + selectors + data → Playwright .spec.ts
17
17
  │ ├── selectors/ # Element locator YAML mappings
18
18
  │ └── test-data/ # Test data YAML values
19
19
  ├── specs/
20
- ├── generated/ # Auto-generated Playwright tests
21
- │ └── .auth/ # Auth storage states
20
+ └── generated/ # Auto-generated Playwright tests
21
+ ├── .claude/
22
+ │ ├── commands/sungen/ # Claude Code slash commands
23
+ │ └── skills/ # Claude Code auto-loaded skills
22
24
  ├── .github/
23
- └── copilot-instructions.md # AI rules for GitHub Copilot
24
- └── CLAUDE.md # AI rules for Claude Code
25
+ ├── copilot-instructions.md # AI rules for GitHub Copilot
26
+ └── prompts/ # Copilot slash commands
27
+ ├── .vscode/
28
+ │ └── settings.json # Copilot auto-attach settings
29
+ ├── CLAUDE.md # AI rules for Claude Code
30
+ └── playwright.config.ts # Playwright config (timeout: 10s, workers: 2)
25
31
  ```
26
32
 
27
33
  ## Workflow
@@ -32,45 +38,50 @@ Step 1 Step 2 Step 3
32
38
  │ /add- │────────▶│ /make-tc │────────▶│ /make- │
33
39
  │ screen │ │ │ │ test │
34
40
  └──────────┘ └──────────┘ └──────────┘
35
- Scaffold Design TCs Compile &
36
- directories & generate run tests
37
- 3 files
41
+ Scaffold Pick sections Generate
42
+ directories design TCs selectors
43
+ generate → compile
44
+ .feature + → run tests
45
+ test-data → auto-fix
38
46
  ```
39
47
 
40
48
  Use AI commands (Claude Code or GitHub Copilot) to drive the workflow:
41
49
 
42
50
  ### Step 1: Add a screen
43
51
 
44
- | Claude Code | GitHub Copilot |
52
+ | Claude Code | GitHub Copilot (VS Code) |
45
53
  |---|---|
46
- | `/sungen:add-screen login /login` | `@workspace /sungen-add-screen login /login` |
54
+ | `/sungen:add-screen` | `/sungen-add-screen` |
47
55
 
48
56
  Scaffolds `qa/screens/<name>/` with empty feature, selectors, and test-data files, then asks if you want to create test cases.
49
57
 
50
58
  ### Step 2: Create test cases
51
59
 
52
- | Claude Code | GitHub Copilot |
60
+ | Claude Code | GitHub Copilot (VS Code) |
53
61
  |---|---|
54
- | `/sungen:make-tc login` | `@workspace /sungen-make-tc login` |
62
+ | `/sungen:make-tc login` | `/sungen-make-tc login` |
55
63
 
56
- AI acts as a **Senior QA Engineer**: explores the live page (via Playwright MCP) or analyzes static designs, gathers test viewpoints (UI/UX, Validation, Logic, Security), and generates the 3 files.
64
+ AI acts as a **Senior QA Engineer**:
65
+ 1. Explores the live page via Playwright MCP (asks user to log in if auth needed)
66
+ 2. Identifies screen sections → asks user which to focus on
67
+ 3. Generates **20+ scenarios per viewpoint** (UI/UX, Validation, Logic, Security) for each section
68
+ 4. Confirms test plan before generating `.feature` + `test-data.yaml`
57
69
 
58
70
  ### Step 3: Compile & run tests
59
71
 
60
- | Claude Code | GitHub Copilot |
72
+ | Claude Code | GitHub Copilot (VS Code) |
61
73
  |---|---|
62
- | `/sungen:make-test login` | `@workspace /sungen-make-test login` |
74
+ | `/sungen:make-test login` | `/sungen-make-test login` |
63
75
 
64
- AI acts as a **Senior Developer**: compiles Gherkin → Playwright `.spec.ts`, runs tests, and auto-fixes selectors/test-data on failure (up to 5 attempts).
76
+ AI acts as a **Senior Developer**:
77
+ 1. Navigates to live page, takes snapshot, verifies DOM properties
78
+ 2. Generates `selectors.yaml` from exact accessible names
79
+ 3. Compiles Gherkin → Playwright `.spec.ts`
80
+ 4. Runs tests, auto-fixes selectors on failure (up to 5 attempts)
65
81
 
66
- ### Auth setup (if needed)
82
+ ### Auth setup
67
83
 
68
- If any page requires authentication, run manually before Step 2:
69
-
70
- ```bash
71
- sungen makeauth admin --url <baseURL>
72
- # Opens browser → login manually → saves specs/.auth/admin.json
73
- ```
84
+ If any page requires authentication, the AI will ask you to **log in manually via the MCP browser** during Step 2 or Step 3. No separate auth command needed.
74
85
 
75
86
  ### Manual CLI commands
76
87
 
@@ -85,7 +96,24 @@ npx playwright test --ui # Interactive mode
85
96
 
86
97
  ---
87
98
 
88
- ## Gherkin Guide
99
+ ## VS Code Copilot Setup
100
+
101
+ Add to `.vscode/settings.json` to auto-load Gherkin syntax when editing `.feature` files:
102
+
103
+ ```json
104
+ {
105
+ "github.copilot.chat.codeGeneration.instructions": [
106
+ {
107
+ "file": ".github/prompts/sungen-gherkin-syntax.prompt.md",
108
+ "applyTo": "**/*.feature"
109
+ }
110
+ ]
111
+ }
112
+ ```
113
+
114
+ ---
115
+
116
+ ## Gherkin Quick Reference
89
117
 
90
118
  ### Syntax
91
119
 
@@ -97,101 +125,50 @@ User <action> [<Target>] <type> with {{<Value>}}
97
125
  - `{{Value}}` → test data reference → lookup in `test-data/*.yaml`
98
126
  - `<type>` → element type: button, link, field, heading, text, etc.
99
127
 
100
- ### Pattern Shapes (17 total)
101
-
102
- #### Actions
103
-
104
- | Pattern | Example |
105
- |---|---|
106
- | Simple click | `User click [Submit] button` |
107
- | Click with data | `User click [Teammate] button with {{name}}` |
108
- | Fill field | `User fill [Email] field with {{email}}` |
109
- | Check/Uncheck | `User check [Remember me] checkbox` |
110
- | Select dropdown | `User select [Country] dropdown with {{country}}` |
111
- | Upload file | `User upload [Avatar] file with {{path}}` |
112
- | Double click | `User double click [Cell] element` |
113
- | Hover | `User hover [Info] icon` |
114
- | Clear | `User clear [Search] field` |
115
-
116
- #### Keyboard
117
-
118
- | Pattern | Example |
119
- |---|---|
120
- | Global key | `User press Escape key` |
121
- | Key on element | `User press Enter on [Search] field` |
122
-
123
- #### Navigation
124
-
125
- | Pattern | Example |
126
- |---|---|
127
- | Open page | `User is on [login] page` |
128
- | See page (URL) | `User see [dashboard] page` |
129
-
130
- #### Wait
131
-
132
- | Pattern | Example |
133
- |---|---|
134
- | Wait timeout | `User wait for 3 seconds` |
135
- | Wait element | `User wait for [Modal] dialog` |
136
- | Wait hidden | `User wait for [Loading] spinner is hidden` |
137
- | Wait with data | `User wait for [Dialog] dialog with {{title}}` |
138
-
139
- #### Assertions
140
-
141
- | Pattern | Example |
142
- |---|---|
143
- | See element | `User see [Welcome] heading` |
144
- | See with value | `User see [Title] heading with {{title}}` |
145
- | State check | `User see [Submit] button is disabled` |
146
- | State + data | `User see [Panel] dialog with {{title}} is hidden` |
147
- | Text contains | `User see [Message] text contains {{partial}}` |
148
-
149
- States: `hidden`, `visible`, `disabled`, `enabled`, `checked`, `unchecked`, `focused`, `empty`
150
-
151
- #### Table
128
+ ### Key Patterns
152
129
 
153
130
  | Pattern | Example |
154
131
  |---|---|
155
- | Row exists | `User see [Users] table has row with {{name}}` |
156
- | No row | `User see [Users] table has no row with {{name}}` |
157
- | Row count | `User see [Users] table has {{count}} rows` |
158
- | Column exists | `User see [Users] table has [Email] column` |
159
- | Cell by filter | `User see [Users] table row with {{name}} has [Status] with {{status}}` |
160
- | Cell by index | `User see [Users] table row 1 [Name] cell with {{name}}` |
161
- | Action in row | `User click [Edit] in [Users] table row with {{name}}` |
162
- | Empty table | `User see [Users] table is empty` |
163
-
164
- #### Scope
165
-
166
- | Pattern | Example |
167
- |---|---|
168
- | Scroll | `User scroll to [Footer] section` |
169
- | Frame enter | `User switch to [Payment] frame` |
170
- | Frame exit | `User switch to [main] frame` |
132
+ | Navigate | `User is on [login] page` |
133
+ | Click | `User click [Submit] button` |
134
+ | Fill | `User fill [Email] field with {{email}}` |
135
+ | Assert visible | `User see [Welcome] heading is visible` |
136
+ | Assert text | `User see [Title] heading with {{title}}` |
137
+ | Assert state | `User see [Submit] button is disabled` |
138
+ | Wait for | `User wait for [Modal] dialog is visible` |
139
+ | Table row | `User see [Users] table has row with {{name}}` |
140
+ | Table column | `User see [Users] table has [Email] column` |
141
+
142
+ States: `hidden` `visible` `disabled` `enabled` `checked` `unchecked` `focused` `empty` `loading` `selected`
171
143
 
172
144
  ### Tags
173
145
 
174
146
  | Tag | Purpose |
175
147
  |---|---|
176
148
  | `@auth:role` | Use Playwright storage state for auth |
149
+ | `@no-auth` | Disable inherited auth for this scenario |
177
150
  | `@steps:name` | Define reusable step group |
178
151
  | `@extend:name` | Inherit steps from another scenario |
179
152
  | `@manual` | Skip scenario in generation |
180
153
 
181
- ### Selector YAML
154
+ ### YAML Selector Keys
155
+
156
+ Keys are **lowercase with spaces**, Unicode preserved:
157
+ - `[Search Content]` → `search content:`
158
+ - `[Thời gian]` → `thời gian:`
159
+ - Same label, different types → `add campaign--button:` / `add campaign--text:`
160
+
161
+ ### Selector Types
182
162
 
183
163
  ```yaml
184
- submit.button:
185
- type: 'role' # testid, role, text, label, placeholder, locator, page
186
- value: 'button' # role name, testid value, CSS selector, etc.
187
- name: 'Submit' # accessible name
188
- nth: 0 # element index, 0-indexed (omit for strict mode)
189
- exact: true # exact name match (avoid "Submit" matching "Submit Form")
190
- scope: 'desktop navigation' # scope to parent landmark
191
- match: 'exact' # exact text match for getByText
164
+ search:
165
+ type: 'placeholder' # testid, role, placeholder, label, text, locator, page
166
+ value: 'Search users' # placeholder text, role name, CSS selector, etc.
167
+ name: 'Search' # accessible name (for role type)
168
+ nth: 0 # element index (for multiple matches)
192
169
  ```
193
170
 
194
- Locator priority: `data-testid` > `role+name` > `label` > `text` > `CSS`
171
+ Priority: `data-testid` > `role+name` > `placeholder` > `label` > `text` > `CSS locator`
195
172
 
196
173
  ---
197
174
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sun-asterisk/sungen",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Deterministic E2E Test Compiler - Gherkin + Selectors → Playwright tests",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/cli/index.ts CHANGED
@@ -16,7 +16,7 @@ async function main() {
16
16
  program
17
17
  .name('sungen')
18
18
  .description('Deterministic E2E Test Compiler — Gherkin + Selectors → Playwright')
19
- .version('2.1.0');
19
+ .version('2.2.0');
20
20
 
21
21
  // Global options
22
22
  program
@@ -3,13 +3,19 @@
3
3
  * Defines the contract for framework-specific test code generation
4
4
  */
5
5
 
6
+ export interface AuthGroup {
7
+ authRole?: string;
8
+ scenarios: string[];
9
+ }
10
+
6
11
  export interface TestFileData {
7
12
  imports: string;
8
13
  featureName: string;
9
14
  featureDescription?: string;
10
15
  background?: string;
11
16
  scenarios: string[];
12
- authRole?: string; // Feature-level auth role (optional)
17
+ authGroups?: AuthGroup[]; // Grouped by auth role for nested describes
18
+ singleAuthRole?: string; // Auth role when all scenarios share the same role
13
19
  }
14
20
 
15
21
  export interface ScenarioData {
@@ -1,7 +1,3 @@
1
- {{#if authRole}}
2
- test.use({ storageState: 'specs/.auth/{{authRole}}.json' });
3
-
4
- {{/if}}
5
1
  test('{{scenarioName}}', async ({ page }) => {
6
2
  {{#each steps}}
7
3
  {{#if comment}}
@@ -0,0 +1 @@
1
+ await {{> locator}}.dragTo({{targetLocator}});
@@ -0,0 +1,11 @@
1
+ {{#if (eq direction 'expand')}}
2
+ if (await {{> locator}}.getAttribute('aria-expanded') !== 'true') {
3
+ await {{> locator}}.click();
4
+ }
5
+ await expect({{> locator}}).toHaveAttribute('aria-expanded', 'true');
6
+ {{else}}
7
+ if (await {{> locator}}.getAttribute('aria-expanded') !== 'false') {
8
+ await {{> locator}}.click();
9
+ }
10
+ await expect({{> locator}}).toHaveAttribute('aria-expanded', 'false');
11
+ {{/if}}
@@ -1 +1 @@
1
- await {{> locator}}.fill('{{fillValue}}');
1
+ await {{> locator}}.fill('{{escapeQuotes fillValue}}');
@@ -1,3 +1,3 @@
1
1
  const editorLocator = {{> locator}};
2
2
  await editorLocator.click();
3
- await editorLocator.pressSequentially('{{fillValue}}');
3
+ await editorLocator.pressSequentially('{{escapeQuotes fillValue}}');
@@ -1,2 +1 @@
1
- await page.waitForLoadState('networkidle');
2
1
  await expect({{> locator}}).toBeChecked();
@@ -1,3 +1,2 @@
1
- await page.waitForLoadState('networkidle');
2
1
  const {{columnIndexVar}} = (await page.getByRole('columnheader').allTextContents()).findIndex(h => h.includes('{{columnName}}'));
3
2
  await expect(page.getByRole('row').nth({{rowNth}}).getByRole('cell').nth({{columnIndexVar}})).toHaveText('{{dataValue}}');
@@ -1,2 +1 @@
1
- await page.waitForLoadState('networkidle');
2
1
  await expect({{> locator}}).toContainText('{{expectedText}}');
@@ -1,2 +1 @@
1
- await page.waitForLoadState('networkidle');
2
1
  await expect({{> locator}}).toHaveCount({{expectedCount}});
@@ -1,2 +1 @@
1
- await page.waitForLoadState('networkidle');
2
1
  await expect({{> locator}}).toBeDisabled();
@@ -1,2 +1 @@
1
- await page.waitForLoadState('networkidle');
2
1
  await expect({{> locator-base}}.filter({ hasText: /^{{escapeRegex dataValue}}$/ }){{> locator-nth}}).toBeDisabled();
@@ -1,4 +1,3 @@
1
- await page.waitForLoadState('networkidle');
2
1
  {{#if name}}
3
2
  await expect(page.getByRole('{{role}}', { name: '{{escapeQuotes name}}'{{#if exact}}, exact: true{{/if}} }).filter({ hasText: /^{{escapeRegex dataValue}}$/ }){{#if (gte nth 0)}}.nth({{nth}}){{/if}}).toBeDisabled();
4
3
  {{else}}
@@ -1,2 +1 @@
1
- await page.waitForLoadState('networkidle');
2
1
  await expect(page.getByText('{{escapeQuotes selectorRef}}').filter({ hasText: {{#if (eq selectorValue "")}}/.*{{escapeRegex dataValue}}$/{{else}}'{{escapeQuotes dataValue}}'{{/if}} }){{#if (gte nth 0)}}.nth({{nth}}){{/if}}).toBeDisabled();
@@ -1,2 +1 @@
1
- await page.waitForLoadState('networkidle');
2
1
  await expect({{> locator}}).toBeEmpty();
@@ -1,2 +1 @@
1
- await page.waitForLoadState('networkidle');
2
1
  await expect({{> locator}}).toBeEnabled();
@@ -1,2 +1 @@
1
- await page.waitForLoadState('networkidle');
2
1
  await expect({{> locator}}).toBeFocused();
@@ -1,2 +1 @@
1
- await page.waitForLoadState('networkidle');
2
1
  await expect({{> locator}}).toHaveText('{{expectedText}}');
@@ -1,2 +1 @@
1
- await page.waitForLoadState('networkidle');
2
1
  await expect(page.getByRole('dialog').getByRole('heading', { name: '{{escapeQuotes dataValue}}' })).toBeHidden();
@@ -1,2 +1 @@
1
- await page.waitForLoadState('networkidle');
2
1
  await expect({{> locator-base}}.filter({ hasText: /^{{escapeRegex dataValue}}$/ }){{> locator-nth}}).toBeHidden();
@@ -1,4 +1,3 @@
1
- await page.waitForLoadState('networkidle');
2
1
  {{#if name}}
3
2
  await expect(page.getByRole('{{role}}', { name: '{{escapeQuotes name}}'{{#if exact}}, exact: true{{/if}} }).filter({ hasText: /^{{escapeRegex dataValue}}$/ }){{#if (gte nth 0)}}.nth({{nth}}){{/if}}).toBeHidden();
4
3
  {{else}}
@@ -1,2 +1 @@
1
- await page.waitForLoadState('networkidle');
2
1
  await expect(page.getByText('{{escapeQuotes selectorRef}}').filter({ hasText: {{#if (eq selectorValue "")}}/.*{{escapeRegex dataValue}}$/{{else}}'{{escapeQuotes dataValue}}'{{/if}} }){{#if (gte nth 0)}}.nth({{nth}}){{/if}}).toBeHidden();
@@ -1,2 +1 @@
1
- await page.waitForLoadState('networkidle');
2
1
  await expect({{> locator}}).toBeHidden();
@@ -1,2 +1 @@
1
- await page.waitForLoadState('networkidle');
2
1
  await expect(page.getByText(/{{#if label}}{{escapeRegex label}}\s*{{else}}(?:^|\s){{/if}}{{escapeRegex dataValue}}$/)).toBeVisible();
@@ -1,2 +1 @@
1
- await page.waitForLoadState('networkidle');
2
1
  await expect({{> locator}}.getByRole('listitem')).toHaveCount({{expectedCount}});
@@ -0,0 +1 @@
1
+ await expect({{> locator}}).toHaveAttribute('aria-busy', 'true');
@@ -1,2 +1 @@
1
- await page.waitForLoadState('networkidle');
2
1
  await expect({{> locator}}).toBeChecked({ checked: false });
@@ -1,2 +1 @@
1
- await page.waitForLoadState('networkidle');
2
1
  await expect(page).toHaveURL(/{{pathRegex}}/);
@@ -0,0 +1 @@
1
+ await expect({{> locator}}).toHaveAttribute('aria-selected', 'true');
@@ -0,0 +1 @@
1
+ await expect({{> locator}}).toHaveAttribute('aria-sort', '{{sortDirection}}');
@@ -1,3 +1,2 @@
1
- await page.waitForLoadState('networkidle');
2
1
  { const tableRow = {{> locator}}.getByRole('row').filter({ hasText: '{{escapeQuotes filterValue}}' });
3
2
  await expect(tableRow.getByRole('cell').filter({ hasText: '{{escapeQuotes cellValue}}' })).toBeVisible(); }
@@ -1,3 +1,2 @@
1
- await page.waitForLoadState('networkidle');
2
1
  { const tableRow = {{> locator}}.getByRole('row').nth({{rowIndex}});
3
2
  await expect(tableRow.getByRole('cell').filter({ hasText: '{{escapeQuotes cellValue}}' })).toBeVisible(); }
@@ -1,2 +1 @@
1
- await page.waitForLoadState('networkidle');
2
1
  await expect({{> locator}}.getByRole('columnheader', { name: '{{escapeQuotes columnName}}' })).toBeVisible();
@@ -1,2 +1 @@
1
- await page.waitForLoadState('networkidle');
2
1
  await expect({{> locator}}.locator('tbody').getByRole('row')).toHaveCount(0);
@@ -1,2 +1 @@
1
- await page.waitForLoadState('networkidle');
2
1
  await expect({{> locator}}.locator('tbody').getByRole('row')).toHaveCount({{expectedCount}});
@@ -1,2 +1 @@
1
- await page.waitForLoadState('networkidle');
2
1
  await expect({{> locator}}.getByRole('row').filter({ hasText: '{{escapeQuotes filterValue}}' })).toBeVisible();
@@ -1,2 +1 @@
1
- await page.waitForLoadState('networkidle');
2
1
  await expect({{> locator}}.getByRole('row').filter({ hasText: '{{escapeQuotes filterValue}}' })).toHaveCount(0);
@@ -1,2 +1 @@
1
- await page.waitForLoadState('networkidle');
2
1
  await expect({{> locator}}).toBeVisible();
@@ -1,2 +1 @@
1
- await page.waitForLoadState('networkidle');
2
1
  await expect(page.getByRole('dialog').getByRole('heading', { name: '{{escapeQuotes dataValue}}' })).toBeVisible();
@@ -1,2 +1 @@
1
- await page.waitForLoadState('networkidle');
2
1
  await expect({{> locator-base}}.filter({ hasText: /^{{escapeRegex dataValue}}$/ }){{> locator-nth}}).toBeVisible();
@@ -1,4 +1,3 @@
1
- await page.waitForLoadState('networkidle');
2
1
  {{#if name}}
3
2
  {{#if dataValue}}
4
3
  await expect(page.getByRole('{{role}}', { name: '{{escapeQuotes name}}'{{#if exact}}, exact: true{{/if}} }).filter({ hasText: /^{{escapeRegex dataValue}}$/ }){{#if (gte nth 0)}}.nth({{nth}}){{/if}}).toBeVisible();
@@ -1,2 +1 @@
1
- await page.waitForLoadState('networkidle');
2
1
  await expect(page.getByText('{{escapeQuotes value}}'{{#if exact}}, { exact: true }{{/if}}){{#if (gte nth 0)}}.nth({{nth}}){{/if}}).toBeVisible();
@@ -1,2 +1 @@
1
- await page.waitForLoadState('networkidle');
2
1
  await expect(page.getByText('{{escapeQuotes selectorValue}}').filter({ hasText: /.*{{escapeRegex dataValue}}$/ }){{#if (gte nth 0)}}.nth({{nth}}){{/if}}).toBeVisible();
@@ -1 +1 @@
1
- await page.goto('{{#if baseURL}}{{baseURL}}{{/if}}{{path}}', { waitUntil: 'networkidle' });
1
+ await page.goto('{{#if baseURL}}{{baseURL}}{{/if}}{{path}}', { waitUntil: 'load' });
@@ -8,12 +8,37 @@
8
8
  {{/if}}
9
9
 
10
10
  test.describe('{{featureName}}', () => {
11
+ {{#if singleAuthRole}}
12
+ test.use({ storageState: 'specs/.auth/{{singleAuthRole}}.json' });
13
+
14
+ {{/if}}
11
15
  {{#if background}}
12
16
  {{background}}
13
17
 
14
18
  {{/if}}
19
+ {{#if authGroups}}
20
+ {{#each authGroups}}
21
+ {{#if authRole}}
22
+ test.describe('{{authRole}}', () => {
23
+ test.use({ storageState: 'specs/.auth/{{authRole}}.json' });
24
+
25
+ {{#each scenarios}}
26
+ {{indent this 2}}
27
+
28
+ {{/each}}
29
+ });
30
+
31
+ {{else}}
32
+ {{#each scenarios}}
33
+ {{this}}
34
+
35
+ {{/each}}
36
+ {{/if}}
37
+ {{/each}}
38
+ {{else}}
15
39
  {{#each scenarios}}
16
40
  {{this}}
17
41
 
18
42
  {{/each}}
43
+ {{/if}}
19
44
  });
@@ -229,7 +229,8 @@ export class CodeGenerator {
229
229
 
230
230
  // Generate all scenarios with feature tags for inheritance
231
231
  // Skip scenarios tagged with @manual
232
- const scenarios: string[] = [];
232
+ // Track auth role per scenario for grouping
233
+ const renderedScenarios: Array<{ code: string; authRole?: string }> = [];
233
234
  for (const scenario of feature.scenarios) {
234
235
  if (isManual(scenario.tags)) {
235
236
  if (this.options.verbose) {
@@ -237,22 +238,63 @@ export class CodeGenerator {
237
238
  }
238
239
  continue;
239
240
  }
240
- scenarios.push(
241
- await this.generateScenario(
242
- scenario,
243
- !!feature.background,
244
- feature.tags || []
245
- )
241
+
242
+ // Resolve auth tags for @extend scenarios (same logic as generateScenario)
243
+ let authFeatureTags = feature.tags || [];
244
+ if (scenario.extendsName) {
245
+ const baseScenario = this.stepsRegistry.get(scenario.extendsName);
246
+ if (baseScenario) {
247
+ authFeatureTags = [...baseScenario.tags, ...authFeatureTags];
248
+ }
249
+ }
250
+ const authRole = getEffectiveAuthRole(scenario.tags, authFeatureTags);
251
+
252
+ const code = await this.generateScenario(
253
+ scenario,
254
+ !!feature.background,
255
+ feature.tags || []
246
256
  );
257
+ renderedScenarios.push({ code, authRole });
247
258
  }
248
259
 
260
+ // Group scenarios by auth role for nested test.describe blocks
261
+ // This ensures test.use({ storageState }) only applies to its group
262
+ const authGroupMap = new Map<string, string[]>();
263
+ const groupOrder: string[] = [];
264
+ for (const { code, authRole } of renderedScenarios) {
265
+ const key = authRole || '';
266
+ if (!authGroupMap.has(key)) {
267
+ authGroupMap.set(key, []);
268
+ groupOrder.push(key);
269
+ }
270
+ authGroupMap.get(key)!.push(code);
271
+ }
272
+
273
+ const authGroups = groupOrder.map(key => ({
274
+ authRole: key || undefined,
275
+ scenarios: authGroupMap.get(key)!,
276
+ }));
277
+
278
+ // Determine rendering strategy:
279
+ // - Single group: flat structure (test.use at describe level if auth)
280
+ // - Multiple groups: nested describes per auth role
281
+ const needsGrouping = authGroups.length > 1;
282
+ const scenarios = renderedScenarios.map(s => s.code);
283
+
284
+ // For single group, extract the auth role to put test.use at describe level
285
+ const singleAuthRole = !needsGrouping && authGroups.length === 1
286
+ ? authGroups[0].authRole
287
+ : undefined;
288
+
249
289
  // Use adapter to render the complete test file structure
250
290
  return this.adapter.renderTestFile({
251
291
  imports: '', // Not used in template as it's rendered separately
252
292
  featureName: feature.name,
253
293
  featureDescription: feature.description,
254
294
  background,
255
- scenarios,
295
+ scenarios: needsGrouping ? [] : scenarios,
296
+ authGroups: needsGrouping ? authGroups : undefined,
297
+ singleAuthRole,
256
298
  });
257
299
  }
258
300